From dc17433634d80ad472585358cadc9d8026a85f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sat, 12 Jan 2019 20:22:07 +0100 Subject: [PATCH] Support IP identifiers for tls-alpn-01 --- .../acme4j/util/CertificateUtils.java | 43 +++++++++++++++++- .../acme4j/util/CertificateUtilsTest.java | 45 +++++++++++++++++-- src/site/markdown/challenge/tls-alpn-01.md | 6 +-- src/site/markdown/migration.md | 4 ++ 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java index 29e43add..89bb2c64 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java @@ -42,6 +42,8 @@ import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.shredzone.acme4j.Authorization; +import org.shredzone.acme4j.Identifier; import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; /** @@ -95,11 +97,36 @@ public final class CertificateUtils { * {@link TlsAlpn01Challenge#getAcmeValidation()} * @return Created certificate * @since 2.1 + * @deprecated Use {@link #createTlsAlpn01Certificate(KeyPair, Identifier, byte[])} + * and {@link Identifier#dns(String)}. If an {@link Authorization} + * instance is at hand, you can also use + * {@link Authorization#getIdentifier()}. */ + @Deprecated public static X509Certificate createTlsAlpn01Certificate(KeyPair keypair, String subject, byte[] acmeValidation) throws IOException { - Objects.requireNonNull(keypair, "keypair"); Objects.requireNonNull(subject, "subject"); + return createTlsAlpn01Certificate(keypair, Identifier.dns(subject), acmeValidation); + } + + /** + * Creates a self-signed {@link X509Certificate} that can be used for the + * {@link TlsAlpn01Challenge}. The certificate is valid for 7 days. + * + * @param keypair + * A domain {@link KeyPair} to be used for the challenge + * @param id + * The {@link Identifier} that is to be validated + * @param acmeValidation + * The value that is returned by + * {@link TlsAlpn01Challenge#getAcmeValidation()} + * @return Created certificate + * @since 2.6 + */ + public static X509Certificate createTlsAlpn01Certificate(KeyPair keypair, Identifier id, byte[] acmeValidation) + throws IOException { + Objects.requireNonNull(keypair, "keypair"); + Objects.requireNonNull(id, "id"); if (acmeValidation == null || acmeValidation.length != 32) { throw new IllegalArgumentException("Bad acmeValidation parameter"); } @@ -118,7 +145,19 @@ public final class CertificateUtils { issuer, keypair.getPublic()); GeneralName[] gns = new GeneralName[1]; - gns[0] = new GeneralName(GeneralName.dNSName, subject); + + switch (id.getType()) { + case Identifier.TYPE_DNS: + gns[0] = new GeneralName(GeneralName.dNSName, id.getDomain()); + break; + + case Identifier.TYPE_IP: + gns[0] = new GeneralName(GeneralName.iPAddress, id.getIP().getHostAddress()); + break; + + default: + throw new IllegalArgumentException("Unsupported Identifier type " + id.getType()); + } certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns)); certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation)); diff --git a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java index 6eeab50f..9d49867e 100644 --- a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java +++ b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.security.KeyPair; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; @@ -38,6 +40,7 @@ import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.junit.Test; +import org.shredzone.acme4j.Identifier; import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; import org.shredzone.acme4j.toolbox.AcmeUtils; @@ -83,8 +86,8 @@ public class CertificateUtilsTest { /** * Test if - * {@link CertificateUtils#createTlsAlpn01Certificate(KeyPair, String, byte[])} - * creates a good certificate. + * {@link CertificateUtils#createTlsAlpn01Certificate(KeyPair, Identifier, byte[])} + * with domain name creates a good certificate. */ @Test public void testCreateTlsAlpn01Certificate() throws IOException, CertificateParsingException { @@ -92,7 +95,7 @@ public class CertificateUtilsTest { String subject = "example.com"; byte[] acmeValidationV1 = AcmeUtils.sha256hash("rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ"); - X509Certificate cert = CertificateUtils.createTlsAlpn01Certificate(keypair, subject, acmeValidationV1); + X509Certificate cert = CertificateUtils.createTlsAlpn01Certificate(keypair, Identifier.dns(subject), acmeValidationV1); Instant now = Instant.now(); Instant end = now.plus(Duration.ofDays(8)); @@ -122,6 +125,23 @@ public class CertificateUtilsTest { } } + /** + * Test if + * {@link CertificateUtils#createTlsAlpn01Certificate(KeyPair, Identifier, byte[])} + * with IP creates a good certificate. + */ + @Test + public void testCreateTlsAlpn01CertificateWithIp() throws IOException, CertificateParsingException { + KeyPair keypair = KeyPairUtils.createKeyPair(2048); + InetAddress subject = InetAddress.getLocalHost(); + byte[] acmeValidationV1 = AcmeUtils.sha256hash("rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ"); + + X509Certificate cert = CertificateUtils.createTlsAlpn01Certificate(keypair, Identifier.ip(subject), acmeValidationV1); + + assertThat(cert.getSubjectX500Principal().getName(), is("CN=acme.invalid")); + assertThat(getIpSANs(cert), contains(subject)); + } + /** * Extracts all DNSName SANs from a certificate. * @@ -141,4 +161,23 @@ public class CertificateUtilsTest { return result; } + /** + * Extracts all IPAddress SANs from a certificate. + * + * @param cert + * {@link X509Certificate} + * @return Set of IPAddresses + */ + private Set getIpSANs(X509Certificate cert) throws CertificateParsingException, UnknownHostException { + Set result = new HashSet<>(); + + for (List list : cert.getSubjectAlternativeNames()) { + if (((Number) list.get(0)).intValue() == GeneralName.iPAddress) { + result.add(InetAddress.getByName(list.get(1).toString())); + } + } + + return result; + } + } diff --git a/src/site/markdown/challenge/tls-alpn-01.md b/src/site/markdown/challenge/tls-alpn-01.md index 50668df3..fa5cd29f 100644 --- a/src/site/markdown/challenge/tls-alpn-01.md +++ b/src/site/markdown/challenge/tls-alpn-01.md @@ -22,14 +22,14 @@ After that, configure your web server so it will use this certificate on an inco The `TlsAlpn01Challenge` class does not generate a self-signed certificate, as it would require _Bouncy Castle_. However, there is a utility method in the _acme4j-utils_ module for this use case: ```java -String subject = auth.getDomain(); +Identifier identifier = auth.getIdentifier(); KeyPair certKeyPair = KeyPairUtils.createKeyPair(2048); X509Certificate cert = CertificateUtils. - createTlsAlpn01Certificate(certKeyPair, subject, acmeValidation); + createTlsAlpn01Certificate(certKeyPair, identifier, acmeValidation); ``` -Now use `cert` and `certKeyPair` to let your web server respond to TLS requests containing an ALPN extension with the value `acme-tls/1` and a SNI extension containing `subject`. +Now use `cert` and `certKeyPair` to let your web server respond to TLS requests containing an ALPN extension with the value `acme-tls/1` and a SNI extension containing your subject.