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

Reply via email to