From 6fc30900b94308b8651eee368c39e272fe8e40de Mon Sep 17 00:00:00 2001 From: Andy LoPresto Date: Thu, 9 Feb 2017 14:47:39 +0100 Subject: [PATCH] NIFI-3331 TLS Toolkit - add the possibility to define SAN in issued certificates. Added unit tests for SAN inclusion in CertificateUtils#generateIssuedCertificate() and TlsHelper#generateCertificationRequest(). Fixed typos. This closes #1491. Signed-off-by: Andy LoPresto --- .../nifi/security/util/CertificateUtils.java | 50 ++++++ .../security/util/CertificateUtilsTest.groovy | 170 +++++++++++------- .../toolkit/tls/configuration/TlsConfig.java | 9 + ...CertificateAuthorityClientCommandLine.java | 23 ++- ...TlsCertificateSigningRequestPerformer.java | 36 ++-- ...TlsCertificateAuthorityServiceHandler.java | 4 +- .../tls/standalone/TlsToolkitStandalone.java | 4 +- .../nifi/toolkit/tls/util/TlsHelper.java | 66 ++++--- ...ertificateSigningRequestPerformerTest.java | 2 +- ...ertificateAuthorityServiceHandlerTest.java | 2 +- .../nifi/toolkit/tls/util/TlsHelperTest.java | 118 +++++++++--- 11 files changed, 345 insertions(+), 139 deletions(-) diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java index 1a26cf435a..4d8033620f 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java @@ -17,7 +17,12 @@ package org.apache.nifi.security.util; import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Set; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.AttributeTypeAndValue; import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.X500Name; @@ -25,6 +30,7 @@ import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; @@ -37,6 +43,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -124,6 +131,7 @@ public final class CertificateUtils { this.description = description; } + @Override public String toString() { return "Client Auth: " + this.description + " (" + this.value + ")"; } @@ -541,6 +549,24 @@ public final class CertificateUtils { */ public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days) throws CertificateException { + return generateIssuedCertificate(dn, publicKey, null, issuer, issuerKeyPair, signingAlgorithm, days); + } + + /** + * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * + * @param dn the distinguished name to use + * @param publicKey the public key to issue the certificate to + * @param extensions extensions extracted from the CSR + * @param issuer the issuer's certificate + * @param issuerKeyPair the issuer's keypair + * @param signingAlgorithm the signing algorithm to use + * @param days the number of days it should be valid for + * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * @throws CertificateException if there is an error issuing the certificate + */ + public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, Extensions extensions, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days) + throws CertificateException { try { ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(issuerKeyPair.getPrivate()); SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); @@ -567,6 +593,11 @@ public final class CertificateUtils { // (2) extendedKeyUsage extension certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth})); + // (3) subjectAlternativeName + if(extensions != null && extensions.getExtension(Extension.subjectAlternativeName) != null) { + certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName)); + } + X509CertificateHolder certificateHolder = certBuilder.build(sigGen); return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder); } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) { @@ -613,6 +644,25 @@ public final class CertificateUtils { } } + /** + * Extract extensions from CSR object + */ + public static Extensions getExtensionsFromCSR(JcaPKCS10CertificationRequest csr) { + Attribute[] attributess = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest); + for (Attribute attribute : attributess) { + ASN1Set attValue = attribute.getAttrValues(); + if (attValue != null) { + ASN1Encodable extension = attValue.getObjectAt(0); + if (extension instanceof Extensions) { + return (Extensions) extension; + } else if (extension instanceof DERSequence) { + return Extensions.getInstance(extension); + } + } + } + return null; + } + private CertificateUtils() { } } diff --git a/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy index 81e8ccaa9f..5deb2782a6 100644 --- a/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy +++ b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy @@ -16,6 +16,11 @@ */ package org.apache.nifi.security.util +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.Extensions +import org.bouncycastle.asn1.x509.ExtensionsGenerator +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames import org.bouncycastle.operator.OperatorCreationException import org.junit.After import org.junit.Before @@ -51,17 +56,18 @@ import static org.junit.Assert.assertTrue @RunWith(JUnit4.class) class CertificateUtilsTest extends GroovyTestCase { - private static final Logger logger = LoggerFactory.getLogger(CertificateUtilsTest.class); + private static final Logger logger = LoggerFactory.getLogger(CertificateUtilsTest.class) - private static final int KEY_SIZE = 2048; + private static final int KEY_SIZE = 2048 - private static final long YESTERDAY = System.currentTimeMillis() - 24 * 60 * 60 * 1000; - private static final long ONE_YEAR_FROM_NOW = System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000; - private static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; - private static final String PROVIDER = "BC"; + private static final int DAYS_IN_YEAR = 365 + private static final long YESTERDAY = System.currentTimeMillis() - 24 * 60 * 60 * 1000 + private static final long ONE_YEAR_FROM_NOW = System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000 + private static final String SIGNATURE_ALGORITHM = "SHA256withRSA" + private static final String PROVIDER = "BC" - private static final String SUBJECT_DN = "CN=NiFi Test Server,OU=Security,O=Apache,ST=CA,C=US"; - private static final String ISSUER_DN = "CN=NiFi Test CA,OU=Security,O=Apache,ST=CA,C=US"; + private static final String SUBJECT_DN = "CN=NiFi Test Server,OU=Security,O=Apache,ST=CA,C=US" + private static final String ISSUER_DN = "CN=NiFi Test CA,OU=Security,O=Apache,ST=CA,C=US" @BeforeClass static void setUpOnce() { @@ -88,9 +94,9 @@ class CertificateUtilsTest extends GroovyTestCase { * @throws java.security.NoSuchAlgorithmException if the RSA algorithm is not available */ private static KeyPair generateKeyPair() throws NoSuchAlgorithmException { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(KEY_SIZE); - return keyPairGenerator.generateKeyPair(); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(KEY_SIZE) + return keyPairGenerator.generateKeyPair() } /** @@ -108,8 +114,8 @@ class CertificateUtilsTest extends GroovyTestCase { */ private static X509Certificate generateCertificate(String dn) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, SignatureException, InvalidKeyException, OperatorCreationException { - KeyPair keyPair = generateKeyPair(); - return CertificateUtils.generateSelfSignedX509Certificate(keyPair, dn, SIGNATURE_ALGORITHM, 365); + KeyPair keyPair = generateKeyPair() + return CertificateUtils.generateSelfSignedX509Certificate(keyPair, dn, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) } /** @@ -129,15 +135,15 @@ class CertificateUtilsTest extends GroovyTestCase { */ private static X509Certificate generateIssuedCertificate(String dn, X509Certificate issuer, KeyPair issuerKey) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, SignatureException, InvalidKeyException, OperatorCreationException { - KeyPair keyPair = generateKeyPair(); - return CertificateUtils.generateIssuedCertificate(dn, keyPair.getPublic(), issuer, issuerKey, SIGNATURE_ALGORITHM, 365); + KeyPair keyPair = generateKeyPair() + return CertificateUtils.generateIssuedCertificate(dn, keyPair.getPublic(), issuer, issuerKey, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) } private static X509Certificate[] generateCertificateChain(String dn = SUBJECT_DN, String issuerDn = ISSUER_DN) { - final KeyPair issuerKeyPair = generateKeyPair(); + final KeyPair issuerKeyPair = generateKeyPair() - final X509Certificate issuerCertificate = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair, issuerDn, SIGNATURE_ALGORITHM, 365); - final X509Certificate certificate = generateIssuedCertificate(dn, issuerCertificate, issuerKeyPair); + final X509Certificate issuerCertificate = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair, issuerDn, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) + final X509Certificate certificate = generateIssuedCertificate(dn, issuerCertificate, issuerKeyPair) [certificate, issuerCertificate] as X509Certificate[] } @@ -150,7 +156,7 @@ class CertificateUtilsTest extends GroovyTestCase { } private static Date inFuture(int days) { - return new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(days)); + return new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(days)) } @Test @@ -429,76 +435,72 @@ class CertificateUtilsTest extends GroovyTestCase { assert !dn1MatchesEmpty } - - @Test public void testShouldGenerateSelfSignedCert() throws Exception { - String dn = "CN=testDN,O=testOrg"; + String dn = "CN=testDN,O=testOrg" - int days = 365; - X509Certificate x509Certificate = CertificateUtils.generateSelfSignedX509Certificate(generateKeyPair(), dn, SIGNATURE_ALGORITHM, days); + int days = 365 + X509Certificate x509Certificate = CertificateUtils.generateSelfSignedX509Certificate(generateKeyPair(), dn, SIGNATURE_ALGORITHM, days) - Date notAfter = x509Certificate.getNotAfter(); - assertTrue(notAfter.after(inFuture(days - 1))); - assertTrue(notAfter.before(inFuture(days + 1))); + Date notAfter = x509Certificate.getNotAfter() + assertTrue(notAfter.after(inFuture(days - 1))) + assertTrue(notAfter.before(inFuture(days + 1))) - Date notBefore = x509Certificate.getNotBefore(); - assertTrue(notBefore.after(inFuture(-1))); - assertTrue(notBefore.before(inFuture(1))); + Date notBefore = x509Certificate.getNotBefore() + assertTrue(notBefore.after(inFuture(-1))) + assertTrue(notBefore.before(inFuture(1))) - assertEquals(dn, x509Certificate.getIssuerX500Principal().getName()); - assertEquals(SIGNATURE_ALGORITHM.toUpperCase(), x509Certificate.getSigAlgName().toUpperCase()); - assertEquals("RSA", x509Certificate.getPublicKey().getAlgorithm()); + assertEquals(dn, x509Certificate.getIssuerX500Principal().getName()) + assertEquals(SIGNATURE_ALGORITHM.toUpperCase(), x509Certificate.getSigAlgName().toUpperCase()) + assertEquals("RSA", x509Certificate.getPublicKey().getAlgorithm()) - x509Certificate.checkValidity(); + x509Certificate.checkValidity() } - - @Test public void testIssueCert() throws Exception { int days = 365; - KeyPair issuerKeyPair = generateKeyPair(); - X509Certificate issuer = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair, "CN=testCa,O=testOrg", SIGNATURE_ALGORITHM, days); + KeyPair issuerKeyPair = generateKeyPair() + X509Certificate issuer = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair, "CN=testCa,O=testOrg", SIGNATURE_ALGORITHM, days) - String dn = "CN=testIssued, O=testOrg"; + String dn = "CN=testIssued, O=testOrg" - KeyPair keyPair = generateKeyPair(); - X509Certificate x509Certificate = CertificateUtils.generateIssuedCertificate(dn, keyPair.getPublic(), issuer, issuerKeyPair, SIGNATURE_ALGORITHM, days); - assertEquals(dn, x509Certificate.getSubjectX500Principal().toString()); - assertEquals(issuer.getSubjectX500Principal().toString(), x509Certificate.getIssuerX500Principal().toString()); - assertEquals(keyPair.getPublic(), x509Certificate.getPublicKey()); + KeyPair keyPair = generateKeyPair() + X509Certificate x509Certificate = CertificateUtils.generateIssuedCertificate(dn, keyPair.getPublic(), issuer, issuerKeyPair, SIGNATURE_ALGORITHM, days) + assertEquals(dn, x509Certificate.getSubjectX500Principal().toString()) + assertEquals(issuer.getSubjectX500Principal().toString(), x509Certificate.getIssuerX500Principal().toString()) + assertEquals(keyPair.getPublic(), x509Certificate.getPublicKey()) - Date notAfter = x509Certificate.getNotAfter(); - assertTrue(notAfter.after(inFuture(days - 1))); - assertTrue(notAfter.before(inFuture(days + 1))); + Date notAfter = x509Certificate.getNotAfter() + assertTrue(notAfter.after(inFuture(days - 1))) + assertTrue(notAfter.before(inFuture(days + 1))) - Date notBefore = x509Certificate.getNotBefore(); - assertTrue(notBefore.after(inFuture(-1))); - assertTrue(notBefore.before(inFuture(1))); + Date notBefore = x509Certificate.getNotBefore() + assertTrue(notBefore.after(inFuture(-1))) + assertTrue(notBefore.before(inFuture(1))) - assertEquals(SIGNATURE_ALGORITHM.toUpperCase(), x509Certificate.getSigAlgName().toUpperCase()); - assertEquals("RSA", x509Certificate.getPublicKey().getAlgorithm()); + assertEquals(SIGNATURE_ALGORITHM.toUpperCase(), x509Certificate.getSigAlgName().toUpperCase()) + assertEquals("RSA", x509Certificate.getPublicKey().getAlgorithm()) - x509Certificate.verify(issuerKeyPair.getPublic()); + x509Certificate.verify(issuerKeyPair.getPublic()) } @Test public void reorderShouldPutElementsInCorrectOrder() { - String cn = "CN=testcn"; - String l = "L=testl"; - String st = "ST=testst"; - String o = "O=testo"; - String ou = "OU=testou"; - String c = "C=testc"; - String street = "STREET=teststreet"; - String dc = "DC=testdc"; - String uid = "UID=testuid"; - String surname = "SURNAME=testsurname"; - String initials = "INITIALS=testinitials"; - String givenName = "GIVENNAME=testgivenname"; + String cn = "CN=testcn" + String l = "L=testl" + String st = "ST=testst" + String o = "O=testo" + String ou = "OU=testou" + String c = "C=testc" + String street = "STREET=teststreet" + String dc = "DC=testdc" + String uid = "UID=testuid" + String surname = "SURNAME=testsurname" + String initials = "INITIALS=testinitials" + String givenName = "GIVENNAME=testgivenname" assertEquals("$cn,$l,$st,$o,$ou,$c,$street,$dc,$uid,$surname,$givenName,$initials".toString(), - CertificateUtils.reorderDn("$surname,$st,$o,$initials,$givenName,$uid,$street,$c,$cn,$ou,$l,$dc")); + CertificateUtils.reorderDn("$surname,$st,$o,$initials,$givenName,$uid,$street,$c,$cn,$ou,$l,$dc")) } @Test @@ -548,4 +550,40 @@ class CertificateUtilsTest extends GroovyTestCase { executorService.shutdown() } } + + @Test + void testShouldGenerateIssuedCertificateWithSans() { + // Arrange + final String SUBJECT_DN = "CN=localhost" + final List SANS = ["127.0.0.1", "nifi.nifi.apache.org"] + logger.info("Creating a certificate with subject: ${SUBJECT_DN} and SAN: ${SANS}") + + final KeyPair subjectKeyPair = generateKeyPair() + final KeyPair issuerKeyPair = generateKeyPair() + + final X509Certificate issuerCertificate = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair, ISSUER_DN, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) + + // Form the SANS into GeneralName instances and populate the container with the array + def gns = SANS.collect { String san -> + new GeneralName(GeneralName.dNSName, san) + } + def generalNames = new GeneralNames(gns as GeneralName[]) + logger.info("Created GeneralNames object: ${generalNames.names*.toString()}") + + // Form the Extensions object + ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator() + extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, generalNames) + Extensions extensions = extensionsGenerator.generate() + logger.info("Generated extensions object: ${extensions.oids()*.toString()}") + + // Act + X509Certificate certificate = CertificateUtils.generateIssuedCertificate(SUBJECT_DN, subjectKeyPair.public, extensions, issuerCertificate, issuerKeyPair, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) + logger.info("Issued certificate with subject: ${certificate.getSubjectDN().name} and SAN: ${certificate.getSubjectAlternativeNames().join(",")}") + + // Assert + assert certificate instanceof X509Certificate + assert certificate.getSubjectDN().name == SUBJECT_DN + assert certificate.getSubjectAlternativeNames().size() == SANS.size() + assert certificate.getSubjectAlternativeNames()*.last().containsAll(SANS) + } } diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java index 780dfa688c..86084c0f70 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java @@ -41,6 +41,7 @@ public class TlsConfig { private String signingAlgorithm = DEFAULT_SIGNING_ALGORITHM; private String dn; + private String domainAlternativeNames; private String keyStore; private String keyStoreType = DEFAULT_KEY_STORE_TYPE; private String keyStorePassword; @@ -206,4 +207,12 @@ public class TlsConfig { dn = calcDefaultDn(caHostname); } } + + public String getDomainAlternativeNames() { + return domainAlternativeNames; + } + + public void setDomainAlternativeNames(String domainAlternativeNames) { + this.domainAlternativeNames = domainAlternativeNames; + } } diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLine.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLine.java index 413255e481..db73b41ea9 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLine.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLine.java @@ -18,6 +18,12 @@ package org.apache.nifi.toolkit.tls.service.client; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; import org.apache.commons.cli.CommandLine; import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; import org.apache.nifi.toolkit.tls.commandLine.ExitCode; @@ -28,25 +34,20 @@ import org.apache.nifi.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.net.UnknownHostException; - /** * Command line parser for a TlsClientConfig object and a main entry point to invoke the parser and run the CA client */ public class TlsCertificateAuthorityClientCommandLine extends BaseCertificateAuthorityCommandLine { public static final String DESCRIPTION = "Generates a private key and gets it signed by the certificate authority."; public static final String CERTIFICATE_DIRECTORY = "certificateDirectory"; + public static final String SUBJECT_ALTERNATIVE_NAMES = "subjectAlternativeNames"; public static final String DEFAULT_CERTIFICATE_DIRECTORY = "."; private final Logger logger = LoggerFactory.getLogger(TlsCertificateAuthorityClientCommandLine.class); private final InputStreamFactory inputStreamFactory; private String certificateDirectory; + private String domainAlternativeNames; public TlsCertificateAuthorityClientCommandLine() { this(FileInputStream::new); @@ -56,6 +57,7 @@ public class TlsCertificateAuthorityClientCommandLine extends BaseCertificateAut super(DESCRIPTION); this.inputStreamFactory = inputStreamFactory; addOptionWithArg("C", CERTIFICATE_DIRECTORY, "The file to write the CA certificate to", DEFAULT_CERTIFICATE_DIRECTORY); + addOptionWithArg("S", SUBJECT_ALTERNATIVE_NAMES, "Comma-separated list of domains to use as Subject Alternative Names in the certificate"); } public static void main(String[] args) throws Exception { @@ -110,6 +112,7 @@ public class TlsCertificateAuthorityClientCommandLine extends BaseCertificateAut protected CommandLine doParse(String[] args) throws CommandLineParseException { CommandLine commandLine = super.doParse(args); certificateDirectory = commandLine.getOptionValue(CERTIFICATE_DIRECTORY, DEFAULT_CERTIFICATE_DIRECTORY); + domainAlternativeNames = commandLine.getOptionValue(SUBJECT_ALTERNATIVE_NAMES); return commandLine; } @@ -117,6 +120,10 @@ public class TlsCertificateAuthorityClientCommandLine extends BaseCertificateAut return certificateDirectory; } + public String getDomainAlternativeNames() { + return domainAlternativeNames; + } + public TlsClientConfig createClientConfig() throws IOException { String configJsonIn = getConfigJsonIn(); if (!StringUtils.isEmpty(configJsonIn)) { @@ -129,6 +136,7 @@ public class TlsCertificateAuthorityClientCommandLine extends BaseCertificateAut TlsClientConfig tlsClientConfig = new TlsClientConfig(); tlsClientConfig.setCaHostname(getCertificateAuthorityHostname()); tlsClientConfig.setDn(getDn()); + tlsClientConfig.setDomainAlternativeNames(getDomainAlternativeNames()); tlsClientConfig.setToken(getToken()); tlsClientConfig.setPort(getPort()); tlsClientConfig.setKeyStore(KEYSTORE + getKeyStoreType().toLowerCase()); @@ -140,4 +148,5 @@ public class TlsCertificateAuthorityClientCommandLine extends BaseCertificateAut return tlsClientConfig; } } + } diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformer.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformer.java index 889f217cee..14dc46fbd4 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformer.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformer.java @@ -18,6 +18,16 @@ package org.apache.nifi.toolkit.tls.service.client; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BoundedInputStream; import org.apache.http.HttpHost; @@ -38,17 +48,6 @@ import org.eclipse.jetty.server.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.StringReader; -import java.nio.charset.StandardCharsets; -import java.security.KeyPair; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Supplier; - public class TlsCertificateSigningRequestPerformer { public static final String RECEIVED_RESPONSE_CODE = "Received response code "; public static final String EXPECTED_ONE_CERTIFICATE = "Expected one certificate"; @@ -59,23 +58,28 @@ public class TlsCertificateSigningRequestPerformer { private final Supplier httpClientBuilderSupplier; private final String caHostname; private final String dn; + private final String domainAlternativeNames; private final String token; private final int port; private final ObjectMapper objectMapper; private final String signingAlgorithm; public TlsCertificateSigningRequestPerformer(TlsClientConfig tlsClientConfig) throws NoSuchAlgorithmException { - this(HttpClientBuilder::create, tlsClientConfig.getCaHostname(), tlsClientConfig.getDn(), tlsClientConfig.getToken(), tlsClientConfig.getPort(), tlsClientConfig.getSigningAlgorithm()); + this(HttpClientBuilder::create, tlsClientConfig.getCaHostname(), tlsClientConfig.getDn(), tlsClientConfig.getDomainAlternativeNames(), + tlsClientConfig.getToken(), tlsClientConfig.getPort(), tlsClientConfig.getSigningAlgorithm()); } protected TlsCertificateSigningRequestPerformer(Supplier httpClientBuilderSupplier, TlsClientConfig tlsClientConfig) throws NoSuchAlgorithmException { - this(httpClientBuilderSupplier, tlsClientConfig.getCaHostname(), tlsClientConfig.getDn(), tlsClientConfig.getToken(), tlsClientConfig.getPort(), tlsClientConfig.getSigningAlgorithm()); + this(httpClientBuilderSupplier, tlsClientConfig.getCaHostname(), tlsClientConfig.getDn(), tlsClientConfig.getDomainAlternativeNames(), + tlsClientConfig.getToken(), tlsClientConfig.getPort(), tlsClientConfig.getSigningAlgorithm()); } - private TlsCertificateSigningRequestPerformer(Supplier httpClientBuilderSupplier, String caHostname, String dn, String token, int port, String signingAlgorithm) { + private TlsCertificateSigningRequestPerformer(Supplier httpClientBuilderSupplier, String caHostname, + String dn, String domainAlternativeNames, String token, int port, String signingAlgorithm) { this.httpClientBuilderSupplier = httpClientBuilderSupplier; this.caHostname = caHostname; this.dn = CertificateUtils.reorderDn(dn); + this.domainAlternativeNames = domainAlternativeNames; this.token = token; this.port = port; this.objectMapper = new ObjectMapper(); @@ -87,7 +91,7 @@ public class TlsCertificateSigningRequestPerformer { * * @param keyPair the keypair to generate the csr for * @throws IOException if there is a problem during the process - * @returnd the resulting certificate chain + * @return the resulting certificate chain */ public X509Certificate[] perform(KeyPair keyPair) throws IOException { try { @@ -104,7 +108,7 @@ public class TlsCertificateSigningRequestPerformer { String jsonResponseString; int responseCode; try (CloseableHttpClient client = httpClientBuilder.build()) { - JcaPKCS10CertificationRequest request = TlsHelper.generateCertificationRequest(dn, keyPair, signingAlgorithm); + JcaPKCS10CertificationRequest request = TlsHelper.generateCertificationRequest(dn, domainAlternativeNames, keyPair, signingAlgorithm); TlsCertificateAuthorityRequest tlsCertificateAuthorityRequest = new TlsCertificateAuthorityRequest(TlsHelper.calculateHMac(token, request.getPublicKey()), TlsHelper.pemEncodeJcaObject(request)); diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandler.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandler.java index 27d995ec03..b0172ae7d2 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandler.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandler.java @@ -86,8 +86,8 @@ public class TlsCertificateAuthorityServiceHandler extends AbstractHandler { if (logger.isInfoEnabled()) { logger.info("Received CSR with DN " + dn); } - X509Certificate x509Certificate = CertificateUtils.generateIssuedCertificate(dn, - jcaPKCS10CertificationRequest.getPublicKey(), caCert, keyPair, signingAlgorithm, days); + X509Certificate x509Certificate = CertificateUtils.generateIssuedCertificate(dn, jcaPKCS10CertificationRequest.getPublicKey(), + CertificateUtils.getExtensionsFromCSR(jcaPKCS10CertificationRequest), caCert, keyPair, signingAlgorithm, days); writeResponse(objectMapper, request, response, new TlsCertificateAuthorityResponse(TlsHelper.calculateHMac(token, caCert.getPublicKey()), TlsHelper.pemEncodeJcaObject(x509Certificate)), Response.SC_OK); return; diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java index 8abe5c6a6d..aa619dacb9 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java @@ -180,7 +180,7 @@ public class TlsToolkitStandalone { TlsClientManager tlsClientManager = new TlsClientManager(tlsClientConfig); KeyPair keyPair = TlsHelper.generateKeyPair(keyPairAlgorithm, keySize); tlsClientManager.addPrivateKeyToKeyStore(keyPair, NIFI_KEY, CertificateUtils.generateIssuedCertificate(tlsClientConfig.calcDefaultDn(hostname), - keyPair.getPublic(), certificate, caKeyPair, signingAlgorithm, days), certificate); + keyPair.getPublic(), null, certificate, caKeyPair, signingAlgorithm, days), certificate); tlsClientManager.setCertificateEntry(NIFI_CERT, certificate); tlsClientManager.addClientConfigurationWriter(new NifiPropertiesTlsClientConfigWriter(niFiPropertiesWriterFactory, new File(hostDir, "nifi.properties"), hostname, instanceDefinition.getNumber())); @@ -213,7 +213,7 @@ public class TlsToolkitStandalone { logger.info("Generating new client certificate " + clientCertFile); } KeyPair keyPair = TlsHelper.generateKeyPair(keyPairAlgorithm, keySize); - X509Certificate clientCert = CertificateUtils.generateIssuedCertificate(reorderedDn, keyPair.getPublic(), certificate, caKeyPair, signingAlgorithm, days); + X509Certificate clientCert = CertificateUtils.generateIssuedCertificate(reorderedDn, keyPair.getPublic(), null, certificate, caKeyPair, signingAlgorithm, days); KeyStore keyStore = KeyStoreUtils.getKeyStore(KeystoreType.PKCS12.toString()); keyStore.load(null, null); keyStore.setKeyEntry(NIFI_KEY, keyPair.getPrivate(), null, new Certificate[]{clientCert, certificate}); diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java index f59e2c128b..746571420c 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java @@ -17,7 +17,33 @@ package org.apache.nifi.toolkit.tls.util; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.ExtensionsGenerator; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; @@ -35,25 +61,6 @@ import org.bouncycastle.util.io.pem.PemWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.crypto.Cipher; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - public class TlsHelper { private static final Logger logger = LoggerFactory.getLogger(TlsHelper.class); private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128; @@ -184,8 +191,27 @@ public class TlsHelper { return createKeyPairGenerator(algorithm, keySize).generateKeyPair(); } - public static JcaPKCS10CertificationRequest generateCertificationRequest(String requestedDn, KeyPair keyPair, String signingAlgorithm) throws OperatorCreationException { + public static JcaPKCS10CertificationRequest generateCertificationRequest(String requestedDn, String domainAlternativeNames, + KeyPair keyPair, String signingAlgorithm) throws OperatorCreationException { JcaPKCS10CertificationRequestBuilder jcaPKCS10CertificationRequestBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Name(requestedDn), keyPair.getPublic()); + + // add Subject Alternative Name(s) + if(StringUtils.isNotBlank(domainAlternativeNames)) { + try { + List namesList = new ArrayList<>(); + for(String alternativeName : domainAlternativeNames.split(",")) { + namesList.add(new GeneralName(GeneralName.dNSName, alternativeName)); + } + + GeneralNames subjectAltNames = new GeneralNames(namesList.toArray(new GeneralName [] {})); + ExtensionsGenerator extGen = new ExtensionsGenerator(); + extGen.addExtension(Extension.subjectAlternativeName, false, subjectAltNames); + jcaPKCS10CertificationRequestBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate()); + } catch (IOException e) { + throw new OperatorCreationException("Error while adding " + domainAlternativeNames + " as Subject Alternative Name.", e); + } + } + JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(signingAlgorithm); return new JcaPKCS10CertificationRequest(jcaPKCS10CertificationRequestBuilder.build(jcaContentSignerBuilder.build(keyPair.getPrivate()))); } diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformerTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformerTest.java index 37fc8e1cd2..fb20739812 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformerTest.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformerTest.java @@ -106,7 +106,7 @@ public class TlsCertificateSigningRequestPerformerTest { when(tlsClientConfig.getPort()).thenReturn(testPort); when(tlsClientConfig.createCertificateSigningRequestPerformer()).thenReturn(tlsCertificateSigningRequestPerformer); when(tlsClientConfig.getSigningAlgorithm()).thenReturn(TlsConfig.DEFAULT_SIGNING_ALGORITHM); - JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = TlsHelper.generateCertificationRequest(tlsClientConfig.getDn(), keyPair, TlsConfig.DEFAULT_SIGNING_ALGORITHM); + JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = TlsHelper.generateCertificationRequest(tlsClientConfig.getDn(), null, keyPair, TlsConfig.DEFAULT_SIGNING_ALGORITHM); String testCsrPem = TlsHelper.pemEncodeJcaObject(jcaPKCS10CertificationRequest); when(httpClientBuilderSupplier.get()).thenReturn(httpClientBuilder); when(httpClientBuilder.build()).thenAnswer(invocation -> { diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandlerTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandlerTest.java index 4d4be70ecd..00c5ec837c 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandlerTest.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandlerTest.java @@ -122,7 +122,7 @@ public class TlsCertificateAuthorityServiceHandlerTest { caCert = CertificateUtils.generateSelfSignedX509Certificate(keyPair, "CN=fakeCa", TlsConfig.DEFAULT_SIGNING_ALGORITHM, TlsConfig.DEFAULT_DAYS); requestedDn = new TlsConfig().calcDefaultDn(TlsConfig.DEFAULT_HOSTNAME); certificateKeyPair = TlsHelper.generateKeyPair(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM, TlsConfig.DEFAULT_KEY_SIZE); - jcaPKCS10CertificationRequest = TlsHelper.generateCertificationRequest(requestedDn, certificateKeyPair, TlsConfig.DEFAULT_SIGNING_ALGORITHM); + jcaPKCS10CertificationRequest = TlsHelper.generateCertificationRequest(requestedDn, null, certificateKeyPair, TlsConfig.DEFAULT_SIGNING_ALGORITHM); testPemEncodedCsr = TlsHelper.pemEncodeJcaObject(jcaPKCS10CertificationRequest); tlsCertificateAuthorityServiceHandler = new TlsCertificateAuthorityServiceHandler(TlsConfig.DEFAULT_SIGNING_ALGORITHM, TlsConfig.DEFAULT_DAYS, testToken, caCert, keyPair, objectMapper); testHmac = TlsHelper.calculateHMac(testToken, jcaPKCS10CertificationRequest.getPublicKey()); diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/TlsHelperTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/TlsHelperTest.java index 0a3c38f6bc..223dbb7a34 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/TlsHelperTest.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/TlsHelperTest.java @@ -17,21 +17,14 @@ package org.apache.nifi.toolkit.tls.util; -import org.apache.nifi.security.util.CertificateUtils; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.PEMKeyPair; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; -import org.bouncycastle.operator.OperatorCreationException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.AdditionalMatchers; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; import java.io.File; @@ -53,20 +46,43 @@ import java.security.Provider; import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.security.util.CertificateUtils; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalMatchers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @RunWith(MockitoJUnitRunner.class) public class TlsHelperTest { + public static final Logger logger = LoggerFactory.getLogger(TlsHelperTest.class); + private static final boolean originalUnlimitedCrypto = TlsHelper.isUnlimitedStrengthCryptographyEnabled(); private int days; @@ -280,4 +296,58 @@ public class TlsHelperTest { assertEquals(ioException2, e); } } + + @Test + public void testShouldIncludeSANFromCSR() throws Exception { + // Arrange + final List SAN_ENTRIES = Arrays.asList("127.0.0.1", "nifi.nifi.apache.org"); + final String SAN = StringUtils.join(SAN_ENTRIES, ","); + final int SAN_COUNT = SAN_ENTRIES.size(); + final String DN = "CN=localhost"; + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + logger.info("Generating CSR with DN: " + DN); + + // Act + JcaPKCS10CertificationRequest csrWithSan = TlsHelper.generateCertificationRequest(DN, SAN, keyPair, TlsConfig.DEFAULT_SIGNING_ALGORITHM); + logger.info("Created CSR with SAN: " + SAN); + String testCsrPem = TlsHelper.pemEncodeJcaObject(csrWithSan); + logger.info("Encoded CSR as PEM: " + testCsrPem); + + // Assert + String subjectName = csrWithSan.getSubject().toString(); + logger.info("CSR Subject Name: " + subjectName); + assert subjectName.equals(DN); + + List extractedSans = extractSanFromCsr(csrWithSan); + assert extractedSans.size() == SAN_COUNT; + List formattedSans = SAN_ENTRIES.stream().map(s -> "DNS: " + s).collect(Collectors.toList()); + assert extractedSans.containsAll(formattedSans); + } + + private List extractSanFromCsr(JcaPKCS10CertificationRequest csr) { + List sans = new ArrayList<>(); + Attribute[] certAttributes = csr.getAttributes(); + for (Attribute attribute : certAttributes) { + if (attribute.getAttrType().equals(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) { + Extensions extensions = Extensions.getInstance(attribute.getAttrValues().getObjectAt(0)); + GeneralNames gns = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName); + GeneralName[] names = gns.getNames(); + for (GeneralName name : names) { + logger.info("Type: " + name.getTagNo() + " | Name: " + name.getName()); + String title = ""; + if (name.getTagNo() == GeneralName.dNSName) { + title = "DNS"; + } else if (name.getTagNo() == GeneralName.iPAddress) { + title = "IP Address"; + // name.toASN1Primitive(); + } else if (name.getTagNo() == GeneralName.otherName) { + title = "Other Name"; + } + sans.add(title + ": " + name.getName()); + } + } + } + + return sans; + } }