This is an automated email from the ASF dual-hosted git repository. alopresto pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push: new 1e6619b NIFI-7767 - Fixed issue with tls-toolkit not adding SANs to generated certificates. Added tests. NIFI-7767 - Fixed up TlsCertificateAuthorityTest to include SAN in tests. 1e6619b is described below commit 1e6619b91f2d9a7af2d7195464d4a0f5b595b93d Author: Nathan Gough <thena...@gmail.com> AuthorDate: Fri Aug 28 16:33:51 2020 -0400 NIFI-7767 - Fixed issue with tls-toolkit not adding SANs to generated certificates. Added tests. NIFI-7767 - Fixed up TlsCertificateAuthorityTest to include SAN in tests. --- .../nifi/security/util/CertificateUtils.java | 3 +- .../nifi/security/util/CertificateUtilsTest.groovy | 51 +++++++++++++ .../tls/service/TlsCertificateAuthorityTest.java | 27 +++++++ .../TlsCertificateAuthorityServiceHandlerTest.java | 88 ++++++++++++++++------ 4 files changed, 145 insertions(+), 24 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 ee0c33e..a93c518 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 @@ -50,6 +50,7 @@ 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.DLSequence; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.pkcs.Attribute; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; @@ -598,7 +599,7 @@ public final class CertificateUtils { ASN1Encodable extension = attValue.getObjectAt(0); if (extension instanceof Extensions) { return (Extensions) extension; - } else if (extension instanceof DERSequence) { + } else if (extension instanceof DERSequence || extension instanceof DLSequence) { return Extensions.getInstance(extension); } } 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 d20cfeb..a1044ca 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,12 +16,20 @@ */ package org.apache.nifi.security.util +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.asn1.x500.style.IETFUtils 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.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder +import org.bouncycastle.util.IPAddress import org.junit.After import org.junit.Before import org.junit.BeforeClass @@ -68,6 +76,7 @@ class CertificateUtilsTest extends GroovyTestCase { 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 List<String> SUBJECT_ALT_NAMES = ["127.0.0.1", "nifi.nifi.apache.org"] @BeforeClass static void setUpOnce() { @@ -655,4 +664,46 @@ class CertificateUtilsTest extends GroovyTestCase { assert tlsVersion == "TLSv1.3" } } + + @Test + void testGetExtensionsFromCSR() { + // Arrange + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA") + KeyPair keyPair = generator.generateKeyPair() + Extensions sanExtensions = createDomainAlternativeNamesExtensions(SUBJECT_ALT_NAMES, SUBJECT_DN) + + JcaPKCS10CertificationRequestBuilder jcaPKCS10CertificationRequestBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Name(SUBJECT_DN), keyPair.getPublic()) + jcaPKCS10CertificationRequestBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, sanExtensions) + JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder("SHA256WITHRSA") + JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = new JcaPKCS10CertificationRequest(jcaPKCS10CertificationRequestBuilder.build(jcaContentSignerBuilder.build(keyPair.getPrivate()))) + + // Act + Extensions extensions = CertificateUtils.getExtensionsFromCSR(jcaPKCS10CertificationRequest) + + // Assert + assert(extensions.equivalent(sanExtensions)) + } + + // Using this directly from tls-toolkit results in a dependency loop, so it's added here for testing purposes. + private static Extensions createDomainAlternativeNamesExtensions(List<String> domainAlternativeNames, String requestedDn) throws IOException { + List<GeneralName> namesList = new ArrayList<>() + + try { + final String cn = IETFUtils.valueToString(new X500Name(requestedDn).getRDNs(BCStyle.CN)[0].getFirst().getValue()) + namesList.add(new GeneralName(GeneralName.dNSName, cn)) + } catch (Exception e) { + throw new IOException("Failed to extract CN from request DN: " + requestedDn, e) + } + + if (domainAlternativeNames != null) { + for (String alternativeName : domainAlternativeNames) { + namesList.add(new GeneralName(IPAddress.isValid(alternativeName) ? GeneralName.iPAddress : GeneralName.dNSName, alternativeName)) + } + } + + GeneralNames subjectAltNames = new GeneralNames(namesList.toArray([] as GeneralName[])) + ExtensionsGenerator extGen = new ExtensionsGenerator() + extGen.addExtension(Extension.subjectAlternativeName, false, subjectAltNames) + return extGen.generate() + } } diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/TlsCertificateAuthorityTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/TlsCertificateAuthorityTest.java index 7137dfe..afefee3 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/TlsCertificateAuthorityTest.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/TlsCertificateAuthorityTest.java @@ -51,6 +51,11 @@ import java.security.SignatureException; import java.security.UnrecoverableEntryException; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -73,6 +78,7 @@ public class TlsCertificateAuthorityTest { private ByteArrayOutputStream clientTrustStoreOutputStream; private ByteArrayOutputStream serverConfigFileOutputStream; private ByteArrayOutputStream clientConfigFileOutputStream; + private String subjectAlternativeName; @Before public void setup() throws FileNotFoundException { @@ -87,6 +93,7 @@ public class TlsCertificateAuthorityTest { clientTrustStoreOutputStream = new ByteArrayOutputStream(); serverConfigFileOutputStream = new ByteArrayOutputStream(); clientConfigFileOutputStream = new ByteArrayOutputStream(); + subjectAlternativeName = "nifi.apache.org"; String myTestTokenUseSomethingStronger = "myTestTokenUseSomethingStronger"; int port = availablePort(); @@ -106,6 +113,7 @@ public class TlsCertificateAuthorityTest { clientConfig.setKeyStore(clientKeyStore); clientConfig.setTrustStore(clientTrustStore); clientConfig.setToken(myTestTokenUseSomethingStronger); + clientConfig.setDomainAlternativeNames(Arrays.asList(subjectAlternativeName)); clientConfig.setPort(port); clientConfig.setKeySize(2048); clientConfig.initDefaults(); @@ -240,6 +248,9 @@ public class TlsCertificateAuthorityTest { certificateChain[0].verify(caCertificate.getPublicKey()); assertPrivateAndPublicKeyMatch(clientPrivateKeyEntry.getPrivateKey(), certificateChain[0].getPublicKey()); + // Does the certificate contain the SAN we defined in the client config? + assert(isSANPresent(certificateChain[0])); + KeyStore clientTrustStore = KeyStoreUtils.getTrustStore(KeystoreType.JKS.toString()); clientTrustStore.load(new ByteArrayInputStream(clientTrustStoreOutputStream.toByteArray()), clientConfig.getTrustStorePassword().toCharArray()); assertEquals(caCertificate, clientTrustStore.getCertificate(TlsToolkitStandalone.NIFI_CERT)); @@ -257,6 +268,22 @@ public class TlsCertificateAuthorityTest { verify.verify(signature.sign()); } + private boolean isSANPresent(Certificate cert) { + Iterator<List<?>> iterator = null; + try { + iterator = ((X509Certificate) cert).getSubjectAlternativeNames().iterator(); + } catch (CertificateParsingException e) { + e.printStackTrace(); + } + boolean containsSAN = false; + while(iterator.hasNext()) { + if(iterator.next().contains(subjectAlternativeName)) { + containsSAN = true; + } + } + return containsSAN; + } + /** * Will determine the available port used by ca server */ 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 eb0dbc3..54e49ca 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 @@ -17,15 +17,48 @@ package org.apache.nifi.toolkit.tls.service.server; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.apache.nifi.security.util.CertificateUtils; import org.apache.nifi.toolkit.tls.configuration.TlsConfig; import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityRequest; import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityResponse; import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.cert.crmf.CRMFException; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.junit.After; @@ -35,29 +68,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringReader; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.X509Certificate; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @RunWith(MockitoJUnitRunner.class) public class TlsCertificateAuthorityServiceHandlerTest { X509Certificate caCert; @@ -95,6 +105,9 @@ public class TlsCertificateAuthorityServiceHandlerTest { private String requestedDn; private KeyPair certificateKeyPair; + private static final String SUBJECT_DN = "CN=NiFi Test Server,OU=Security,O=Apache,ST=CA,C=US"; + private static final List<String> SUBJECT_ALT_NAMES = Arrays.asList("127.0.0.1", "nifi.nifi.apache.org"); + @Before public void setup() throws Exception { testToken = "testTokenTestToken"; @@ -166,6 +179,35 @@ public class TlsCertificateAuthorityServiceHandlerTest { tlsCertificateAuthorityServiceHandler.handle(null, baseRequest, httpServletRequest, httpServletResponse); } + @Test + public void testSANAgainUsingCertificationRequestMethod() throws GeneralSecurityException, IOException, OperatorCreationException { + // Arrange + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + KeyPair keyPair = generator.generateKeyPair(); + Extensions exts = TlsHelper.createDomainAlternativeNamesExtensions(SUBJECT_ALT_NAMES, SUBJECT_DN); + String token = "someTokenData16B"; + + JcaPKCS10CertificationRequestBuilder jcaPKCS10CertificationRequestBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Name(SUBJECT_DN), keyPair.getPublic()); + jcaPKCS10CertificationRequestBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, exts); + JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder("SHA256WITHRSA"); + JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = new JcaPKCS10CertificationRequest( + jcaPKCS10CertificationRequestBuilder.build(jcaContentSignerBuilder.build(keyPair.getPrivate()))); + TlsCertificateAuthorityRequest tlsCertificateAuthorityRequest = new TlsCertificateAuthorityRequest( + TlsHelper.calculateHMac(token, jcaPKCS10CertificationRequest.getPublicKey()), TlsHelper.pemEncodeJcaObject(jcaPKCS10CertificationRequest)); + + JcaPKCS10CertificationRequest jcaPKCS10CertificationDecoded = TlsHelper.parseCsr(tlsCertificateAuthorityRequest.getCsr()); + + // Act + Extensions extensions = CertificateUtils.getExtensionsFromCSR(jcaPKCS10CertificationDecoded); + // Satisfy @After requirement + baseRequest.setHandled(true); + + // Assert + assertNotNull("The extensions parsed from the CSR were found to be null when they should have been present.", extensions); + assertNotNull("The Subject Alternate Name parsed from the CSR was found to be null when it should have been present.", extensions.getExtension(Extension.subjectAlternativeName)); + assertTrue(extensions.equivalent(exts)); + } + @After public void verifyHandled() { verify(baseRequest).setHandled(true);