I've tried to set up the minimal possible HTTPS server in Java based on Simple Java HTTPS server, with one difference: it uses a dynamically generated certificate signed by a static CA. (The purpose of this is to facilitate man-in-the-middle proxying, but for simplicity I'm just using a regular server here.)
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();
}
}
To build and run this (on a Unix-like system):
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
And then, to check whether it's working:
keytool -exportcert -keystore cybervillainsCA.jks -alias signingcert -storepass password -rfc -file cybervillainsCA.pem
curl --cacert cybervillainsCA.pem --verbose https://localhost:8443
This fails with the following output:
* 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.
What am I doing wrong?
CodePudding user response:
Here's what ended up working:
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()))));