All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.netty.pkitesting.RevocationServer Maven / Gradle / Ivy

/*
 * Copyright 2024 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.pkitesting;

import com.sun.net.httpserver.HttpServer;

import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A simple HTTP server that serves Certificate Revocation Lists.
 * 

* Issuer certificates can be registered with the server, and revocations of their certificates and be published * and added to the revocation lists. *

* The server is only intended for testing usage, and runs entirely in a single thread. * * @implNote The CRLs will have the same very short life times, to minimize caching effects in tests. * This currently means the time in the "this update" and "next update" fields are set to the same value. */ public final class RevocationServer { private static volatile RevocationServer instance; private final HttpServer crlServer; private final String crlBaseAddress; private final AtomicInteger issuerCounter; private final ConcurrentMap issuers; private final ConcurrentMap paths; /** * Get the shared revocation server instance. * This will start the server, if it isn't already running, and bind it to a random port on the loopback address. * @return The revocation server instance. * @throws Exception If the server failed to start. */ public static RevocationServer getInstance() throws Exception { if (instance != null) { return instance; } synchronized (RevocationServer.class) { RevocationServer server = instance; if (server == null) { server = new RevocationServer(); server.start(); instance = server; } return server; } } private RevocationServer() throws Exception { // Use the JDK built-in HttpServer to avoid any circular dependencies with Netty itself. crlServer = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); crlBaseAddress = "http://localhost:" + crlServer.getAddress().getPort(); issuerCounter = new AtomicInteger(); issuers = new ConcurrentHashMap<>(); paths = new ConcurrentHashMap<>(); crlServer.createContext("/", exchange -> { if ("GET".equals(exchange.getRequestMethod())) { String path = exchange.getRequestURI().getPath(); CrlInfo info = paths.get(path); if (info == null) { exchange.sendResponseHeaders(404, 0); exchange.close(); return; } byte[] crl = generateCrl(info); exchange.getResponseHeaders().put("Content-Type", Collections.singletonList("application/pkix-crl")); exchange.sendResponseHeaders(200, crl.length); try (OutputStream out = exchange.getResponseBody()) { out.write(crl); out.flush(); } } else { exchange.sendResponseHeaders(405, 0); } exchange.close(); }); } private void start() { if (Thread.currentThread().isDaemon()) { crlServer.start(); } else { // It's important the CRL server creates a daemon thread, // because it's a singleton and won't be stopped except by terminating the JVM. // Threads in the ForkJoin common pool are always daemon, and JUnit 5 initializes // it anyway, so we can let it call start() for us. ForkJoinPool.commonPool().execute(crlServer::start); } } /** * Register an issuer with the revocation server. * This must be done before CRLs can be served for that issuer, and before any of its certificates can be revoked. * @param issuer The issuer to register. */ public void register(X509Bundle issuer) { issuers.computeIfAbsent(issuer.getCertificate(), bundle -> { String path = "/crl/" + issuerCounter.incrementAndGet() + ".crl"; URI uri = URI.create(crlBaseAddress + path); CrlInfo info = new CrlInfo(issuer, uri); paths.put(path, info); return info; }); } /** * Revoke the given certificate with the given revocation time. *

* The issuer of the given certificate must be {@linkplain #register(X509Bundle) registered} before its certifiactes * can be revoked. * @param cert The certificate to revoke. * @param time The time of revocation. */ public void revoke(X509Bundle cert, Instant time) { X509Certificate[] certPath = cert.getCertificatePathWithRoot(); X509Certificate issuer = certPath.length == 1 ? certPath[0] : certPath[1]; CrlInfo info = issuers.get(issuer); if (info != null) { info.revokedCerts.put(cert.getCertificate().getSerialNumber(), time); } else { throw new IllegalArgumentException("Not a registered issuer: " + issuer.getSubjectX500Principal()); } } /** * Get the URI of the Certificate Revocation List for the given issuer. * @param issuer The issuer to get the CRL for. * @return The URI to the CRL for the given issuer, * or {@code null} if the issuer is not {@linkplain #register(X509Bundle) registered}. */ public URI getCrlUri(X509Bundle issuer) { CrlInfo info = issuers.get(issuer.getCertificate()); if (info != null) { return info.uri; } return null; } private static byte[] generateCrl(CrlInfo info) { X509Bundle issuer = info.issuer; Map certs = info.revokedCerts; Instant now = Instant.now(); CertificateList list = new CertificateList(issuer, now, now, certs.entrySet()); try { Signed signed = new Signed(list.getEncoded(), issuer); return signed.getEncoded(); } catch (Exception e) { throw new IllegalStateException("Failed to sign CRL", e); } } private static final class CrlInfo { private final X509Bundle issuer; private final URI uri; private final Map revokedCerts; CrlInfo(X509Bundle issuer, URI uri) { this.issuer = issuer; this.uri = uri; revokedCerts = new ConcurrentHashMap<>(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy