Skip to content
Merged
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ mkdir -p ~/dev/certificates
mkcert -cert-file ~/dev/certificates/example.org.pem -key-file ~/dev/certificates/example.org.key example.org
```

In production environments, your certificate will likely be signed by one or more intermediate Certificate Authorities. In addition to the server certificate, ensure that all intermediate CA certificates in the chain are included in your pem file.

Now you can load these into the HTTP server like this:

```java
Expand Down
2 changes: 1 addition & 1 deletion build.savant
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
restifyVersion = "4.1.2"
testngVersion = "7.6.1"

project(group: "io.fusionauth", name: "java-http", version: "0.1.10", licenses: ["ApacheV2_0"]) {
project(group: "io.fusionauth", name: "java-http", version: "0.1.11", licenses: ["ApacheV2_0"]) {
workflow {
fetch {
cache()
Expand Down
107 changes: 104 additions & 3 deletions src/main/java/io/fusionauth/http/security/SecurityTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.security.auth.x500.X500Principal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
Expand All @@ -28,10 +29,13 @@
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;

/**
* A toolkit for security helper methods.
Expand Down Expand Up @@ -74,12 +78,87 @@ public static SSLContext clientContext(Certificate certificate) throws GeneralSe
return context;
}

public static Certificate parseCertificate(String certificate) throws CertificateException {
/**
* Parses a single certificate from a PEM string.
*
* @param certificateString PEM-formatted certificate text.
* @return The first {@link Certificate} encoded in the file.
* @throws CertificateException If unable to parse PEM content.
*/
public static Certificate parseCertificate(String certificateString) throws CertificateException {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
byte[] certBytes = parseDERFromPEM(certificate, CERT_START, CERT_END);
return factory.generateCertificate(new ByteArrayInputStream(certBytes));
var is = new ByteArrayInputStream(certificateString.getBytes());
return factory.generateCertificates(is).stream().findFirst().get();
}

/**
* Parses and re-orders multiple Certificates from a PEM-formatted string into an ordered certificate chain array.
*
* @param certificateString the PEM-formatted content of one or more certificates in a chain.
* @return An array of {@link Certificate}s ordered from the end-entity through each supplied issuer.
* @throws CertificateException If unable to parse PEM content.
*/
public static Certificate[] parseCertificates(String certificateString) throws CertificateException {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
var is = new ByteArrayInputStream(certificateString.getBytes());
var certs = (Collection<X509Certificate>) factory.generateCertificates(is);
return reorderCertificates(certs);
}

/**
* Returns an array of Certificates ordered starting from the end-entity through each supplied issuer in the List.
*
* @param certs The certificates to re-order.
* @return An array of {@link Certificate}s ordered from the end-entity through each supplied issuer.
* @throws IllegalArgumentException if certs is empty or missing an intermediate issuer cert.
*/
private static Certificate[] reorderCertificates(Collection<X509Certificate> certs) {
if (certs.isEmpty()) {
throw new IllegalArgumentException("Empty certificate list");
}

var orderedCerts = new X509Certificate[certs.size()];

// Short circuit if only one cert is in collection
if (certs.size() == 1) {
orderedCerts[0] = certs.stream().findFirst().get();
return orderedCerts;
}

// Index certificates by Issuer and Subject
var certsByIssuer = new HashMap<X500Principal, X509Certificate>(certs.size());
var certsBySubject = new HashMap<X500Principal, X509Certificate>(certs.size());

// Load all certificates into maps
for (X509Certificate cert : certs) {
certsByIssuer.put(cert.getIssuerX500Principal(), cert);
certsBySubject.put(cert.getSubjectX500Principal(), cert);
}

// Find the server certificate. It's the one that no other certificate refers to as its issuer. Store it in the first array element.
for (var cert : certs) {
if (!certsByIssuer.containsKey(cert.getSubjectX500Principal())) {
orderedCerts[0] = cert;
break;
}
}

// Start at the server cert and add each issuer certificate in order.
for (int i = 0; i < orderedCerts.length - 1; i++) {
var issuer = certsBySubject.get(orderedCerts[i].getIssuerX500Principal());
if (issuer == null) {
throw new IllegalArgumentException("Missing issuer cert for " + orderedCerts[i].getIssuerX500Principal());
}
orderedCerts[i + 1] = issuer;
}

return orderedCerts;
}


/**
* Parses a single object in a PEM-formatted string into a byte[].
*/
public static byte[] parseDERFromPEM(String pem, String beginDelimiter, String endDelimiter) {
int startIndex = pem.indexOf(beginDelimiter);
if (startIndex < 0) {
Expand Down Expand Up @@ -124,4 +203,26 @@ public static SSLContext serverContext(Certificate certificate, PrivateKey priva
context.init(kmf.getKeyManagers(), null, null);
return context;
}

/**
* This creates an in-memory keystore containing the certificate chain and private key and initializes the SSLContext with the key
* material it contains.
*
* @param certificateChain The chain of certificates to include in the TLS negotiation. Should be ordered by end-entity first.
* @param privateKey The PrivateKey corresponding to the end-entity certificate in the chain.
* @return A SSLContext configured with the Certificate and Private Key.
*/
public static SSLContext serverContext(Certificate[] certificateChain, PrivateKey privateKey)
throws GeneralSecurityException, IOException {
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null);
keystore.setKeyEntry("key-alias", privateKey, "changeit".toCharArray(), certificateChain);

KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keystore, "changeit".toCharArray());

SSLContext context = SSLContext.getInstance("TLS");
context.init(kmf.getKeyManagers(), null, null);
return context;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
public class HTTPListenerConfiguration {
private final InetAddress bindAddress;

private final Certificate certificate;
private final Certificate[] certificateChain;

private final int port;

Expand All @@ -54,15 +54,15 @@ public HTTPListenerConfiguration(int port) {
this.bindAddress = allInterfaces();
this.port = port;
this.tls = false;
this.certificate = null;
this.certificateChain = null;
this.privateKey = null;
}

/**
* Stores the configuration for a single HTTP listener for the server. This constructor sets up a TLS based listener.
*
* @param port The port of this listener.
* @param certificate The certificate as a PEM encoded X.509 certificate String.
* @param certificate The certificate as a PEM encoded X.509 certificate String. May include intermediate CA certificates.
* @param privateKey The private key as a PKCS8 encoded DER private key.
* @throws GeneralSecurityException If the private key or certificate Strings were not valid and could not be parsed.
*/
Expand All @@ -73,7 +73,7 @@ public HTTPListenerConfiguration(int port, String certificate, String privateKey
this.bindAddress = allInterfaces();
this.port = port;
this.tls = true;
this.certificate = SecurityTools.parseCertificate(certificate);
this.certificateChain = SecurityTools.parseCertificates(certificate);
this.privateKey = SecurityTools.parsePrivateKey(privateKey);
}

Expand All @@ -91,7 +91,26 @@ public HTTPListenerConfiguration(int port, Certificate certificate, PrivateKey p
this.bindAddress = allInterfaces();
this.port = port;
this.tls = true;
this.certificate = certificate;
this.certificateChain = new Certificate[]{certificate};
this.privateKey = privateKey;
}

/**
* Stores the configuration for a single HTTP listener for the server. This constructor sets up a TLS based listener using the supplied
* certificate chain.
*
* @param port The port of this listener.
* @param certificateChain The certificate Object.
* @param privateKey The private key Object.
*/
public HTTPListenerConfiguration(int port, Certificate[] certificateChain, PrivateKey privateKey) {
Objects.requireNonNull(certificateChain);
Objects.requireNonNull(privateKey);

this.bindAddress = allInterfaces();
this.port = port;
this.tls = true;
this.certificateChain = certificateChain;
this.privateKey = privateKey;
}

Expand All @@ -107,7 +126,7 @@ public HTTPListenerConfiguration(InetAddress bindAddress, int port) {
this.bindAddress = bindAddress;
this.port = port;
this.tls = false;
this.certificate = null;
this.certificateChain = null;
this.privateKey = null;
}

Expand All @@ -129,7 +148,7 @@ public HTTPListenerConfiguration(InetAddress bindAddress, int port, String certi
this.bindAddress = bindAddress;
this.port = port;
this.tls = true;
this.certificate = SecurityTools.parseCertificate(certificate);
this.certificateChain = SecurityTools.parseCertificates(certificate);
this.privateKey = SecurityTools.parsePrivateKey(privateKey);
}

Expand All @@ -149,7 +168,7 @@ public HTTPListenerConfiguration(InetAddress bindAddress, int port, Certificate
this.bindAddress = bindAddress;
this.port = port;
this.tls = true;
this.certificate = certificate;
this.certificateChain = new Certificate[]{certificate};
this.privateKey = privateKey;
}

Expand All @@ -158,7 +177,15 @@ public InetAddress getBindAddress() {
}

public Certificate getCertificate() {
return certificate;
if (certificateChain != null && certificateChain.length > 0) {
return certificateChain[0];
} else {
return null;
}
}

public Certificate[] getCertificateChain() {
return certificateChain;
}

public int getPort() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public HTTPS11Processor(HTTP11Processor delegate, HTTPServerConfiguration config
this.logger = configuration.getLoggerFactory().getLogger(HTTPS11Processor.class);

if (listenerConfiguration.isTLS()) {
SSLContext context = SecurityTools.serverContext(listenerConfiguration.getCertificate(), listenerConfiguration.getPrivateKey());
SSLContext context = SecurityTools.serverContext(listenerConfiguration.getCertificateChain(), listenerConfiguration.getPrivateKey());
this.engine = context.createSSLEngine();
this.engine.setUseClientMode(false);

Expand Down
Loading