|
@@ -0,0 +1,373 @@
|
|
|
+/*
|
|
|
+ * 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.certificate.authority;
|
|
|
+
|
|
|
+import com.google.common.annotations.VisibleForTesting;
|
|
|
+import com.google.common.base.Preconditions;
|
|
|
+import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
|
|
|
+import org.apache.hadoop.hdds.security.x509.SecurityConfig;
|
|
|
+import org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateCodec;
|
|
|
+import org.apache.hadoop.hdds.security.x509.certificates.utils.CertificateSignRequest;
|
|
|
+import org.apache.hadoop.hdds.security.x509.certificates.utils.SelfSignedCertificate;
|
|
|
+import org.apache.hadoop.hdds.security.x509.keys.HDDSKeyGenerator;
|
|
|
+import org.apache.hadoop.hdds.security.x509.keys.KeyCodec;
|
|
|
+import org.bouncycastle.cert.X509CertificateHolder;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+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.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.LocalTime;
|
|
|
+import java.util.concurrent.Future;
|
|
|
+import java.util.function.Consumer;
|
|
|
+
|
|
|
+/**
|
|
|
+ * The default CertificateServer used by SCM. This has no dependencies on any
|
|
|
+ * external system, this allows us to bootstrap a CertificateServer from
|
|
|
+ * Scratch.
|
|
|
+ * <p>
|
|
|
+ * Details =======
|
|
|
+ * <p>
|
|
|
+ * The Default CA server is one of the many possible implementations of an SCM
|
|
|
+ * Certificate Authority.
|
|
|
+ * <p>
|
|
|
+ * A certificate authority needs the Root Certificates and its private key to
|
|
|
+ * operate. The init function of the DefaultCA Server detects four possible
|
|
|
+ * states the System can be in.
|
|
|
+ * <p>
|
|
|
+ * 1. Success - This means that the expected Certificates and Keys are in
|
|
|
+ * place, and the CA was able to read those files into memory.
|
|
|
+ * <p>
|
|
|
+ * 2. Missing Keys - This means that private keys are missing. This is an error
|
|
|
+ * state which SCM CA cannot recover from. The cluster might have been
|
|
|
+ * initialized earlier and for some reason, we are not able to find the private
|
|
|
+ * keys for the CA. Eventually we will have 2 ways to recover from this state,
|
|
|
+ * first one is to copy the SCM CA private keys from a backup. Second one is to
|
|
|
+ * rekey the whole cluster. Both of these are improvements we will support in
|
|
|
+ * future.
|
|
|
+ * <p>
|
|
|
+ * 3. Missing Certificate - Similar to Missing Keys, but the root certificates
|
|
|
+ * are missing.
|
|
|
+ * <p>
|
|
|
+ * 4. Initialize - We don't have keys or certificates. DefaultCA assumes that
|
|
|
+ * this is a system bootup and will generate the keys and certificates
|
|
|
+ * automatically.
|
|
|
+ * <p>
|
|
|
+ * The init() follows the following logic,
|
|
|
+ * <p>
|
|
|
+ * 1. Compute the Verification Status -- Success, Missing Keys, Missing Certs or
|
|
|
+ * Initialize.
|
|
|
+ * <p>
|
|
|
+ * 2. ProcessVerificationStatus - Returns a Lambda, based on the Verification
|
|
|
+ * Status.
|
|
|
+ * <p>
|
|
|
+ * 3. Invoke the Lambda function.
|
|
|
+ * <p>
|
|
|
+ * At the end of the init function, we have functional CA. This function can be
|
|
|
+ * invoked as many times since we will regenerate the keys and certs only if
|
|
|
+ * both of them are missing.
|
|
|
+ */
|
|
|
+public class DefaultCAServer implements CertificateServer {
|
|
|
+ private static final Logger LOG =
|
|
|
+ LoggerFactory.getLogger(DefaultCAServer.class);
|
|
|
+ private final String subject;
|
|
|
+ private final String clusterID;
|
|
|
+ private final String scmID;
|
|
|
+ private String componentName = Paths.get("scm", "ca").toString();
|
|
|
+ private Path caKeysPath;
|
|
|
+ private Path caRootX509Path;
|
|
|
+ private SecurityConfig config;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create an Instance of DefaultCAServer.
|
|
|
+ *
|
|
|
+ * @param subject - String Subject
|
|
|
+ * @param clusterID - String ClusterID
|
|
|
+ * @param scmID - String SCMID.
|
|
|
+ */
|
|
|
+ public DefaultCAServer(String subject, String clusterID, String scmID) {
|
|
|
+ this.subject = subject;
|
|
|
+ this.clusterID = clusterID;
|
|
|
+ this.scmID = scmID;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void init(SecurityConfig securityConfig, CAType type)
|
|
|
+ throws SCMSecurityException {
|
|
|
+ caKeysPath = securityConfig.getKeyLocation(componentName);
|
|
|
+ caRootX509Path = securityConfig.getCertificateLocation(componentName);
|
|
|
+ this.config = securityConfig;
|
|
|
+
|
|
|
+ /* In future we will spilt this code to have different kind of CAs.
|
|
|
+ * Right now, we have only self-signed CertificateServer.
|
|
|
+ */
|
|
|
+
|
|
|
+ if (type == CAType.SELF_SIGNED_CA) {
|
|
|
+ VerificationStatus status = verifySelfSignedCA(securityConfig);
|
|
|
+ Consumer<SecurityConfig> caInitializer =
|
|
|
+ processVerificationStatus(status);
|
|
|
+ caInitializer.accept(securityConfig);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ LOG.error("We support only Self-Signed CAs for now.");
|
|
|
+ throw new IllegalStateException("Not implemented functionality requested.");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public X509CertificateHolder getCACertificate() throws
|
|
|
+ CertificateException, IOException {
|
|
|
+ CertificateCodec certificateCodec =
|
|
|
+ new CertificateCodec(config, componentName);
|
|
|
+ return certificateCodec.readCertificate();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Future<X509CertificateHolder> requestCertificate(
|
|
|
+ CertificateSignRequest csr, CertificateApprover approver)
|
|
|
+ throws SCMSecurityException {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Future<Boolean> revokeCertificate(X509Certificate certificate,
|
|
|
+ CertificateApprover approver) throws SCMSecurityException {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Generates a Self Signed CertificateServer. These are the steps in
|
|
|
+ * generating a Self-Signed CertificateServer.
|
|
|
+ * <p>
|
|
|
+ * 1. Generate a Private/Public Key Pair. 2. Persist to a protected location.
|
|
|
+ * 3. Generate a SelfSigned Root CertificateServer certificate.
|
|
|
+ *
|
|
|
+ * @param securityConfig - Config.
|
|
|
+ */
|
|
|
+ private void generateSelfSignedCA(SecurityConfig securityConfig) throws
|
|
|
+ NoSuchAlgorithmException, NoSuchProviderException, IOException {
|
|
|
+ KeyPair keyPair = generateKeys(securityConfig);
|
|
|
+ generateRootCertificate(securityConfig, keyPair);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Verify Self-Signed CertificateServer. 1. Check if the Certificate exist. 2.
|
|
|
+ * Check if the key pair exists.
|
|
|
+ *
|
|
|
+ * @param securityConfig -- Config
|
|
|
+ * @return Verification Status
|
|
|
+ */
|
|
|
+ private VerificationStatus verifySelfSignedCA(SecurityConfig securityConfig) {
|
|
|
+ /*
|
|
|
+ The following is the truth table for the States.
|
|
|
+ True means we have that file False means it is missing.
|
|
|
+ +--------------+--------+--------+--------------+
|
|
|
+ | Certificates | Keys | Result | Function |
|
|
|
+ +--------------+--------+--------+--------------+
|
|
|
+ | True | True | True | Success |
|
|
|
+ | False | False | True | Initialize |
|
|
|
+ | True | False | False | Missing Key |
|
|
|
+ | False | True | False | Missing Cert |
|
|
|
+ +--------------+--------+--------+--------------+
|
|
|
+
|
|
|
+ This truth table maps to ~(certs xor keys) or certs == keys
|
|
|
+ */
|
|
|
+ boolean keyStatus = checkIfKeysExist();
|
|
|
+ boolean certStatus = checkIfCertificatesExist();
|
|
|
+
|
|
|
+ if ((certStatus == keyStatus) && (certStatus)) {
|
|
|
+ return VerificationStatus.SUCCESS;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((certStatus == keyStatus) && (!certStatus)) {
|
|
|
+ return VerificationStatus.INITIALIZE;
|
|
|
+ }
|
|
|
+
|
|
|
+ // At this point certStatus is not equal to keyStatus.
|
|
|
+ if (certStatus) {
|
|
|
+ return VerificationStatus.MISSING_KEYS;
|
|
|
+ }
|
|
|
+
|
|
|
+ return VerificationStatus.MISSING_CERTIFICATE;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns Keys status.
|
|
|
+ *
|
|
|
+ * @return True if the key files exist.
|
|
|
+ */
|
|
|
+ private boolean checkIfKeysExist() {
|
|
|
+ if (!Files.exists(caKeysPath)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Files.exists(Paths.get(caKeysPath.toString(),
|
|
|
+ this.config.getPrivateKeyFileName()))) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns certificate Status.
|
|
|
+ *
|
|
|
+ * @return True if the Certificate files exist.
|
|
|
+ */
|
|
|
+ private boolean checkIfCertificatesExist() {
|
|
|
+ if (!Files.exists(caRootX509Path)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!Files.exists(Paths.get(caRootX509Path.toString(),
|
|
|
+ this.config.getCertificateFileName()))) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Based on the Status of the verification, we return a lambda that gets
|
|
|
+ * executed by the init function of the CA.
|
|
|
+ *
|
|
|
+ * @param status - Verification Status.
|
|
|
+ */
|
|
|
+ @VisibleForTesting
|
|
|
+ Consumer<SecurityConfig> processVerificationStatus(
|
|
|
+ VerificationStatus status) {
|
|
|
+ Consumer<SecurityConfig> consumer = null;
|
|
|
+ switch (status) {
|
|
|
+ case SUCCESS:
|
|
|
+ consumer = (arg) -> LOG.info("CertificateServer validation is " +
|
|
|
+ "successful");
|
|
|
+ break;
|
|
|
+ case MISSING_KEYS:
|
|
|
+ consumer = (arg) -> {
|
|
|
+ LOG.error("We have found the Certificate for this CertificateServer, " +
|
|
|
+ "but keys used by this CertificateServer is missing. This is a " +
|
|
|
+ "non-recoverable error. Please restart the system after locating " +
|
|
|
+ "the Keys used by the CertificateServer.");
|
|
|
+ LOG.error("Exiting due to unrecoverable CertificateServer error.");
|
|
|
+ throw new IllegalStateException("Missing Keys, cannot continue.");
|
|
|
+ };
|
|
|
+ break;
|
|
|
+ case MISSING_CERTIFICATE:
|
|
|
+ consumer = (arg) -> {
|
|
|
+ LOG.error("We found the keys, but the root certificate for this " +
|
|
|
+ "CertificateServer is missing. Please restart SCM after locating " +
|
|
|
+ "the " +
|
|
|
+ "Certificates.");
|
|
|
+ LOG.error("Exiting due to unrecoverable CertificateServer error.");
|
|
|
+ throw new IllegalStateException("Missing Root Certs, cannot continue.");
|
|
|
+ };
|
|
|
+ break;
|
|
|
+ case INITIALIZE:
|
|
|
+ consumer = (arg) -> {
|
|
|
+ try {
|
|
|
+ generateSelfSignedCA(arg);
|
|
|
+ } catch (NoSuchProviderException | NoSuchAlgorithmException
|
|
|
+ | IOException e) {
|
|
|
+ LOG.error("Unable to initialize CertificateServer.", e);
|
|
|
+ }
|
|
|
+ VerificationStatus newStatus = verifySelfSignedCA(arg);
|
|
|
+ if (newStatus != VerificationStatus.SUCCESS) {
|
|
|
+ LOG.error("Unable to initialize CertificateServer, failed in " +
|
|
|
+ "verification.");
|
|
|
+ }
|
|
|
+ };
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ /* Make CheckStyle happy */
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return consumer;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Generates a KeyPair for the Certificate.
|
|
|
+ *
|
|
|
+ * @param securityConfig - SecurityConfig.
|
|
|
+ * @return Key Pair.
|
|
|
+ * @throws NoSuchProviderException - on Error.
|
|
|
+ * @throws NoSuchAlgorithmException - on Error.
|
|
|
+ * @throws IOException - on Error.
|
|
|
+ */
|
|
|
+ private KeyPair generateKeys(SecurityConfig securityConfig)
|
|
|
+ throws NoSuchProviderException, NoSuchAlgorithmException, IOException {
|
|
|
+ HDDSKeyGenerator keyGenerator = new HDDSKeyGenerator(securityConfig);
|
|
|
+ KeyPair keys = keyGenerator.generateKey();
|
|
|
+ KeyCodec keyPEMWriter = new KeyCodec(securityConfig,
|
|
|
+ componentName);
|
|
|
+ keyPEMWriter.writeKey(keys);
|
|
|
+ return keys;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Generates a self-signed Root Certificate for CA.
|
|
|
+ *
|
|
|
+ * @param securityConfig - SecurityConfig
|
|
|
+ * @param key - KeyPair.
|
|
|
+ * @throws IOException - on Error.
|
|
|
+ * @throws SCMSecurityException - on Error.
|
|
|
+ */
|
|
|
+ private void generateRootCertificate(SecurityConfig securityConfig,
|
|
|
+ KeyPair key) throws IOException, SCMSecurityException {
|
|
|
+ Preconditions.checkNotNull(this.config);
|
|
|
+ LocalDate beginDate = LocalDate.now().atStartOfDay().toLocalDate();
|
|
|
+ LocalDateTime temp = LocalDateTime.of(beginDate, LocalTime.MIDNIGHT);
|
|
|
+ LocalDate endDate =
|
|
|
+ temp.plus(securityConfig.getMaxCertificateDuration()).toLocalDate();
|
|
|
+ X509CertificateHolder selfSignedCertificate =
|
|
|
+ SelfSignedCertificate
|
|
|
+ .newBuilder()
|
|
|
+ .setSubject(this.subject)
|
|
|
+ .setScmID(this.scmID)
|
|
|
+ .setClusterID(this.clusterID)
|
|
|
+ .setBeginDate(beginDate)
|
|
|
+ .setEndDate(endDate)
|
|
|
+ .makeCA()
|
|
|
+ .setConfiguration(securityConfig.getConfiguration())
|
|
|
+ .setKey(key)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ CertificateCodec certCodec =
|
|
|
+ new CertificateCodec(config, componentName);
|
|
|
+ certCodec.writeCertificate(selfSignedCertificate);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This represents the verification status of the CA. Based on this enum
|
|
|
+ * appropriate action is taken in the Init.
|
|
|
+ */
|
|
|
+ @VisibleForTesting
|
|
|
+ enum VerificationStatus {
|
|
|
+ SUCCESS, /* All artifacts needed by CertificateServer is present */
|
|
|
+ MISSING_KEYS, /* Private key is missing, certificate Exists.*/
|
|
|
+ MISSING_CERTIFICATE, /* Keys exist, but root certificate missing.*/
|
|
|
+ INITIALIZE /* All artifacts are missing, we should init the system. */
|
|
|
+ }
|
|
|
+}
|