为什么这个 Java HttpsServer 没有成功完成 TLS 握手?

Why doesn't this Java HttpsServer successfully complete a TLS handshake?

我已尝试在 Java 中基于 设置尽可能少的 HTTPS 服务器,但有一个区别:它使用由静态 CA 签名的动态生成的证书。 (这样做的目的是为了方便中间人代理,但为了简单起见,我在这里只使用普通服务器。)

import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManagerFactory;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

public class MinimalHttpsServer {
    public static void main(String[] args) throws Exception {
        var server = HttpsServer.create(
                new InetSocketAddress("localhost", 8443), /* backlog= */ 0);
        server.createContext("/", exchange -> {
            exchange.sendResponseHeaders(/* rCode= */ 204,
                    /* responseLength= */ 0);
            exchange.close();
        });
        var rootStore = KeyStore.getInstance("jks");
        try (InputStream rootStoreStream = Files
                .newInputStream(Paths.get("cybervillainsCA.jks"))) {
            rootStore.load(rootStoreStream, "password".toCharArray());
        }
        var leafGenerator = KeyPairGenerator.getInstance("RSA");
        leafGenerator.initialize(/* keysize= */ 3072);
        KeyPair leafPair = leafGenerator.generateKeyPair();
        var leafStore = KeyStore.getInstance("jks");
        leafStore.load(/* stream= */ null, /* password= */ null);
        String issuerName = "O = CyberVillians.com, OU = CyberVillians Certification Authority, C = US"; // [sic]
        var now = Instant.now();
        X509Certificate leafCert = new JcaX509CertificateConverter()
                .getCertificate(new JcaX509v3CertificateBuilder(
                        new X500Principal(issuerName),
                        /* serial= */ BigInteger.valueOf(now.toEpochMilli()),
                        /* notBefore= */ Date
                                .from(now.minus(Duration.ofMinutes(5))),
                        /* notAfter= */ Date.from(now.plus(Duration.ofDays(1))),
                        /* subject= */ new X500Principal("CN = localhost"),
                        leafPair.getPublic()).build(
                                new JcaContentSignerBuilder("SHA256withRSA")
                                        .build((PrivateKey) rootStore.getKey(
                                                "signingcertprivkey",
                                                "password".toCharArray()))));
        leafStore.setCertificateEntry("leafcert", leafCert);
        leafStore.setKeyEntry("leafcertprivkey", leafPair.getPrivate(),
                /* password= */ new char[0], new Certificate[] { leafCert,
                        rootStore.getCertificate("signingcert") });
        var keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        keyManagerFactory.init(leafStore, /* password= */ new char[0]);
        var trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        trustManagerFactory.init(leafStore);
        var outerContext = SSLContext.getInstance("TLS");
        outerContext.init(keyManagerFactory.getKeyManagers(),
                trustManagerFactory.getTrustManagers(), /* random= */ null);
        server.setHttpsConfigurator(new HttpsConfigurator(outerContext) {
            @Override
            public void configure(HttpsParameters params) {
                try {
                    var innerContext = SSLContext.getDefault();
                    SSLEngine engine = innerContext.createSSLEngine();
                    params.setNeedClientAuth(false);
                    params.setCipherSuites(engine.getEnabledCipherSuites());
                    params.setProtocols(engine.getEnabledProtocols());
                    params.setSSLParameters(
                            innerContext.getDefaultSSLParameters());
                } catch (NoSuchAlgorithmException ex) {
                    ex.printStackTrace();
                    System.exit(1);
                }
            }
        });
        server.setExecutor(Runnable::run);
        System.err.println("Serving...");
        server.start();
    }
}

要构建并 运行 这个(在类 Unix 系统上):

wget \
  https://downloads.bouncycastle.org/java/bcpkix-jdk15on-170.jar \
  https://downloads.bouncycastle.org/java/bcprov-jdk15on-170.jar \
  https://downloads.bouncycastle.org/java/bcutil-jdk15on-170.jar \
  https://raw.githubusercontent.com/lightbody/browsermob-proxy/ec2f7bdf00336af7009cdf59ab3cac128ace8ee8/browsermob-core/src/main/resources/sslSupport/cybervillainsCA.jks
javac -cp bcpkix-jdk15on-170.jar:bcprov-jdk15on-170.jar MinimalHttpsServer.java
java -cp bcpkix-jdk15on-170.jar:bcprov-jdk15on-170.jar:bcutil-jdk15on-170.jar:. MinimalHttpsServer

然后,检查它是否正常工作:

keytool -exportcert -keystore cybervillainsCA.jks -alias signingcert -storepass password -rfc -file cybervillainsCA.pem
curl --cacert cybervillainsCA.pem --verbose https://localhost:8443

此操作失败,输出如下:

*   Trying 127.0.0.1:8443...
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: cybervillainsCA.pem
*  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection 0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

我做错了什么?

这是最终起作用的:

X509Certificate leafCert = new JcaX509CertificateConverter()
        .getCertificate(new JcaX509v3CertificateBuilder(
                rootStore.getCertificate("signingcert"),
                /* serial= */ BigInteger.valueOf(now.toEpochMilli()),
                /* notBefore= */ Date
                        .from(now.minus(Duration.ofMinutes(5))),
                /* notAfter= */ Date.from(now.plus(Duration.ofDays(1))),
                new X500Name(new RDN[0]), leafPair.getPublic())
                        .addExtension(Extension.subjectAlternativeName,
                                /* isCritical= */ true,
                                new GeneralNames(new GeneralName(
                                        GeneralName.dNSName,
                                        "localhost")))
                        .addExtension(Extension.extendedKeyUsage,
                                /* isCritical= */ false,
                                new ExtendedKeyUsage(
                                        KeyPurposeId.id_kp_serverAuth))
                        .build(new JcaContentSignerBuilder(
                                "SHA256withRSA").build(
                                        (PrivateKey) rootStore.getKey(
                                                "signingcertprivkey",
                                                "password"
                                                        .toCharArray()))));