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 <alopresto@apache.org>
This commit is contained in:
Andy LoPresto 2017-02-09 14:47:39 +01:00
parent 71e2061b5d
commit 6fc30900b9
No known key found for this signature in database
GPG Key ID: 3C6EF65B2F7DEF69
11 changed files with 345 additions and 139 deletions

View File

@ -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() {
}
}

View File

@ -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<String> 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)
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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<HttpClientBuilder> 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<HttpClientBuilder> 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<HttpClientBuilder> httpClientBuilderSupplier, String caHostname, String dn, String token, int port, String signingAlgorithm) {
private TlsCertificateSigningRequestPerformer(Supplier<HttpClientBuilder> 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));

View File

@ -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;

View File

@ -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});

View File

@ -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<GeneralName> 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())));
}

View File

@ -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 -> {

View File

@ -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());

View File

@ -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<String> 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<String> extractedSans = extractSanFromCsr(csrWithSan);
assert extractedSans.size() == SAN_COUNT;
List<String> formattedSans = SAN_ENTRIES.stream().map(s -> "DNS: " + s).collect(Collectors.toList());
assert extractedSans.containsAll(formattedSans);
}
private List<String> extractSanFromCsr(JcaPKCS10CertificationRequest csr) {
List<String> 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;
}
}