You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ozone.apache.org by ad...@apache.org on 2026/10/24 08:25:32 UTC

(ozone) branch master updated: HDDS-17353. Register BouncyCastle provider only when it is looked up by name (#11725)

This is an automated email from the ASF dual-hosted git repository.

adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 69d77beff60 HDDS-17353. Register BouncyCastle provider only when it is looked up by name (#11725)
69d77beff60 is described below

commit 69d77beff60b9106e2517c36a7a1b2cc726af568
Author: Andrey Yarovoy <11...@users.noreply.github.com>
AuthorDate: Sat Oct 24 04:25:25 2026 -0400

    HDDS-17353. Register BouncyCastle provider only when it is looked up by name (#11725)
    
    Co-authored-by: Claude Code (Claude Opus 5.5)
---
 .../hadoop/hdds/security/SecurityConfig.java       | 23 +++++--
 .../x509/certificate/utils/CertificateCodec.java   |  5 +-
 .../org/apache/hadoop/ozone/OzoneSecurityUtil.java |  2 -
 .../TestCertificateCodecProviderRegistration.java  | 53 ++++++++++++++
 .../security/TestSecurityProviderRegistration.java | 80 ++++++++++++++++++++++
 5 files changed, 155 insertions(+), 8 deletions(-)

diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/SecurityConfig.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/SecurityConfig.java
index 2d38da89f67..70bacd89fe4 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/SecurityConfig.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/SecurityConfig.java
@@ -167,7 +167,7 @@ public SecurityConfig(ConfigurationSource configuration) {
     this.size = configuration.getInt(HDDS_KEY_LEN, HDDS_DEFAULT_KEY_LEN);
     this.keyAlgo = configuration.get(HDDS_KEY_ALGORITHM,
         HDDS_DEFAULT_KEY_ALGORITHM);
-    initSecurityProvider(configuration);
+    // The provider is registered by getProvider(), when something needs it: most clients never do.
     this.providerString = configuration.get(HDDS_SECURITY_PROVIDER, HDDS_DEFAULT_SECURITY_PROVIDER);
 
     // Please Note: To make it easy for our customers we will attempt to read
@@ -304,10 +304,22 @@ public SecurityConfig(ConfigurationSource configuration) {
         : Collections.unmodifiableList(Arrays.asList(configuration.getTrimmedStrings(HDDS_GRPC_TLS_CIPHERS)));
   }
 
-  public static synchronized void initSecurityProvider(ConfigurationSource configuration) {
+  public static void initSecurityProvider(ConfigurationSource configuration) {
+    initSecurityProvider(configuration.get(HDDS_SECURITY_PROVIDER, HDDS_DEFAULT_SECURITY_PROVIDER));
+  }
+
+  /**
+   * Registers the security provider, BouncyCastle for "BC", if no provider has been resolved yet.
+   */
+  public static void initSecurityProvider(String providerString) {
+    if (SecurityConfig.provider == null) {
+      initSecurityProviderSynchronized(providerString);
+    }
+  }
+
+  private static synchronized void initSecurityProviderSynchronized(String providerString) {
     // First Startup -- if the provider is null, check for the provider.
     if (SecurityConfig.provider == null) {
-      String providerString = configuration.get(HDDS_SECURITY_PROVIDER, HDDS_DEFAULT_SECURITY_PROVIDER);
       provider = Security.getProvider(providerString);
       if (SecurityConfig.provider == null) {
         // Provider not found, let us try to Dynamically initialize the
@@ -504,12 +516,13 @@ public int getSize() {
   }
 
   /**
-   * Returns the Provider name. SCM defaults to using Bouncy Castle and will
-   * return "BC".
+   * Returns the Provider name, after registering the provider so that it can be looked up by that name.
+   * SCM defaults to using Bouncy Castle and will return "BC".
    *
    * @return String Provider name.
    */
   public String getProvider() {
+    initSecurityProvider(providerString);
     return providerString;
   }
 
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/CertificateCodec.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/CertificateCodec.java
index f737e785ade..3089b20bd0c 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/CertificateCodec.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/CertificateCodec.java
@@ -164,7 +164,8 @@ public static X509Certificate getX509Certificate(byte[] pemEncoded)
   }
 
   public static X509Certificate readX509Certificate(InputStream input) throws CertificateException {
-    final Certificate cert = getCertFactory().generateCertificate(input);
+    // The JDK's X.509 factory: a client reading CA certificates must not need the BouncyCastle provider.
+    final Certificate cert = CertificateFactory.getInstance("X.509").generateCertificate(input);
     if (cert instanceof X509Certificate) {
       return (X509Certificate) cert;
     }
@@ -184,6 +185,8 @@ public static X509Certificate firstCertificateFrom(CertPath certificatePath) {
   }
 
   public static CertificateFactory getCertFactory() throws CertificateException {
+    // BouncyCastle, for the "PEM" cert path encoding only it supports
+    SecurityConfig.initSecurityProvider("BC");
     try {
       return CertificateFactory.getInstance("X.509", "BC");
     } catch (NoSuchProviderException e) {
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneSecurityUtil.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneSecurityUtil.java
index 6e9da83b2bb..438cdcee7d4 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneSecurityUtil.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneSecurityUtil.java
@@ -35,7 +35,6 @@
 import org.apache.hadoop.hdds.annotation.InterfaceAudience;
 import org.apache.hadoop.hdds.annotation.InterfaceStability;
 import org.apache.hadoop.hdds.conf.ConfigurationSource;
-import org.apache.hadoop.hdds.security.SecurityConfig;
 import org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateCodec;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,7 +53,6 @@ private OzoneSecurityUtil() {
   }
 
   public static boolean isSecurityEnabled(ConfigurationSource conf) {
-    SecurityConfig.initSecurityProvider(conf);
     return conf.getBoolean(OZONE_SECURITY_ENABLED_KEY, OZONE_SECURITY_ENABLED_DEFAULT);
   }
 
diff --git a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/TestCertificateCodecProviderRegistration.java b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/TestCertificateCodecProviderRegistration.java
new file mode 100644
index 00000000000..7fa83ee6361
--- /dev/null
+++ b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/TestCertificateCodecProviderRegistration.java
@@ -0,0 +1,53 @@
+/*
+ * 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.hadoop.hdds.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Security;
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import org.apache.hadoop.hdds.security.x509.CertificateTestUtils;
+import org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateCodec;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests that {@link CertificateCodec#getCertFactory()} registers BouncyCastle when nothing has registered it yet.
+ * Registration is JVM-wide, so this runs in its own class (and so its own JVM).
+ */
+class TestCertificateCodecProviderRegistration {
+
+  @Test
+  void certFactoryRegistersBouncyCastle() throws Exception {
+    assertNull(Security.getProvider("BC"), "BouncyCastle registered before the test");
+    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+    generator.initialize(2048);
+    KeyPair keys = generator.generateKeyPair();
+    X509Certificate cert = CertificateTestUtils.createSelfSignedCert(keys, "ca");
+
+    // the "PEM" cert path encoding is BouncyCastle's
+    CertPath path = CertificateCodec.getCertPathFromPemEncodedString(CertificateCodec.getPEMEncodedString(cert));
+
+    assertEquals(cert, CertificateCodec.firstCertificateFrom(path));
+    assertNotNull(Security.getProvider("BC"));
+  }
+}
diff --git a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/TestSecurityProviderRegistration.java b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/TestSecurityProviderRegistration.java
new file mode 100644
index 00000000000..1ee52877c89
--- /dev/null
+++ b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/TestSecurityProviderRegistration.java
@@ -0,0 +1,80 @@
+/*
+ * 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.hadoop.hdds.security;
+
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SECURITY_ENABLED_KEY;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.security.x509.CertificateTestUtils;
+import org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateCodec;
+import org.apache.hadoop.ozone.OzoneSecurityUtil;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+/**
+ * Tests that the client path does not register the BouncyCastle provider, and that code which names the provider
+ * gets it registered. Registration is JVM-wide, so the tests run in order, in this class's own JVM.
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class TestSecurityProviderRegistration {
+
+  private static final String BC = "BC";
+
+  @Test
+  @Order(1)
+  void clientPathDoesNotRegisterBouncyCastle() throws Exception {
+    assertNull(Security.getProvider(BC), "BouncyCastle registered before the test");
+    OzoneConfiguration conf = new OzoneConfiguration();
+    conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
+
+    new SecurityConfig(conf);
+    assertTrue(OzoneSecurityUtil.isSecurityEnabled(conf));
+
+    // a client reading a CA certificate, as from ServiceInfoEx.getCaCertificates()
+    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+    generator.initialize(2048);
+    KeyPair keys = generator.generateKeyPair();
+    X509Certificate original = CertificateTestUtils.createSelfSignedCert(keys, "ca");
+    X509Certificate parsed = CertificateCodec.getX509Certificate(CertificateCodec.getPEMEncodedString(original));
+    assertArrayEquals(original.getEncoded(), parsed.getEncoded());
+    assertEquals(original, parsed);
+
+    assertNull(Security.getProvider(BC), "The client path must not register BouncyCastle");
+  }
+
+  @Test
+  @Order(2)
+  void getProviderRegistersBouncyCastle() {
+    SecurityConfig securityConfig = new SecurityConfig(new OzoneConfiguration());
+    assertNull(Security.getProvider(BC));
+
+    assertEquals(BC, securityConfig.getProvider());
+    assertNotNull(Security.getProvider(BC), "getProvider() must register the provider it names");
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@ozone.apache.org
For additional commands, e-mail: commits-help@ozone.apache.org