This is an automated email from the ASF dual-hosted git repository.
pvillard31 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 9a143afbddc NIFI-16412 Replaced Bouncy Castle with custom Certificate
Builder (#11747)
9a143afbddc is described below
commit 9a143afbddc177020ee0c446318c47c2f027f452
Author: David Handermann <[email protected]>
AuthorDate: Fri Oct 2 03:37:40 2026 -0500
NIFI-16412 Replaced Bouncy Castle with custom Certificate Builder (#11747)
---
nifi-commons/nifi-security-cert-builder/pom.xml | 10 -
.../nifi/security/cert/builder/DerEncoder.java | 273 +++++++++++++++++++
.../cert/builder/StandardCertificateBuilder.java | 289 ++++++++++++---------
.../builder/StandardCertificateBuilderTest.java | 21 +-
4 files changed, 453 insertions(+), 140 deletions(-)
diff --git a/nifi-commons/nifi-security-cert-builder/pom.xml
b/nifi-commons/nifi-security-cert-builder/pom.xml
index 9cd79a46681..dc5cb0ad6d6 100644
--- a/nifi-commons/nifi-security-cert-builder/pom.xml
+++ b/nifi-commons/nifi-security-cert-builder/pom.xml
@@ -21,15 +21,5 @@
</parent>
<artifactId>nifi-security-cert-builder</artifactId>
<description>Shared X.509 certificate builder components</description>
- <dependencies>
- <dependency>
- <groupId>org.bouncycastle</groupId>
- <artifactId>bcpkix-jdk18on</artifactId>
- </dependency>
- <dependency>
- <groupId>org.bouncycastle</groupId>
- <artifactId>bcprov-jdk18on</artifactId>
- </dependency>
- </dependencies>
</project>
diff --git
a/nifi-commons/nifi-security-cert-builder/src/main/java/org/apache/nifi/security/cert/builder/DerEncoder.java
b/nifi-commons/nifi-security-cert-builder/src/main/java/org/apache/nifi/security/cert/builder/DerEncoder.java
new file mode 100644
index 00000000000..fe2349852f9
--- /dev/null
+++
b/nifi-commons/nifi-security-cert-builder/src/main/java/org/apache/nifi/security/cert/builder/DerEncoder.java
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.cert.builder;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Distinguished Encoding Rules encoder for the X.509 certificates
+ */
+final class DerEncoder {
+ private static final int BOOLEAN_TAG = 0x01;
+ private static final int INTEGER_TAG = 0x02;
+ private static final int BIT_STRING_TAG = 0x03;
+ private static final int OCTET_STRING_TAG = 0x04;
+ private static final int NULL_TAG = 0x05;
+ private static final int OBJECT_IDENTIFIER_TAG = 0x06;
+ private static final int SEQUENCE_TAG = 0x30;
+ private static final int UTC_TIME_TAG = 0x17;
+ private static final int GENERALIZED_TIME_TAG = 0x18;
+ private static final int CONTEXT_SPECIFIC_PRIMITIVE = 0x80;
+ private static final int CONTEXT_SPECIFIC_CONSTRUCTED = 0xA0;
+ private static final int LONG_FORM_LENGTH = 0x80;
+ private static final int SHORT_FORM_LENGTH_LIMIT = 128;
+ private static final int LENGTH_BYTE_LIMIT = 4;
+ private static final int BASE_128_SHIFT = 7;
+ private static final int BASE_128_MASK = 0x7F;
+ private static final int CONTINUATION_BIT = 0x80;
+ private static final String OID_SEPARATOR = "\\.";
+ private static final int OID_FIRST_COMPONENT_MULTIPLIER = 40;
+ private static final int BYTE_MASK = 0xFF;
+ private static final int BITS_PER_BYTE = 8;
+ private static final int AUTHORITY_KEY_IDENTIFIER_TAG = 0;
+ private static final String KEY_IDENTIFIER_DIGEST_ALGORITHM = "SHA-1";
+ private static final int GENERALIZED_TIME_FIRST_YEAR = 2050;
+ private static final DateTimeFormatter UTC_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyMMddHHmmss'Z'",
Locale.ROOT).withZone(ZoneOffset.UTC);
+ private static final DateTimeFormatter GENERALIZED_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss'Z'",
Locale.ROOT).withZone(ZoneOffset.UTC);
+ private static final byte[] BOOLEAN_TRUE = new byte[] {BOOLEAN_TAG, 0x01,
(byte) 0xFF};
+ private static final byte[] NULL_VALUE = new byte[] {NULL_TAG, 0x00};
+
+ private DerEncoder() {
+ }
+
+ static byte[] sequence(final byte[]... encodedValues) {
+ return tagged(SEQUENCE_TAG, concatenate(encodedValues));
+ }
+
+ static byte[] integer(final BigInteger value) {
+ return tagged(INTEGER_TAG, value.toByteArray());
+ }
+
+ static byte[] objectIdentifier(final String objectIdentifier) {
+ final String[] components = objectIdentifier.split(OID_SEPARATOR);
+ final ByteArrayOutputStream contents = new ByteArrayOutputStream();
+ final int firstComponent = Integer.parseInt(components[0]);
+ final int secondComponent = Integer.parseInt(components[1]);
+ final int encodedFirstComponents = firstComponent *
OID_FIRST_COMPONENT_MULTIPLIER + secondComponent;
+ contents.write(encodedFirstComponents);
+ for (int i = 2; i < components.length; i++) {
+ final long component = Long.parseLong(components[i]);
+ writeBase128(contents, component);
+ }
+
+ final byte[] encodedObjectIdentifier = contents.toByteArray();
+ return tagged(OBJECT_IDENTIFIER_TAG, encodedObjectIdentifier);
+ }
+
+ static byte[] octetString(final byte[] contents) {
+ return tagged(OCTET_STRING_TAG, contents);
+ }
+
+ static byte[] bitString(final int unusedBits, final byte[] contents) {
+ final byte[] body = new byte[contents.length + 1];
+ body[0] = (byte) unusedBits;
+ System.arraycopy(contents, 0, body, 1, contents.length);
+ return tagged(BIT_STRING_TAG, body);
+ }
+
+ static byte[] booleanTrue() {
+ return BOOLEAN_TRUE;
+ }
+
+ static byte[] nullValue() {
+ return NULL_VALUE;
+ }
+
+ static byte[] time(final Date date) {
+ final int year = date.toInstant().atZone(ZoneOffset.UTC).getYear();
+ if (year < GENERALIZED_TIME_FIRST_YEAR) {
+ return tagged(UTC_TIME_TAG,
UTC_TIME_FORMATTER.format(date.toInstant()).getBytes(StandardCharsets.US_ASCII));
+ }
+
+ return tagged(GENERALIZED_TIME_TAG,
GENERALIZED_TIME_FORMATTER.format(date.toInstant()).getBytes(StandardCharsets.US_ASCII));
+ }
+
+ static byte[] explicit(final int tagNumber, final byte[] encodedValue) {
+ return tagged(CONTEXT_SPECIFIC_CONSTRUCTED | tagNumber, encodedValue);
+ }
+
+ static byte[] implicit(final int tagNumber, final byte[] contents) {
+ return tagged(CONTEXT_SPECIFIC_PRIMITIVE | tagNumber, contents);
+ }
+
+ /**
+ * Subject key identifier is the SHA-1 hash of the subject public key bit
string, excluding the unused-bits count, which is RFC 5280 method 1
+ *
+ * @param subjectPublicKeyInfo Subject public key info encoding
+ * @return Subject key identifier extension value
+ */
+ static byte[] subjectKeyIdentifier(final byte[] subjectPublicKeyInfo) {
+ return octetString(keyIdentifier(subjectPublicKeyInfo));
+ }
+
+ /**
+ * Authority key identifier containing only the key identifier as an
implicit context-specific tag inside a sequence
+ *
+ * @param issuerPublicKeyInfo Issuer public key info encoding
+ * @return Authority key identifier extension value
+ */
+ static byte[] authorityKeyIdentifier(final byte[] issuerPublicKeyInfo) {
+ final byte[] keyIdentifierEncoded =
implicit(AUTHORITY_KEY_IDENTIFIER_TAG, keyIdentifier(issuerPublicKeyInfo));
+ return sequence(keyIdentifierEncoded);
+ }
+
+ private static byte[] keyIdentifier(final byte[] subjectPublicKeyInfo) {
+ try {
+ final MessageDigest messageDigest =
MessageDigest.getInstance(KEY_IDENTIFIER_DIGEST_ALGORITHM);
+ return messageDigest.digest(publicKeyBytes(subjectPublicKeyInfo));
+ } catch (final NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Key identifier digest is not
available", e);
+ }
+ }
+
+ private static byte[] publicKeyBytes(final byte[] subjectPublicKeyInfo) {
+ final ByteBuffer buffer = ByteBuffer.wrap(subjectPublicKeyInfo);
+ try {
+ expectTag(buffer, SEQUENCE_TAG);
+ readLength(buffer);
+
+ expectTag(buffer, SEQUENCE_TAG);
+ final int algorithmLength = readLength(buffer);
+ if (buffer.remaining() < algorithmLength) {
+ throw new IllegalArgumentException("Subject public key
encoding is not valid");
+ }
+
+ buffer.position(buffer.position() + algorithmLength);
+ expectTag(buffer, BIT_STRING_TAG);
+ final int bitStringLength = readLength(buffer);
+ if (bitStringLength < 1 || buffer.remaining() < bitStringLength) {
+ throw new IllegalArgumentException("Subject public key
encoding is not valid");
+ }
+
+ buffer.get();
+ final byte[] keyBytes = new byte[bitStringLength - 1];
+ buffer.get(keyBytes);
+ return keyBytes;
+ } catch (final BufferUnderflowException e) {
+ throw new IllegalArgumentException("Subject public key encoding is
not valid", e);
+ }
+ }
+
+ private static void expectTag(final ByteBuffer buffer, final int
expectedTag) {
+ final int tag = buffer.get() & BYTE_MASK;
+ if (tag != expectedTag) {
+ throw new IllegalArgumentException("Subject public key encoding is
not valid");
+ }
+ }
+
+ private static int readLength(final ByteBuffer buffer) {
+ final int firstLengthByte = buffer.get() & BYTE_MASK;
+ if (firstLengthByte < LONG_FORM_LENGTH) {
+ return firstLengthByte;
+ }
+
+ final int lengthByteCount = firstLengthByte & BASE_128_MASK;
+ if (lengthByteCount == 0 || lengthByteCount > LENGTH_BYTE_LIMIT) {
+ throw new IllegalArgumentException("Subject public key encoding
length is not supported");
+ }
+
+ int length = 0;
+ for (int i = 0; i < lengthByteCount; i++) {
+ length = (length << BITS_PER_BYTE) | (buffer.get() & BYTE_MASK);
+ }
+
+ return length;
+ }
+
+ private static void writeBase128(final ByteArrayOutputStream outputStream,
final long value) {
+ int shift = 0;
+ long remaining = value;
+ while (remaining > BASE_128_MASK) {
+ shift += BASE_128_SHIFT;
+ remaining >>>= BASE_128_SHIFT;
+ }
+
+ while (shift > 0) {
+ final int encoded = (int) (((value >>> shift) & BASE_128_MASK) |
CONTINUATION_BIT);
+ outputStream.write(encoded);
+ shift -= BASE_128_SHIFT;
+ }
+
+ outputStream.write((int) (value & BASE_128_MASK));
+ }
+
+ private static byte[] tagged(final int tag, final byte[] contents) {
+ final byte[] encodedLength = length(contents.length);
+ final byte[] encoded = new byte[1 + encodedLength.length +
contents.length];
+ encoded[0] = (byte) tag;
+ System.arraycopy(encodedLength, 0, encoded, 1, encodedLength.length);
+ System.arraycopy(contents, 0, encoded, 1 + encodedLength.length,
contents.length);
+ return encoded;
+ }
+
+ private static byte[] length(final int contentsLength) {
+ if (contentsLength < SHORT_FORM_LENGTH_LIMIT) {
+ return new byte[] {(byte) contentsLength};
+ }
+
+ int byteCount = 0;
+ int remaining = contentsLength;
+ while (remaining > 0) {
+ byteCount++;
+ remaining >>>= BITS_PER_BYTE;
+ }
+
+ final byte[] encoded = new byte[byteCount + 1];
+ encoded[0] = (byte) (LONG_FORM_LENGTH | byteCount);
+ for (int i = 0; i < byteCount; i++) {
+ encoded[byteCount - i] = (byte) (contentsLength >>> (i *
BITS_PER_BYTE));
+ }
+
+ return encoded;
+ }
+
+ private static byte[] concatenate(final byte[]... encodedValues) {
+ int contentsLength = 0;
+ for (final byte[] encodedValue : encodedValues) {
+ contentsLength += encodedValue.length;
+ }
+
+ final byte[] concatenated = new byte[contentsLength];
+ int offset = 0;
+ for (final byte[] encodedValue : encodedValues) {
+ System.arraycopy(encodedValue, 0, concatenated, offset,
encodedValue.length);
+ offset += encodedValue.length;
+ }
+
+ return concatenated;
+ }
+}
diff --git
a/nifi-commons/nifi-security-cert-builder/src/main/java/org/apache/nifi/security/cert/builder/StandardCertificateBuilder.java
b/nifi-commons/nifi-security-cert-builder/src/main/java/org/apache/nifi/security/cert/builder/StandardCertificateBuilder.java
index bd25387a4ef..41c8fd63ce6 100644
---
a/nifi-commons/nifi-security-cert-builder/src/main/java/org/apache/nifi/security/cert/builder/StandardCertificateBuilder.java
+++
b/nifi-commons/nifi-security-cert-builder/src/main/java/org/apache/nifi/security/cert/builder/StandardCertificateBuilder.java
@@ -16,64 +16,67 @@
*/
package org.apache.nifi.security.cert.builder;
-import org.bouncycastle.asn1.ASN1Encodable;
-import org.bouncycastle.asn1.x500.RDN;
-import org.bouncycastle.asn1.x500.X500Name;
-import org.bouncycastle.asn1.x500.style.BCStyle;
-import org.bouncycastle.asn1.x500.style.IETFUtils;
-import org.bouncycastle.asn1.x500.style.RFC4519Style;
-import org.bouncycastle.asn1.x509.BasicConstraints;
-import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
-import org.bouncycastle.asn1.x509.Extension;
-import org.bouncycastle.asn1.x509.GeneralName;
-import org.bouncycastle.asn1.x509.GeneralNames;
-import org.bouncycastle.asn1.x509.KeyPurposeId;
-import org.bouncycastle.asn1.x509.KeyUsage;
-import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
-import org.bouncycastle.cert.CertIOException;
-import org.bouncycastle.cert.X509CertificateHolder;
-import org.bouncycastle.cert.X509v3CertificateBuilder;
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
-import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.OperatorCreationException;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
-
+import java.io.ByteArrayInputStream;
import java.math.BigInteger;
+import java.net.IDN;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
import java.security.KeyPair;
-import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
+import java.security.Signature;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Objects;
import java.util.Set;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
import javax.security.auth.x500.X500Principal;
/**
- * Standard X.509 Certificate Builder using Bouncy Castle components
+ * Standard X.509 Certificate Builder
*/
public class StandardCertificateBuilder implements CertificateBuilder {
- private static final String SIGNING_ALGORITHM = "SHA256withRSA";
-
+ private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
+ private static final String SIGNATURE_ALGORITHM_OID =
"1.2.840.113549.1.1.11";
+ private static final String CERTIFICATE_TYPE = "X.509";
+ private static final String COMMON_NAME_TYPE = "CN";
private static final String LOCALHOST = "localhost";
-
+ private static final String BASIC_CONSTRAINTS_OID = "2.5.29.19";
+ private static final String KEY_USAGE_OID = "2.5.29.15";
+ private static final String SUBJECT_KEY_IDENTIFIER_OID = "2.5.29.14";
+ private static final String AUTHORITY_KEY_IDENTIFIER_OID = "2.5.29.35";
+ private static final String EXTENDED_KEY_USAGE_OID = "2.5.29.37";
+ private static final String SUBJECT_ALTERNATIVE_NAME_OID = "2.5.29.17";
+ private static final String CLIENT_AUTHENTICATION_OID =
"1.3.6.1.5.5.7.3.2";
+ private static final String SERVER_AUTHENTICATION_OID =
"1.3.6.1.5.5.7.3.1";
+ private static final int NO_UNUSED_BITS = 0;
+ private static final int VERSION_TAG = 0;
+ private static final int EXTENSIONS_TAG = 3;
+ private static final int DNS_NAME_TAG = 2;
+ private static final int VERSION_3 = 2;
+ private static final int ASCII_LIMIT = 128;
+ private static final int LEAF_KEY_USAGE_UNUSED_BITS = 3;
+ private static final byte LEAF_KEY_USAGE = (byte) 0xF8;
+ private static final int AUTHORITY_KEY_USAGE_UNUSED_BITS = 1;
+ private static final byte AUTHORITY_KEY_USAGE = (byte) 0xFE;
private static final boolean CRITICAL = true;
-
private static final boolean NOT_CRITICAL = false;
-
- private static final int STANDARD_KEY_USAGE = KeyUsage.digitalSignature
- | KeyUsage.keyEncipherment
- | KeyUsage.dataEncipherment
- | KeyUsage.keyAgreement
- | KeyUsage.nonRepudiation;
-
- private static final int AUTHORITY_KEY_USAGE = STANDARD_KEY_USAGE |
KeyUsage.cRLSign | KeyUsage.keyCertSign;
+ private static final byte[] SIGNATURE_ALGORITHM_IDENTIFIER =
DerEncoder.sequence(
+ DerEncoder.objectIdentifier(SIGNATURE_ALGORITHM_OID),
+ DerEncoder.nullValue()
+ );
private final BigInteger serialNumber =
BigInteger.valueOf(System.nanoTime());
@@ -111,12 +114,14 @@ public class StandardCertificateBuilder implements
CertificateBuilder {
*/
@Override
public X509Certificate build() {
- final X509CertificateHolder certificateHolder = getCertificateHolder();
- final JcaX509CertificateConverter certificateConverter = new
JcaX509CertificateConverter();
try {
- return certificateConverter.getCertificate(certificateHolder);
+ final byte[] encodedCertificate = getEncodedCertificate();
+ final CertificateFactory certificateFactory =
CertificateFactory.getInstance(CERTIFICATE_TYPE);
+ return (X509Certificate)
certificateFactory.generateCertificate(new
ByteArrayInputStream(encodedCertificate));
} catch (final CertificateException e) {
throw new IllegalArgumentException("X.509 Certificate conversion
failed", e);
+ } catch (final GeneralSecurityException e) {
+ throw new IllegalArgumentException("Certificate Signer creation
failed", e);
}
}
@@ -149,115 +154,151 @@ public class StandardCertificateBuilder implements
CertificateBuilder {
* @return Builder
*/
public StandardCertificateBuilder setDnsSubjectAlternativeNames(final
Collection<String> dnsSubjectAlternativeNames) {
- this.dnsSubjectAlternativeNames = new
LinkedHashSet<>(Objects.requireNonNull(dnsSubjectAlternativeNames, "DNS Names
required"));
+ final Collection<String> requiredDnsNames =
Objects.requireNonNull(dnsSubjectAlternativeNames, "DNS Names required");
+ this.dnsSubjectAlternativeNames = new
LinkedHashSet<>(requiredDnsNames);
return this;
}
- private void setExtensions(final X509v3CertificateBuilder
certificateBuilder) {
- final JcaX509ExtensionUtils extensionUtils = getExtensionUtils();
-
- try {
- final BasicConstraints basicConstraints = getBasicConstraints();
- certificateBuilder.addExtension(Extension.basicConstraints,
NOT_CRITICAL, basicConstraints);
-
- final KeyUsage keyUsage = getKeyUsage(basicConstraints.isCA());
- certificateBuilder.addExtension(Extension.keyUsage, CRITICAL,
keyUsage);
-
- certificateBuilder.addExtension(Extension.subjectKeyIdentifier,
NOT_CRITICAL, extensionUtils.createSubjectKeyIdentifier(subjectPublicKey));
-
- final PublicKey issuerPublicKey = issuerKeyPair.getPublic();
- certificateBuilder.addExtension(Extension.authorityKeyIdentifier,
NOT_CRITICAL, extensionUtils.createAuthorityKeyIdentifier(issuerPublicKey));
-
- final KeyPurposeId[] keyPurposes = {KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.id_kp_serverAuth};
- final ExtendedKeyUsage extendedKeyUsage = new
ExtendedKeyUsage(keyPurposes);
- certificateBuilder.addExtension(Extension.extendedKeyUsage,
NOT_CRITICAL, extendedKeyUsage);
+ private byte[] getEncodedCertificate() throws GeneralSecurityException {
+ final byte[] tbsCertificate = getTbsCertificate();
+ final Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
+ final PrivateKey issuerPrivateKey = issuerKeyPair.getPrivate();
+ signature.initSign(issuerPrivateKey);
+ signature.update(tbsCertificate);
+ final byte[] signatureBytes = signature.sign();
+ final byte[] signatureValue = DerEncoder.bitString(NO_UNUSED_BITS,
signatureBytes);
+ return DerEncoder.sequence(tbsCertificate,
SIGNATURE_ALGORITHM_IDENTIFIER, signatureValue);
+ }
- final GeneralNames subjectAlternativeNames =
getSubjectAlternativeNames();
- certificateBuilder.addExtension(Extension.subjectAlternativeName,
NOT_CRITICAL, subjectAlternativeNames);
- } catch (final CertIOException e) {
- throw new IllegalArgumentException("Certificate Extension addition
failed", e);
- }
+ private byte[] getTbsCertificate() {
+ final Date notBefore = new Date();
+ final Instant notBeforeInstant = notBefore.toInstant();
+ final Instant notAfterInstant = notBeforeInstant.plus(validityPeriod);
+ final Date notAfter = Date.from(notAfterInstant);
+ final byte[] notBeforeEncoded = DerEncoder.time(notBefore);
+ final byte[] notAfterEncoded = DerEncoder.time(notAfter);
+ final byte[] validity = DerEncoder.sequence(notBeforeEncoded,
notAfterEncoded);
+ final BigInteger versionNumber = BigInteger.valueOf(VERSION_3);
+ final byte[] versionEncoded = DerEncoder.integer(versionNumber);
+ final byte[] version = DerEncoder.explicit(VERSION_TAG,
versionEncoded);
+ final byte[] serialNumberEncoded = DerEncoder.integer(serialNumber);
+ final byte[] issuerEncoded = issuer.getEncoded();
+ final byte[] subjectEncoded = subject.getEncoded();
+ final byte[] subjectPublicKeyEncoded = subjectPublicKey.getEncoded();
+ final byte[] extensionsEncoded = getExtensions();
+ final byte[] extensions = DerEncoder.explicit(EXTENSIONS_TAG,
extensionsEncoded);
+ return DerEncoder.sequence(
+ version,
+ serialNumberEncoded,
+ SIGNATURE_ALGORITHM_IDENTIFIER,
+ issuerEncoded,
+ validity,
+ subjectEncoded,
+ subjectPublicKeyEncoded,
+ extensions
+ );
}
- private BasicConstraints getBasicConstraints() {
+ private byte[] getExtensions() {
final PublicKey issuerPublicKey = issuerKeyPair.getPublic();
final boolean certificateAuthority =
subjectPublicKey.equals(issuerPublicKey);
- return new BasicConstraints(certificateAuthority);
- }
-
- private KeyUsage getKeyUsage(final boolean certificateAuthority) {
- final int keyUsage = certificateAuthority ? AUTHORITY_KEY_USAGE :
STANDARD_KEY_USAGE;
- return new KeyUsage(keyUsage);
- }
-
- private GeneralNames getSubjectAlternativeNames() {
- final Set<GeneralName> generalNames = new LinkedHashSet<>();
-
- final String subjectCommonName = getSubjectCommonName();
- final GeneralName subjectGeneralName = new
GeneralName(GeneralName.dNSName, subjectCommonName);
- generalNames.add(subjectGeneralName);
-
- for (final String dnsSubjectAlternativeName :
dnsSubjectAlternativeNames) {
- final GeneralName generalName = new
GeneralName(GeneralName.dNSName, dnsSubjectAlternativeName);
- generalNames.add(generalName);
+ final byte[] basicConstraints;
+ if (certificateAuthority) {
+ basicConstraints = DerEncoder.sequence(DerEncoder.booleanTrue());
+ } else {
+ basicConstraints = DerEncoder.sequence();
}
- return new GeneralNames(generalNames.toArray(new GeneralName[]{}));
+ final int keyUsageUnusedBits = certificateAuthority ?
AUTHORITY_KEY_USAGE_UNUSED_BITS : LEAF_KEY_USAGE_UNUSED_BITS;
+ final byte keyUsageByte = certificateAuthority ? AUTHORITY_KEY_USAGE :
LEAF_KEY_USAGE;
+ final byte[] keyUsageBits = new byte[] {keyUsageByte};
+ final byte[] keyUsage = DerEncoder.bitString(keyUsageUnusedBits,
keyUsageBits);
+ final byte[] clientAuthentication =
DerEncoder.objectIdentifier(CLIENT_AUTHENTICATION_OID);
+ final byte[] serverAuthentication =
DerEncoder.objectIdentifier(SERVER_AUTHENTICATION_OID);
+ final byte[] extendedKeyUsage =
DerEncoder.sequence(clientAuthentication, serverAuthentication);
+ final byte[] subjectPublicKeyEncoded = subjectPublicKey.getEncoded();
+ final byte[] subjectKeyIdentifier =
DerEncoder.subjectKeyIdentifier(subjectPublicKeyEncoded);
+ final byte[] issuerPublicKeyEncoded = issuerPublicKey.getEncoded();
+ final byte[] authorityKeyIdentifier =
DerEncoder.authorityKeyIdentifier(issuerPublicKeyEncoded);
+ final byte[] subjectAlternativeNames = getSubjectAlternativeNames();
+ return DerEncoder.sequence(
+ extension(BASIC_CONSTRAINTS_OID, NOT_CRITICAL,
basicConstraints),
+ extension(KEY_USAGE_OID, CRITICAL, keyUsage),
+ extension(SUBJECT_KEY_IDENTIFIER_OID, NOT_CRITICAL,
subjectKeyIdentifier),
+ extension(AUTHORITY_KEY_IDENTIFIER_OID, NOT_CRITICAL,
authorityKeyIdentifier),
+ extension(EXTENDED_KEY_USAGE_OID, NOT_CRITICAL,
extendedKeyUsage),
+ extension(SUBJECT_ALTERNATIVE_NAME_OID, NOT_CRITICAL,
subjectAlternativeNames)
+ );
}
- private String getSubjectCommonName() {
- final X500Name subjectName = getName(subject);
- final RDN[] commonNames = subjectName.getRDNs(BCStyle.CN);
-
- final String subjectCommonName;
- if (commonNames.length == 0) {
- subjectCommonName = LOCALHOST;
- } else {
- final RDN commonName = commonNames[0];
- final ASN1Encodable commonNameEncoded =
commonName.getFirst().getValue();
- subjectCommonName = IETFUtils.valueToString(commonNameEncoded);
+ private byte[] extension(final String extensionOid, final boolean
critical, final byte[] extensionValue) {
+ final byte[] encodedObjectIdentifier =
DerEncoder.objectIdentifier(extensionOid);
+ final byte[] encodedValue = DerEncoder.octetString(extensionValue);
+ if (critical) {
+ return DerEncoder.sequence(encodedObjectIdentifier,
DerEncoder.booleanTrue(), encodedValue);
}
- return subjectCommonName;
+ return DerEncoder.sequence(encodedObjectIdentifier, encodedValue);
}
- private X509CertificateHolder getCertificateHolder() {
- final X509v3CertificateBuilder certificateBuilder =
getCertificateBuilder();
- setExtensions(certificateBuilder);
+ private byte[] getSubjectAlternativeNames() {
+ final List<byte[]> encodedDnsNames = new ArrayList<>();
+ final String subjectCommonName = getSubjectCommonName();
+ final byte[] subjectCommonNameEncoded =
getDnsNameEncoded(subjectCommonName);
+ encodedDnsNames.add(subjectCommonNameEncoded);
+ for (final String dnsSubjectAlternativeName :
dnsSubjectAlternativeNames) {
+ final byte[] dnsNameEncoded =
getDnsNameEncoded(dnsSubjectAlternativeName);
+ boolean encodedDnsNameFound = false;
+ for (final byte[] encodedDnsName : encodedDnsNames) {
+ if (Arrays.equals(encodedDnsName, dnsNameEncoded)) {
+ encodedDnsNameFound = true;
+ break;
+ }
+ }
+
+ if (encodedDnsNameFound) {
+ continue;
+ }
+
+ encodedDnsNames.add(dnsNameEncoded);
+ }
- final ContentSigner contentSigner = getContentSigner();
- return certificateBuilder.build(contentSigner);
- }
+ final byte[][] generalNames = new byte[encodedDnsNames.size()][];
+ int index = 0;
+ for (final byte[] dnsNameEncoded : encodedDnsNames) {
+ final byte[] generalName = DerEncoder.implicit(DNS_NAME_TAG,
dnsNameEncoded);
+ generalNames[index++] = generalName;
+ }
- private X509v3CertificateBuilder getCertificateBuilder() {
- final X500Name issuerName = getName(issuer);
- final Date notBefore = new Date();
- final Date notAfter =
Date.from(notBefore.toInstant().plus(validityPeriod));
- final X500Name subjectName = getName(subject);
- final SubjectPublicKeyInfo subjectPublicKeyInfo =
SubjectPublicKeyInfo.getInstance(subjectPublicKey.getEncoded());
- return new X509v3CertificateBuilder(issuerName, serialNumber,
notBefore, notAfter, subjectName, subjectPublicKeyInfo);
+ return DerEncoder.sequence(generalNames);
}
- private ContentSigner getContentSigner() {
- final JcaContentSignerBuilder contentSignerBuilder = new
JcaContentSignerBuilder(SIGNING_ALGORITHM);
-
- final PrivateKey issuerPrivateKey = issuerKeyPair.getPrivate();
- try {
- return contentSignerBuilder.build(issuerPrivateKey);
- } catch (final OperatorCreationException e) {
- throw new IllegalArgumentException("Certificate Signer creation
failed", e);
+ private byte[] getDnsNameEncoded(final String dnsName) {
+ for (int i = 0; i < dnsName.length(); i++) {
+ if (dnsName.charAt(i) >= ASCII_LIMIT) {
+ final String asciiDnsName = IDN.toASCII(dnsName);
+ return asciiDnsName.getBytes(StandardCharsets.US_ASCII);
+ }
}
+
+ return dnsName.getBytes(StandardCharsets.US_ASCII);
}
- private JcaX509ExtensionUtils getExtensionUtils() {
+ private String getSubjectCommonName() {
try {
- return new JcaX509ExtensionUtils();
- } catch (final NoSuchAlgorithmException e) {
- throw new IllegalArgumentException("Certificate Extension
Utilities creation failed", e);
+ final String subjectDistinguishedName = subject.getName();
+ final LdapName subjectName = new
LdapName(subjectDistinguishedName);
+ for (final Rdn relativeDistinguishedName : subjectName.getRdns()) {
+ final String attributeType =
relativeDistinguishedName.getType();
+ if (COMMON_NAME_TYPE.equalsIgnoreCase(attributeType)) {
+ final Object commonName =
relativeDistinguishedName.getValue();
+ return commonName.toString();
+ }
+ }
+ } catch (final InvalidNameException e) {
+ throw new IllegalArgumentException("Subject common name parsing
failed", e);
}
- }
- private X500Name getName(final X500Principal principal) {
- return new X500Name(RFC4519Style.INSTANCE, principal.getName());
+ return LOCALHOST;
}
}
diff --git
a/nifi-commons/nifi-security-cert-builder/src/test/java/org/apache/nifi/security/cert/builder/StandardCertificateBuilderTest.java
b/nifi-commons/nifi-security-cert-builder/src/test/java/org/apache/nifi/security/cert/builder/StandardCertificateBuilderTest.java
index 6e49cd7a173..a6eb96fd46d 100644
---
a/nifi-commons/nifi-security-cert-builder/src/test/java/org/apache/nifi/security/cert/builder/StandardCertificateBuilderTest.java
+++
b/nifi-commons/nifi-security-cert-builder/src/test/java/org/apache/nifi/security/cert/builder/StandardCertificateBuilderTest.java
@@ -16,10 +16,10 @@
*/
package org.apache.nifi.security.cert.builder;
-import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
@@ -58,7 +58,13 @@ class StandardCertificateBuilderTest {
private static final int STANDARD_CONSTRAINTS = -1;
- private static final List<String> EXTENDED_KEY_USAGE =
Arrays.asList(KeyPurposeId.id_kp_clientAuth.toString(),
KeyPurposeId.id_kp_serverAuth.toString());
+ private static final String KEY_USAGE_OID = "2.5.29.15";
+
+ private static final String CLIENT_AUTHENTICATION_OID =
"1.3.6.1.5.5.7.3.2";
+
+ private static final String SERVER_AUTHENTICATION_OID =
"1.3.6.1.5.5.7.3.1";
+
+ private static final List<String> EXTENDED_KEY_USAGE =
Arrays.asList(CLIENT_AUTHENTICATION_OID, SERVER_AUTHENTICATION_OID);
private static final boolean[] AUTHORITY_KEY_USAGE = new boolean[]{true,
true, true, true, true, true, true, false, false};
@@ -86,7 +92,7 @@ class StandardCertificateBuilderTest {
}
@Test
- void testBuildSelfSigned() throws CertificateParsingException {
+ void testBuildSelfSigned() throws GeneralSecurityException {
final StandardCertificateBuilder builder = new
StandardCertificateBuilder(issuerKeyPair, ISSUER, VALIDITY_PERIOD);
final X509Certificate certificate = builder.build();
@@ -101,7 +107,7 @@ class StandardCertificateBuilderTest {
}
@Test
- void testBuildSelfSignedSubjectAlternativeNames() throws
CertificateParsingException {
+ void testBuildSelfSignedSubjectAlternativeNames() throws
GeneralSecurityException {
final StandardCertificateBuilder builder = new
StandardCertificateBuilder(issuerKeyPair, ISSUER, VALIDITY_PERIOD);
final X509Certificate certificate =
builder.setDnsSubjectAlternativeNames(Collections.singletonList(DNS_NAME)).build();
@@ -117,7 +123,7 @@ class StandardCertificateBuilderTest {
}
@Test
- void testBuildIssued() throws CertificateParsingException {
+ void testBuildIssued() throws GeneralSecurityException {
final StandardCertificateBuilder builder = new
StandardCertificateBuilder(issuerKeyPair, ISSUER, VALIDITY_PERIOD);
final PublicKey subjectPublicKey = subjectKeyPair.getPublic();
@@ -131,9 +137,12 @@ class StandardCertificateBuilderTest {
assertFirstSubjectAlternativeNameEquals(SUBJECT_COMMON_NAME,
certificate);
}
- private void assertPropertiesFound(final X509Certificate certificate)
throws CertificateParsingException {
+ private void assertPropertiesFound(final X509Certificate certificate)
throws GeneralSecurityException {
assertNotNull(certificate);
+ certificate.verify(issuerKeyPair.getPublic());
+ assertEquals(Collections.singleton(KEY_USAGE_OID),
certificate.getCriticalExtensionOIDs());
+
assertEquals(ISSUER, certificate.getIssuerX500Principal());
final Date notBefore = certificate.getNotBefore();