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

sammichen 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 37d1335413 HDDS-8605. Implement the ability to update the ServiceInfo 
object with the new rootCA (#5009)
37d1335413 is described below

commit 37d13354131377822ca20958f2490cfceea0d488
Author: Istvan Fajth <[email protected]>
AuthorDate: Fri Jul 14 04:06:08 2023 +0200

    HDDS-8605. Implement the ability to update the ServiceInfo object with the 
new rootCA (#5009)
---
 .../x509/certificate/client/CertificateClient.java |  11 ++
 .../hdds/security/x509/keys/HDDSKeyGenerator.java  |   0
 .../hdds/security/x509/keys/package-info.java      |  23 +++
 .../hadoop/hdds/security/x509/package-info.java    |  74 ----------
 .../hdds/security/x509/CertificateTestUtils.java   | 162 ++++++++++++++++++++
 .../hadoop/hdds/security/x509/package-info.java    |   0
 .../client/DefaultCertificateClient.java           |   9 ++
 .../client/CertificateClientTestImpl.java          |  11 +-
 .../scm/ha/TestInterSCMGrpcProtocolService.java    |  94 +-----------
 .../compose/ozonesecure-ha/root-ca-rotation.yaml   |   1 +
 .../ozonesecure-ha/test-root-ca-rotation.sh        |   6 +
 .../compose/ozonesecure/test-root-ca-rotation.sh   |   9 +-
 .../root-ca-rotation-client-checks.robot           |  46 ++++++
 .../hadoop/ozone/TestSecureOzoneCluster.java       |   3 +
 .../org/apache/hadoop/ozone/om/OzoneManager.java   |  17 +--
 .../hadoop/ozone/om/ServiceInfoProvider.java       | 164 +++++++++++++++++++++
 .../hadoop/ozone/om/TestServiceInfoProvider.java   | 163 ++++++++++++++++++++
 17 files changed, 616 insertions(+), 177 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/client/CertificateClient.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/client/CertificateClient.java
index 12a47712c5..d969439c3a 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/client/CertificateClient.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/client/CertificateClient.java
@@ -35,6 +35,8 @@ import java.security.cert.X509Certificate;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
 
 import static 
org.apache.hadoop.hdds.security.exception.OzoneSecurityException.ResultCodes.OM_PUBLIC_PRIVATE_KEY_FILE_NOT_EXIST;
 
@@ -232,6 +234,15 @@ public interface CertificateClient extends Closeable {
    */
   void registerNotificationReceiver(CertificateNotification receiver);
 
+  /**
+   * Registers a listener that will be notified if the CA certificates are
+   * changed.
+   *
+   * @param listener the listener to call with the actualized list of CA
+   *                 certificates.
+   */
+  void registerRootCARotationListener(
+      Function<List<X509Certificate>, CompletableFuture<Void>> listener);
 
   /**
    * Initialize certificate client.
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/keys/HDDSKeyGenerator.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/keys/HDDSKeyGenerator.java
similarity index 100%
rename from 
hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/keys/HDDSKeyGenerator.java
rename to 
hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/keys/HDDSKeyGenerator.java
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/keys/package-info.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/keys/package-info.java
new file mode 100644
index 0000000000..4fffbf7da7
--- /dev/null
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/keys/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ *
+ */
+
+/**
+ * Utils for private and public keys.
+ */
+package org.apache.hadoop.hdds.security.x509.keys;
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/package-info.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/package-info.java
index 33f607106c..85f1b7ec78 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/package-info.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/package-info.java
@@ -23,77 +23,3 @@
  * framework for HDDS.
  */
 package org.apache.hadoop.hdds.security.x509;
-/*
-
-Architecture of Certificate Infrastructure for SCM.
-====================================================
-
-The certificate infrastructure has two main parts, the certificate server or
-the Certificate authority and the clients who want certificates. The CA is
-responsible for issuing certificates to participating entities.
-
-To issue a certificate the CA has to verify the identity and the assertions
-in the certificate. The client starts off making a request to CA for a
-certificate.  This request is called Certificate Signing Request or CSR
-(PKCS#10).
-
-When a CSR arrives on the CA, CA will decode the CSR and verify that all the
-fields in the CSR are in line with what the system expects. Since there are
-lots of possible ways to construct an X.509 certificate, we rely on PKI
-profiles.
-
-Generally, PKI profiles are policy documents or general guidelines that get
-followed by the requester and CA. However, most of the PKI profiles that are
-commonly available are general purpose and offers too much surface area.
-
-SCM CA infrastructure supports the notion of a PKI profile class which can
-codify the RDNs, Extensions and other certificate policies. The CA when
-issuing a certificate will invoke a certificate approver class, based on the
-authentication method used. For example, out of the box, we support manual,
-Kerberos, trusted network and testing authentication mechanisms.
-
-If there is no authentication mechanism in place, then when CA receives the
-CSR, it runs the standard PKI profile over it verify that all the fields are
-in expected ranges. Once that is done, The signing request is sent for human
-review and approval. This form of certificate approval is called Manual,  Of
-all the certificate approval process this is the ** most secure **. This
-approval needs to be done once for each data node.
-
-For existing clusters, where data nodes already have a Kerberos keytab,  we
-can leverage the Kerberos identity mechanism to identify the data node that
-is requesting the certificate. In this case, users can configure the system
-to leverage Kerberos while issuing certificates and SCM CA will be able to
-verify the data nodes identity and issue certificates automatically.
-
-In environments like Kubernetes, we can leverage the base system services to
-pass on a shared secret securely. In this model also, we can rely on these
-secrets to make sure that is the right data node that is talking to us. This
-kind of approval is called a Trusted network approval. In this process, each
-data node not only sends the CSR but signs the request with a shared secret
-with SCM. SCM then can issue a certificate without the intervention of a
-human administrator.
-
-The last, TESTING method which never should be used other than in development
- and testing clusters, is merely a mechanism to bypass all identity checks. If
-this flag is setup, then CA will issue a CSR if the base approves all fields.
-
- * Please do not use this mechanism(TESTING) for any purpose other than
- * testing.
-
-CA - Certificate Approval and Code Layout (as of Dec, 1st, 2018)
-=================================================================
-The CA implementation ( as of now it is called DefaultCA) receives a CSR from
- the network layer. The network also tells the system what approver type to
- use, that is if Kerberos or Shared secrets mechanism is used, it reports
- that to Default CA.
-
-The default CA instantiates the approver based on the type of the approver
-indicated by the network layer. This approver creates an instance of the PKI
-profile and passes each field from the certificate signing request. The PKI
-profile (as of today Dec 1st, 2018, we have one profile called Ozone profile)
- verifies that each field in the CSR meets the approved set of values.
-
-Once the PKI Profile validates the request, it is either auto approved or
-queued for manual review.
-
- */
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/x509/CertificateTestUtils.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/x509/CertificateTestUtils.java
new file mode 100644
index 0000000000..09d34fd0bb
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/x509/CertificateTestUtils.java
@@ -0,0 +1,162 @@
+/*
+ * 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.x509;
+
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.security.SecurityConfig;
+import org.apache.hadoop.hdds.security.x509.keys.HDDSKeyGenerator;
+import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509ExtensionUtils;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.DigestCalculator;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+
+/**
+ * Test utilities to create simple certificates/keys for testing.
+ */
+public final class CertificateTestUtils {
+  private CertificateTestUtils() { }
+
+  private static final String HASH_ALGO = "SHA256WithRSA";
+
+  /**
+   * Generates a keypair using the HDDSKeyGenerator with the given config.
+   *
+   * @param conf the config applies to keys
+   *
+   * @return a newly generated keypair
+   *
+   * @throws NoSuchProviderException on wrong security provider in the config
+   * @throws NoSuchAlgorithmException on wrong encryption algo in the config
+   */
+  public static KeyPair aKeyPair(ConfigurationSource conf)
+      throws NoSuchProviderException, NoSuchAlgorithmException {
+    return new HDDSKeyGenerator(new SecurityConfig(conf)).generateKey();
+  }
+
+  /**
+   * Creates a self-signed certificate and returns it as an X509Certificate.
+   * The given keys and common name are being used in the certificate.
+   * The certificate will have its serial id generated based on the hashcode
+   * of the public key, and will expire after 1 day.
+   *
+   * @param keys the keypair to use for the certificate
+   * @param commonName the common name used in the certificate
+   *
+   * @return the X509Certificate representing a self-signed certificate
+   *
+   * @throws Exception in case any error occurs during the certificate creation
+   */
+  public static X509Certificate createSelfSignedCert(KeyPair keys,
+      String commonName) throws Exception {
+    return createSelfSignedCert(keys, commonName, Duration.ofDays(1));
+  }
+
+  /**
+   * Creates a self-signed certificate and returns it as an X509Certificate.
+   * The given keys and common name are being used in the certificate.
+   * The certificate will have its serial id generated based on the hashcode
+   * of the public key, and will expire after the specified duration.
+   *
+   * @param keys the keypair to use for the certificate
+   * @param commonName the common name used in the certificate
+   * @param expiresIn the lifespan of the certificate
+   *
+   * @return the X509Certificate representing a self-signed certificate
+   *
+   * @throws Exception in case any error occurs during the certificate creation
+   */
+  public static X509Certificate createSelfSignedCert(KeyPair keys,
+      String commonName, Duration expiresIn) throws Exception {
+    final Instant now = Instant.now();
+    final Date notBefore = Date.from(now);
+    final Date notAfter = Date.from(now.plus(expiresIn));
+    final ContentSigner contentSigner =
+        new JcaContentSignerBuilder(HASH_ALGO).build(keys.getPrivate());
+    final X500Name x500Name = new X500Name("CN=" + commonName);
+
+    SubjectKeyIdentifier keyId = subjectKeyIdOf(keys);
+    AuthorityKeyIdentifier authorityKeyId = authorityKeyIdOf(keys);
+    BasicConstraints constraints = new BasicConstraints(true);
+
+    final X509v3CertificateBuilder certificateBuilder =
+        new JcaX509v3CertificateBuilder(
+            x500Name,
+            BigInteger.valueOf(keys.getPublic().hashCode()),
+            notBefore,
+            notAfter,
+            x500Name,
+            keys.getPublic()
+        );
+    certificateBuilder
+        .addExtension(Extension.subjectKeyIdentifier, false, keyId)
+        .addExtension(Extension.authorityKeyIdentifier, false, authorityKeyId)
+        .addExtension(Extension.basicConstraints, true, constraints);
+
+    return new JcaX509CertificateConverter()
+        .setProvider(new BouncyCastleProvider())
+        .getCertificate(certificateBuilder.build(contentSigner));
+  }
+
+  private static SubjectKeyIdentifier subjectKeyIdOf(KeyPair keys)
+      throws Exception {
+    return extensionUtil().createSubjectKeyIdentifier(pubKeyInfo(keys));
+  }
+
+  private static AuthorityKeyIdentifier authorityKeyIdOf(KeyPair keys)
+      throws Exception {
+    return extensionUtil().createAuthorityKeyIdentifier(pubKeyInfo(keys));
+  }
+
+  private static SubjectPublicKeyInfo pubKeyInfo(KeyPair keys) {
+    return SubjectPublicKeyInfo.getInstance(keys.getPublic().getEncoded());
+  }
+
+  private static X509ExtensionUtils extensionUtil()
+      throws OperatorCreationException {
+    DigestCalculator digest =
+        new BcDigestCalculatorProvider()
+            .get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
+
+    return new X509ExtensionUtils(digest);
+  }
+}
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/package-info.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/x509/package-info.java
similarity index 100%
rename from 
hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/package-info.java
rename to 
hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/security/x509/package-info.java
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/client/DefaultCertificateClient.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/client/DefaultCertificateClient.java
index 6dff86fd52..e066efef8c 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/client/DefaultCertificateClient.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/client/DefaultCertificateClient.java
@@ -58,6 +58,7 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.stream.Stream;
 import java.util.stream.Collectors;
 
@@ -203,6 +204,14 @@ public abstract class DefaultCertificateClient implements 
CertificateClient {
     }
   }
 
+  @Override
+  public void registerRootCARotationListener(
+      Function<List<X509Certificate>, CompletableFuture<Void>> listener) {
+    if (securityConfig.isAutoCARotationEnabled()) {
+      rootCaRotationPoller.addRootCARotationProcessor(listener);
+    }
+  }
+
   private synchronized void readCertificateFile(Path filePath) {
     CertificateCodec codec = new CertificateCodec(securityConfig, component);
     String fileName = filePath.getFileName().toString();
diff --git 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/client/CertificateClientTestImpl.java
 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/client/CertificateClientTestImpl.java
index 40395ac128..32fa5ef40e 100644
--- 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/client/CertificateClientTestImpl.java
+++ 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/client/CertificateClientTestImpl.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
@@ -37,10 +37,12 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
@@ -386,6 +388,13 @@ public class CertificateClientTestImpl implements 
CertificateClient {
     }
   }
 
+  @Override
+  public void registerRootCARotationListener(
+      Function<List<X509Certificate>, CompletableFuture<Void>> listener) {
+    // we do not have tests that rely on rootCA rotation atm, leaving this
+    // implementation blank for now.
+  }
+
   @Override
   public void close() throws IOException {
     if (serverKeyStoresFactory != null) {
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestInterSCMGrpcProtocolService.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestInterSCMGrpcProtocolService.java
index 998839b2d8..8e644c585a 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestInterSCMGrpcProtocolService.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestInterSCMGrpcProtocolService.java
@@ -19,38 +19,18 @@ package org.apache.hadoop.hdds.scm.ha;
 
 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
 import org.apache.hadoop.hdds.HddsConfigKeys;
-import org.apache.hadoop.hdds.conf.ConfigurationSource;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.scm.ScmConfigKeys;
 import org.apache.hadoop.hdds.scm.metadata.SCMMetadataStore;
 import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
 import org.apache.hadoop.hdds.security.ssl.KeyStoresFactory;
-import org.apache.hadoop.hdds.security.SecurityConfig;
+import org.apache.hadoop.hdds.security.x509.CertificateTestUtils;
 import 
org.apache.hadoop.hdds.security.x509.certificate.client.SCMCertificateClient;
-import org.apache.hadoop.hdds.security.x509.keys.HDDSKeyGenerator;
 import org.apache.hadoop.hdds.utils.TransactionInfo;
 import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
 import org.apache.hadoop.hdds.utils.db.DBStore;
 import org.apache.hadoop.hdds.utils.db.Table;
 import org.apache.hadoop.ozone.OzoneConfigKeys;
-import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
-import org.bouncycastle.asn1.x500.X500Name;
-import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
-import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
-import org.bouncycastle.asn1.x509.BasicConstraints;
-import org.bouncycastle.asn1.x509.Extension;
-import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
-import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
-import org.bouncycastle.cert.X509ExtensionUtils;
-import org.bouncycastle.cert.X509v3CertificateBuilder;
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
-import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.DigestCalculator;
-import org.bouncycastle.operator.OperatorCreationException;
-import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -64,22 +44,17 @@ import javax.net.ssl.X509TrustManager;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.math.BigInteger;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.KeyPair;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Date;
 import java.util.Random;
 import java.util.concurrent.CompletableFuture;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static 
org.apache.hadoop.hdds.security.x509.CertificateTestUtils.createSelfSignedCert;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.ArgumentMatchers.any;
@@ -225,8 +200,8 @@ public class TestInterSCMGrpcProtocolService {
   private SCMCertificateClient setupCertificateClientForMTLS(
       OzoneConfiguration conf
   ) throws Exception {
-    KeyPair serviceKeys = aKeyPair(conf);
-    KeyPair clientKeys = aKeyPair(conf);
+    KeyPair serviceKeys = CertificateTestUtils.aKeyPair(conf);
+    KeyPair clientKeys = CertificateTestUtils.aKeyPair(conf);
 
     serviceCert = createSelfSignedCert(serviceKeys, "service");
     clientCert = createSelfSignedCert(clientKeys, "client");
@@ -287,11 +262,6 @@ public class TestInterSCMGrpcProtocolService {
     return keyManager;
   }
 
-  private KeyPair aKeyPair(ConfigurationSource conf)
-      throws NoSuchProviderException, NoSuchAlgorithmException {
-    return new HDDSKeyGenerator(new SecurityConfig(conf)).generateKey();
-  }
-
   private OzoneConfiguration setupConfiguration(int port) {
     OzoneConfiguration conf = new OzoneConfiguration();
     conf.setInt(ScmConfigKeys.OZONE_SCM_GRPC_PORT_KEY, port);
@@ -301,60 +271,4 @@ public class TestInterSCMGrpcProtocolService {
   }
 
 
-  private static final String HASH_ALGO = "SHA256WithRSA";
-
-  private X509Certificate createSelfSignedCert(KeyPair keys, String commonName)
-      throws Exception {
-    final Instant now = Instant.now();
-    final Date notBefore = Date.from(now);
-    final Date notAfter = Date.from(now.plus(Duration.ofDays(1)));
-    final ContentSigner contentSigner =
-        new JcaContentSignerBuilder(HASH_ALGO).build(keys.getPrivate());
-    final X500Name x500Name = new X500Name("CN=" + commonName);
-
-    SubjectKeyIdentifier keyId = subjectKeyIdOf(keys);
-    AuthorityKeyIdentifier authorityKeyId = authorityKeyIdOf(keys);
-    BasicConstraints constraints = new BasicConstraints(true);
-
-    final X509v3CertificateBuilder certificateBuilder =
-        new JcaX509v3CertificateBuilder(
-            x500Name,
-            BigInteger.valueOf(keys.getPublic().hashCode()),
-            notBefore,
-            notAfter,
-            x500Name,
-            keys.getPublic()
-        );
-    certificateBuilder
-        .addExtension(Extension.subjectKeyIdentifier, false, keyId)
-        .addExtension(Extension.authorityKeyIdentifier, false, authorityKeyId)
-        .addExtension(Extension.basicConstraints, true, constraints);
-
-    return new JcaX509CertificateConverter()
-        .setProvider(new BouncyCastleProvider())
-        .getCertificate(certificateBuilder.build(contentSigner));
-  }
-
-  private SubjectKeyIdentifier subjectKeyIdOf(KeyPair keys) throws Exception {
-    return extensionUtil().createSubjectKeyIdentifier(pubKeyInfo(keys));
-  }
-
-  private AuthorityKeyIdentifier authorityKeyIdOf(KeyPair keys)
-      throws Exception {
-    return extensionUtil().createAuthorityKeyIdentifier(pubKeyInfo(keys));
-  }
-
-  private SubjectPublicKeyInfo pubKeyInfo(KeyPair keys) {
-    return SubjectPublicKeyInfo.getInstance(keys.getPublic().getEncoded());
-  }
-
-  private X509ExtensionUtils extensionUtil()
-      throws OperatorCreationException {
-    DigestCalculator digest =
-        new BcDigestCalculatorProvider()
-            .get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
-
-    return new X509ExtensionUtils(digest);
-  }
-
 }
diff --git 
a/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/root-ca-rotation.yaml 
b/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/root-ca-rotation.yaml
index b857854355..0ca8d10c24 100644
--- a/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/root-ca-rotation.yaml
+++ b/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/root-ca-rotation.yaml
@@ -25,6 +25,7 @@ x-root-cert-rotation-config:
     - OZONE-SITE.XML_hdds.x509.renew.grace.duration=PT45S
     - OZONE-SITE.XML_hdds.x509.ca.rotation.check.interval=PT1S
     - OZONE-SITE.XML_hdds.x509.ca.rotation.ack.timeout=PT20S
+    - OZONE-SITE.XML_hdds.x509.rootca.certificate.polling.interval=PT10s
     - OZONE-SITE.XML_hdds.block.token.expiry.time=15s
     - OZONE-SITE.XML_ozone.manager.delegation.token.max-lifetime=15s
     - OZONE-SITE.XML_ozone.manager.delegation.token.renew-interval=15s
diff --git 
a/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/test-root-ca-rotation.sh 
b/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/test-root-ca-rotation.sh
index a823719cea..2a851b2cb7 100755
--- a/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/test-root-ca-rotation.sh
+++ b/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/test-root-ca-rotation.sh
@@ -45,6 +45,7 @@ wait_for_execute_command scm1.org 240 "ozone admin cert info 
2"
 # transfer leader to scm2.org
 execute_robot_test scm1.org scmha/scm-leader-transfer.robot
 wait_for_execute_command scm1.org 30 "jps | grep 
StorageContainerManagerStarter | sed 's/StorageContainerManagerStarter//' | 
xargs | xargs -I {} jstack {} | grep 'RootCARotationManager-Inactive'"
+execute_robot_test scm1.org -v PREFIX:"rootca" 
certrotation/root-ca-rotation-client-checks.robot
 
 # verify om operations
 execute_commands_in_container scm1.org "ozone sh volume create /r-v1 && ozone 
sh bucket create /r-v1/r-b1"
@@ -65,6 +66,11 @@ wait_for_execute_command scm4.org 30 "ozone admin cert list 
--role=scm | grep sc
 # wait for next root CA rotation
 wait_for_execute_command scm4.org 240 "ozone admin cert info 4"
 
+wait_for_execute_command om1 30 "find /data/metadata/om/certs/ROOTCA-4.crt"
+wait_for_execute_command om2 30 "find /data/metadata/om/certs/ROOTCA-4.crt"
+wait_for_execute_command om3 30 "find /data/metadata/om/certs/ROOTCA-4.crt"
+execute_robot_test scm4.org -v PREFIX:"rootca2" 
certrotation/root-ca-rotation-client-checks.robot
+
 #transfer leader to scm4.org
 execute_robot_test scm4.org -v "TARGET_SCM:scm4.org" 
scmha/scm-leader-transfer.robot
 
diff --git 
a/hadoop-ozone/dist/src/main/compose/ozonesecure/test-root-ca-rotation.sh 
b/hadoop-ozone/dist/src/main/compose/ozonesecure/test-root-ca-rotation.sh
index 9858d41eb8..2f28c87367 100755
--- a/hadoop-ozone/dist/src/main/compose/ozonesecure/test-root-ca-rotation.sh
+++ b/hadoop-ozone/dist/src/main/compose/ozonesecure/test-root-ca-rotation.sh
@@ -41,13 +41,20 @@ wait_for_execute_command scm 30 "jps | grep 
StorageContainerManagerStarter |  se
 # wait and verify root CA is rotated
 wait_for_execute_command scm 180 "ozone admin cert info 2"
 wait_for_execute_command datanode 30 "find 
/data/metadata/dn/certs/ROOTCA-2.crt"
+# We need to wait here for the new certificate in OM as well, because it might
+# get to the OM later, and the client will not trust the DataNode with the new
+# certificate and will not refetch the CA certs as that will be implemented in
+# HDDS-8958.
+wait_for_execute_command om 30 "find /data/metadata/om/certs/ROOTCA-2.crt"
+execute_robot_test scm -v PREFIX:"rootca" 
certrotation/root-ca-rotation-client-checks.robot
 
 # verify om operations and data operations
 execute_commands_in_container scm "ozone sh volume create /r-v1 && ozone sh 
bucket create /r-v1/r-b1"
 
 # wait for second root CA rotation
 wait_for_execute_command scm 180 "ozone admin cert info 3"
-
+wait_for_execute_command om 30 "find /data/metadata/om/certs/ROOTCA-3.crt"
+execute_robot_test scm -v PREFIX:"rootca2" 
certrotation/root-ca-rotation-client-checks.robot
 # check the metrics
 execute_robot_test scm scmha/root-ca-rotation.robot
 
diff --git 
a/hadoop-ozone/dist/src/main/smoketest/certrotation/root-ca-rotation-client-checks.robot
 
b/hadoop-ozone/dist/src/main/smoketest/certrotation/root-ca-rotation-client-checks.robot
new file mode 100644
index 0000000000..8529b338e0
--- /dev/null
+++ 
b/hadoop-ozone/dist/src/main/smoketest/certrotation/root-ca-rotation-client-checks.robot
@@ -0,0 +1,46 @@
+# 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.
+
+*** Settings ***
+Documentation       Generate data
+Library             OperatingSystem
+Library             BuiltIn
+Resource            ../commonlib.robot
+Test Timeout        5 minutes
+
+*** Variables ***
+${PREFIX}           rootca
+
+*** Test Cases ***
+Create a volume and bucket
+    [Tags]    create-volume-and-bucket
+    ${output} =         Execute          ozone sh volume create 
${PREFIX}-volume
+                        Should not contain  ${output}       Failed
+    ${output} =         Execute          ozone sh bucket create 
/${PREFIX}-volume/${PREFIX}-bucket
+                        Should not contain  ${output}       Failed
+
+Create key
+                        Execute and checkrc    echo "${PREFIX}: key created 
using Ozone Shell" > /tmp/sourcekey    0
+    ${output} =         Execute          ozone sh key put 
/${PREFIX}-volume/${PREFIX}-bucket/${PREFIX}-key /tmp/sourcekey
+                        Should not contain  ${output}       Failed
+                        Execute and checkrc    rm /tmp/sourcekey    0
+
+Read data from previously created key
+    ${random} =         Generate Random String  5  [NUMBERS]
+    ${output} =         Execute          ozone sh key get 
/${PREFIX}-volume/${PREFIX}-bucket/${PREFIX}-key /tmp/key-${random}
+                        Should not contain  ${output}       Failed
+    ${output} =         Execute and checkrc    cat /tmp/key-${random}    0
+                        Should contain    ${output}    ${PREFIX}: key created 
using Ozone Shell
+                        Execute and checkrc    rm /tmp/key-${random}    0
\ No newline at end of file
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
index 100f4400bc..129ae506a9 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
@@ -923,6 +923,9 @@ public final class TestSecureOzoneCluster {
     omStorage.forceInitialize();
     CertificateCodec certCodec = new CertificateCodec(securityConfig, "om");
     certCodec.writeCertificate(certHolder);
+    String caCertFileName = CAType.ROOT.getFileNamePrefix()
+        + certHolder.getSerialNumber().toString() + ".crt";
+    certCodec.writeCertificate(certHolder, caCertFileName);
 
     // first renewed cert
     X509CertificateHolder newCertHolder =
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 104496257e..969a3bed89 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -110,7 +110,6 @@ import 
org.apache.hadoop.hdds.security.symmetric.SecretKeySignerClient;
 import org.apache.hadoop.hdds.security.symmetric.DefaultSecretKeySignerClient;
 import org.apache.hadoop.hdds.security.token.OzoneBlockTokenSecretManager;
 import 
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
-import org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateCodec;
 import 
org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateSignRequest;
 import org.apache.hadoop.hdds.server.ServiceRuntimeInfoImpl;
 import org.apache.hadoop.hdds.server.http.RatisDropwizardExports;
@@ -346,14 +345,13 @@ public final class OzoneManager extends 
ServiceRuntimeInfoImpl
   private OzoneBlockTokenSecretManager blockTokenMgr;
   private CertificateClient certClient;
   private SecretKeySignerClient secretKeyClient;
-  private String caCertPem = null;
-  private List<String> caCertPemList = new ArrayList<>();
   private final Text omRpcAddressTxt;
   private OzoneConfiguration configuration;
   private RPC.Server omRpcServer;
   private GrpcOzoneManagerServer omS3gGrpcServer;
   private final InetSocketAddress omRpcAddress;
   private final String omId;
+  private ServiceInfoProvider serviceInfo;
 
   private OMMetadataManager metadataManager;
   private OMMultiTenantManager multiTenantManager;
@@ -642,6 +640,9 @@ public final class OzoneManager extends 
ServiceRuntimeInfoImpl
           HddsServerUtil.getSecretKeyClientForOm(conf);
       secretKeyClient = new DefaultSecretKeySignerClient(secretKeyProtocol);
     }
+    serviceInfo = new ServiceInfoProvider(secConfig, this, certClient,
+        testSecureOmFlag);
+
     if (secConfig.isBlockTokenEnabled()) {
       blockTokenMgr = createBlockTokenSecretManager();
     }
@@ -1114,6 +1115,7 @@ public final class OzoneManager extends 
ServiceRuntimeInfoImpl
       certClient.close();
     }
     certClient = newClient;
+    serviceInfo = new ServiceInfoProvider(secConfig, this, certClient);
   }
 
   /**
@@ -1636,13 +1638,6 @@ public final class OzoneManager extends 
ServiceRuntimeInfoImpl
           versionManager.getMetadataLayoutVersion(), layoutVersionInDB);
     }
 
-    // Perform this to make it work with old clients.
-    if (certClient != null) {
-      caCertPem =
-          CertificateCodec.getPEMEncodedString(certClient.getCACertificate());
-      caCertPemList = HAUtils.buildCAList(certClient, configuration);
-    }
-
     // Set metrics and start metrics back ground thread
     metrics.setNumVolumes(metadataManager.countRowsInTable(metadataManager
         .getVolumeTable()));
@@ -3091,7 +3086,7 @@ public final class OzoneManager extends 
ServiceRuntimeInfoImpl
 
   @Override
   public ServiceInfoEx getServiceInfo() throws IOException {
-    return new ServiceInfoEx(getServiceList(), caCertPem, caCertPemList);
+    return serviceInfo.provide();
   }
 
   @Override
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ServiceInfoProvider.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ServiceInfoProvider.java
new file mode 100644
index 0000000000..d2f8135332
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ServiceInfoProvider.java
@@ -0,0 +1,164 @@
+/*
+ * 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.ozone.om;
+
+import org.apache.hadoop.hdds.security.SecurityConfig;
+import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
+import 
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
+import org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateCodec;
+import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.emptyList;
+import static java.util.Comparator.comparing;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * A helper class for Ozone Manager, to take on the responsibility of caching
+ * the actual CA certificates in PEM format, and handle the update of these
+ * cached values, so that OM can provide this information to clients that needs
+ * to trust the DataNode certificates in a secure environment.
+ */
+final class ServiceInfoProvider {
+
+  private static final Logger LOG = getLogger(ServiceInfoProvider.class);
+
+  private final OzoneManagerProtocol om;
+  private final CertificateClient certClient;
+
+  private String caCertPEM;
+  private List<String> caCertPEMList;
+
+  /**
+   * Initializes the provider.
+   * The OzoneManagerProtocol implementation is used to provide the service
+   * list part of the ServiceInfoEx object this class provides for OM, while
+   * the SecurityConfig and the CertificateClient is used to provide security
+   * and trust related information within the same object.
+   *
+   * @param config the current security configuration
+   * @param om the OzoneManagerProtocol provides the service list
+   * @param certClient the CertificateClient provides certificate information
+   */
+  ServiceInfoProvider(SecurityConfig config, OzoneManagerProtocol om,
+      CertificateClient certClient) {
+    this(config, om, certClient, false);
+  }
+
+  /**
+   * Initializes the provider.
+   * The OzoneManagerProtocol implementation is used to provide the service
+   * list part of the ServiceInfoEx object this class provides for OM, while
+   * the SecurityConfig and the CertificateClient is used to provide security
+   * and trust related information within the same object.
+   *
+   * In some cases OM is initializing this class before a properly set up
+   * CertificateClient is given to the OM for tests. In this case the
+   * initialization code would fail, so this is handled in OM when a new
+   * CertificateClient is set the provider is re-created, but for this to work,
+   * the initial initialization is disabled when we are in a test process.
+   *
+   * @param config the current security configuration
+   * @param om the OzoneManagerProtocol provides the service list
+   * @param certClient the CertificateClient provides certificate information
+   * @param skipInitializationForTesting if we are testing OM in secure env 
this
+   *                                     might need to be true
+   */
+  ServiceInfoProvider(SecurityConfig config, OzoneManagerProtocol om,
+      CertificateClient certClient, boolean skipInitializationForTesting) {
+    this.om = om;
+    if (config.isSecurityEnabled() && !skipInitializationForTesting) {
+      this.certClient = certClient;
+      Set<X509Certificate> certs = getCACertificates();
+      caCertPEM = toPEMEncodedString(newestOf(certs));
+      caCertPEMList = toPEMEncodedStrings(certs);
+      this.certClient.registerRootCARotationListener(onRootCAChange());
+    } else {
+      this.certClient = null;
+      caCertPEM = null;
+      caCertPEMList = emptyList();
+    }
+  }
+
+  private Function<List<X509Certificate>, CompletableFuture<Void>>
+      onRootCAChange() {
+    return certs -> {
+      CompletableFuture<Void> returnedFuture = new CompletableFuture<>();
+      try {
+        synchronized (this) {
+          caCertPEM = toPEMEncodedString(newestOf(certs));
+          caCertPEMList = toPEMEncodedStrings(certs);
+        }
+        returnedFuture.complete(null);
+      } catch (Exception e) {
+        LOG.error("Unable to refresh cached PEM formatted CA certificates.", 
e);
+        returnedFuture.completeExceptionally(e);
+      }
+      return returnedFuture;
+    };
+  }
+
+  public ServiceInfoEx provide() throws IOException {
+    String returnedCaCertPEM;
+    List<String> returnedCaCertPEMList;
+    synchronized (this) {
+      returnedCaCertPEM = caCertPEM;
+      returnedCaCertPEMList = new ArrayList<>(caCertPEMList);
+    }
+    return new ServiceInfoEx(
+        om.getServiceList(), returnedCaCertPEM, returnedCaCertPEMList);
+  }
+
+  private Set<X509Certificate> getCACertificates() {
+    Set<X509Certificate> rootCerts = certClient.getAllRootCaCerts();
+    return !rootCerts.isEmpty() ? rootCerts : certClient.getAllCaCerts();
+  }
+
+  private X509Certificate newestOf(Collection<X509Certificate> certs) {
+    return certs.stream()
+        .max(comparing(X509Certificate::getNotAfter))
+        .orElse(null);
+  }
+
+  private String toPEMEncodedString(X509Certificate cert) {
+    try {
+      return cert == null ? null : CertificateCodec.getPEMEncodedString(cert);
+    } catch (SCMSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private List<String> toPEMEncodedStrings(Collection<X509Certificate> certs) {
+    return certs.stream()
+        .map(this::toPEMEncodedString)
+        .collect(Collectors.toList());
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestServiceInfoProvider.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestServiceInfoProvider.java
new file mode 100644
index 0000000000..2fa69aff15
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestServiceInfoProvider.java
@@ -0,0 +1,163 @@
+/*
+ * 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.ozone.om;
+
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.security.SecurityConfig;
+import 
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
+import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+import static java.util.Collections.emptyList;
+import static 
org.apache.hadoop.hdds.security.x509.CertificateTestUtils.aKeyPair;
+import static 
org.apache.hadoop.hdds.security.x509.CertificateTestUtils.createSelfSignedCert;
+import static 
org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateCodec.getPEMEncodedString;
+import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SECURITY_ENABLED_KEY;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.empty;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests to check functionality of how we provide the ServiceInfoEx object
+ * from OM to clients.
+ */
+public class TestServiceInfoProvider {
+
+  private OzoneConfiguration conf;
+  private OzoneManagerProtocol om;
+
+  @BeforeEach
+  public void setup() throws Exception {
+    conf = new OzoneConfiguration();
+
+    om = mock(OzoneManagerProtocol.class);
+    when(om.getServiceList()).thenReturn(emptyList());
+  }
+
+  /**
+   * Tests for unsecure environment.
+   */
+  @Nested
+  public class UnsecureEnvironment {
+
+    private ServiceInfoProvider provider;
+
+    @BeforeEach
+    public void setup() throws Exception {
+      conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, false);
+      provider = new ServiceInfoProvider(new SecurityConfig(conf), om, null);
+    }
+
+    @Test
+    public void test() throws Exception {
+      ServiceInfoEx info = provider.provide();
+
+      assertThat(info.getServiceInfoList(), sameInstance(emptyList()));
+      assertThat(info.getCaCertificate(), is(nullValue()));
+      assertThat(info.getCaCertPemList(), is(empty()));
+    }
+  }
+
+  /**
+   * Tests for secure environment.
+   */
+  @Nested
+  public class TestSecureEnvironment {
+
+    private CertificateClient certClient;
+    private X509Certificate cert1;
+    private String pem1;
+    private X509Certificate cert2;
+    private String pem2;
+    private ServiceInfoProvider provider;
+
+    @BeforeEach
+    public void setup() throws Exception {
+      conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
+      certClient = mock(CertificateClient.class);
+      cert1 = createSelfSignedCert(aKeyPair(conf), "1st", Duration.ofDays(1));
+      pem1 = getPEMEncodedString(cert1);
+      cert2 = createSelfSignedCert(aKeyPair(conf), "2nd", Duration.ofDays(2));
+      pem2 = getPEMEncodedString(cert2);
+      when(certClient.getAllRootCaCerts())
+          .thenReturn(new HashSet<>(Arrays.asList(cert1, cert2)));
+      provider =
+          new ServiceInfoProvider(new SecurityConfig(conf), om, certClient);
+    }
+
+    @Test
+    public void withoutRootCARenew() throws Exception {
+      ServiceInfoEx info = provider.provide();
+
+      assertThat(info.getServiceInfoList(), sameInstance(emptyList()));
+      assertThat(info.getCaCertificate(), is(equalTo(pem2)));
+      assertThat(info.getCaCertPemList(), containsInAnyOrder(pem1, pem2));
+
+      info = provider.provide();
+
+      assertThat(info.getServiceInfoList(), sameInstance(emptyList()));
+      assertThat(info.getCaCertificate(), is(equalTo(pem2)));
+      assertThat(info.getCaCertPemList(), containsInAnyOrder(pem1, pem2));
+    }
+
+    @Test
+    public void withRootCARenew() throws Exception {
+      ServiceInfoEx info = provider.provide();
+
+      assertThat(info.getServiceInfoList(), sameInstance(emptyList()));
+      assertThat(info.getCaCertificate(), is(equalTo(pem2)));
+      assertThat(info.getCaCertPemList(), containsInAnyOrder(pem1, pem2));
+
+      X509Certificate cert3 =
+          createSelfSignedCert(aKeyPair(conf), "cn", Duration.ofDays(3));
+      String pem3 = getPEMEncodedString(cert3);
+      List<X509Certificate> certs = Arrays.asList(cert2, cert3);
+      ArgumentCaptor<Function<List<X509Certificate>, CompletableFuture<Void>>>
+          captor = ArgumentCaptor.forClass(Function.class);
+      verify(certClient).registerRootCARotationListener(captor.capture());
+      captor.getValue().apply(certs).join();
+
+      info = provider.provide();
+
+      assertThat(info.getServiceInfoList(), sameInstance(emptyList()));
+      assertThat(info.getCaCertificate(), is(equalTo(pem3)));
+      assertThat(info.getCaCertPemList(), containsInAnyOrder(pem2, pem3));
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to