| /* |
| * 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.scm.server; |
| |
| import java.io.IOException; |
| import java.lang.reflect.Proxy; |
| import java.math.BigInteger; |
| import java.security.cert.CRLException; |
| import java.security.cert.X509CRL; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| |
| import java.util.List; |
| import java.util.Date; |
| import java.util.Optional; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| import com.google.common.base.Preconditions; |
| import org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeType; |
| import org.apache.hadoop.hdds.protocol.proto.SCMRatisProtocol; |
| import org.apache.hadoop.hdds.scm.ha.SCMHAInvocationHandler; |
| import org.apache.hadoop.hdds.scm.ha.SCMRatisServer; |
| import org.apache.hadoop.hdds.scm.metadata.SCMMetadataStore; |
| import org.apache.hadoop.hdds.security.exception.SCMSecurityException; |
| import org.apache.hadoop.hdds.security.x509.certificate.authority.CRLApprover; |
| import org.apache.hadoop.hdds.security.x509.certificate.authority.CertificateStore; |
| import org.apache.hadoop.hdds.security.x509.crl.CRLInfo; |
| import org.apache.hadoop.hdds.utils.db.BatchOperation; |
| import org.apache.hadoop.hdds.utils.db.Table; |
| import org.bouncycastle.asn1.x509.CRLReason; |
| import org.bouncycastle.cert.X509CertificateHolder; |
| import org.bouncycastle.cert.X509v2CRLBuilder; |
| import org.bouncycastle.operator.OperatorCreationException; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static org.apache.hadoop.ozone.OzoneConsts.CRL_SEQUENCE_ID_KEY; |
| import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeType.SCM; |
| import static org.apache.hadoop.hdds.security.x509.certificate.authority.CertificateStore.CertType.VALID_CERTS; |
| |
| /** |
| * A Certificate Store class that persists certificates issued by SCM CA. |
| */ |
| public final class SCMCertStore implements CertificateStore { |
| private static final Logger LOG = |
| LoggerFactory.getLogger(SCMCertStore.class); |
| private SCMMetadataStore scmMetadataStore; |
| private final Lock lock; |
| private AtomicLong crlSequenceId; |
| |
| private SCMCertStore(SCMMetadataStore dbStore, long sequenceId) { |
| this.scmMetadataStore = dbStore; |
| lock = new ReentrantLock(); |
| crlSequenceId = new AtomicLong(sequenceId); |
| } |
| |
| @Override |
| public void storeValidCertificate(BigInteger serialID, |
| X509Certificate certificate, NodeType role) |
| throws IOException { |
| lock.lock(); |
| try { |
| // This makes sure that no certificate IDs are reusable. |
| if (role == SCM) { |
| // If the role is SCM, store certificate in scm cert table |
| // and valid cert table. This is to help to return scm certs during |
| // getCertificate call. |
| storeValidScmCertificate(serialID, certificate); |
| } else { |
| // As we don't have different table for other roles, other role |
| // certificates will go to validCertsTable. |
| scmMetadataStore.getValidCertsTable().put(serialID, certificate); |
| } |
| } finally { |
| lock.unlock(); |
| } |
| } |
| |
| /** |
| * Writes a new SCM certificate that was issued to the persistent store. |
| * @param serialID - Certificate Serial Number. |
| * @param certificate - Certificate to persist. |
| * @throws IOException - on Failure. |
| */ |
| private void storeValidScmCertificate(BigInteger serialID, |
| X509Certificate certificate) throws IOException { |
| lock.lock(); |
| try { |
| BatchOperation batchOperation = |
| scmMetadataStore.getBatchHandler().initBatchOperation(); |
| scmMetadataStore.getValidSCMCertsTable().putWithBatch(batchOperation, |
| serialID, certificate); |
| scmMetadataStore.getValidCertsTable().putWithBatch(batchOperation, |
| serialID, certificate); |
| scmMetadataStore.getStore().commitBatchOperation(batchOperation); |
| } finally { |
| lock.unlock(); |
| } |
| } |
| |
| public void checkValidCertID(BigInteger serialID) throws IOException { |
| lock.lock(); |
| try { |
| if ((getCertificateByID(serialID, VALID_CERTS) != null) || |
| (getCertificateByID(serialID, CertType.REVOKED_CERTS) != null)) { |
| throw new SCMSecurityException("Conflicting certificate ID" + serialID); |
| } |
| } finally { |
| lock.unlock(); |
| } |
| } |
| |
| @Override |
| public Optional<Long> revokeCertificates( |
| List<BigInteger> serialIDs, |
| X509CertificateHolder caCertificateHolder, |
| CRLReason reason, |
| Date revocationTime, |
| CRLApprover crlApprover) |
| throws IOException { |
| Date now = new Date(); |
| X509v2CRLBuilder builder = |
| new X509v2CRLBuilder(caCertificateHolder.getIssuer(), now); |
| List<X509Certificate> certsToRevoke = new ArrayList<>(); |
| X509CRL crl; |
| Optional<Long> sequenceId = Optional.empty(); |
| lock.lock(); |
| try { |
| for (BigInteger serialID: serialIDs) { |
| X509Certificate cert = |
| getCertificateByID(serialID, CertType.VALID_CERTS); |
| if (cert == null && LOG.isWarnEnabled()) { |
| LOG.warn("Trying to revoke a certificate that is not valid. " + |
| "Serial ID: {}", serialID.toString()); |
| } else if (getCertificateByID(serialID, CertType.REVOKED_CERTS) |
| != null) { |
| LOG.warn("Trying to revoke a certificate that is already revoked."); |
| } else { |
| builder.addCRLEntry(serialID, revocationTime, |
| reason.getValue().intValue()); |
| certsToRevoke.add(cert); |
| } |
| } |
| if (!certsToRevoke.isEmpty()) { |
| try { |
| crl = crlApprover.sign(builder); |
| } catch (OperatorCreationException | CRLException e) { |
| throw new SCMSecurityException("Unable to create Certificate " + |
| "Revocation List.", e); |
| } |
| // let us do this in a transaction. |
| try (BatchOperation batch = |
| scmMetadataStore.getStore().initBatchOperation()) { |
| // Move the certificates from Valid Certs table to Revoked Certs Table |
| // only if the revocation time has passed. |
| if (now.after(revocationTime) || now.equals(revocationTime)) { |
| for (X509Certificate cert : certsToRevoke) { |
| scmMetadataStore.getRevokedCertsTable() |
| .putWithBatch(batch, cert.getSerialNumber(), cert); |
| scmMetadataStore.getValidCertsTable() |
| .deleteWithBatch(batch, cert.getSerialNumber()); |
| } |
| } |
| long id = crlSequenceId.incrementAndGet(); |
| CRLInfo crlInfo = new CRLInfo.Builder() |
| .setX509CRL(crl) |
| .setCreationTimestamp(now.getTime()) |
| .build(); |
| scmMetadataStore.getCRLInfoTable().putWithBatch( |
| batch, id, crlInfo); |
| |
| // Update the CRL Sequence Id Table with the last sequence id. |
| scmMetadataStore.getCRLSequenceIdTable().putWithBatch(batch, |
| CRL_SEQUENCE_ID_KEY, id); |
| scmMetadataStore.getStore().commitBatchOperation(batch); |
| sequenceId = Optional.of(id); |
| } |
| } |
| } finally { |
| lock.unlock(); |
| } |
| return sequenceId; |
| } |
| |
| @Override |
| public void removeExpiredCertificate(BigInteger serialID) |
| throws IOException { |
| // TODO: Later this allows removal of expired certificates from the system. |
| } |
| |
| @Override |
| public X509Certificate getCertificateByID(BigInteger serialID, |
| CertType certType) |
| throws IOException { |
| if (certType == VALID_CERTS) { |
| return scmMetadataStore.getValidCertsTable().get(serialID); |
| } else { |
| return scmMetadataStore.getRevokedCertsTable().get(serialID); |
| } |
| } |
| |
| @Override |
| public List<X509Certificate> listCertificate(NodeType role, |
| BigInteger startSerialID, int count, CertType certType) |
| throws IOException { |
| |
| Preconditions.checkNotNull(startSerialID); |
| |
| if (startSerialID.longValue() == 0) { |
| startSerialID = null; |
| } |
| |
| List<? extends Table.KeyValue<BigInteger, X509Certificate>> certs = |
| getCertTableList(role, certType, startSerialID, count); |
| |
| List<X509Certificate> results = new ArrayList<>(certs.size()); |
| |
| for (Table.KeyValue<BigInteger, X509Certificate> kv : certs) { |
| try { |
| X509Certificate cert = kv.getValue(); |
| results.add(cert); |
| } catch (IOException e) { |
| LOG.error("Fail to list certificate from SCM metadata store", e); |
| throw new SCMSecurityException( |
| "Fail to list certificate from SCM metadata store."); |
| } |
| } |
| return results; |
| } |
| |
| private List<? extends Table.KeyValue<BigInteger, X509Certificate>> |
| getCertTableList(NodeType role, CertType certType, |
| BigInteger startSerialID, int count) |
| throws IOException { |
| // Implemented for role SCM and CertType VALID_CERTS. |
| // TODO: Implement for role OM/Datanode and for SCM for CertType |
| // REVOKED_CERTS. |
| |
| if (role == SCM) { |
| if (certType == VALID_CERTS) { |
| return scmMetadataStore.getValidSCMCertsTable().getRangeKVs( |
| startSerialID, count); |
| } else { |
| return scmMetadataStore.getRevokedCertsTable().getRangeKVs( |
| startSerialID, count); |
| } |
| } else { |
| if (certType == VALID_CERTS) { |
| return scmMetadataStore.getValidCertsTable().getRangeKVs( |
| startSerialID, count); |
| } else { |
| return scmMetadataStore.getRevokedCertsTable().getRangeKVs( |
| startSerialID, count); |
| } |
| } |
| } |
| |
| /** |
| * Reinitialise the underlying store with SCMMetaStore |
| * during SCM StateMachine reload. |
| * @param metadataStore |
| */ |
| @Override |
| public void reinitialize(SCMMetadataStore metadataStore) { |
| this.scmMetadataStore = metadataStore; |
| } |
| |
| public static class Builder { |
| |
| private SCMMetadataStore metadataStore; |
| private long crlSequenceId; |
| private SCMRatisServer scmRatisServer; |
| |
| |
| public Builder setMetadaStore(SCMMetadataStore scmMetadataStore) { |
| this.metadataStore = scmMetadataStore; |
| return this; |
| } |
| |
| public Builder setCRLSequenceId(long sequenceId) { |
| this.crlSequenceId = sequenceId; |
| return this; |
| } |
| |
| public Builder setRatisServer(final SCMRatisServer ratisServer) { |
| scmRatisServer = ratisServer; |
| return this; |
| } |
| |
| public CertificateStore build() { |
| final SCMCertStore scmCertStore = new SCMCertStore(metadataStore, |
| crlSequenceId); |
| |
| final SCMHAInvocationHandler scmhaInvocationHandler = |
| new SCMHAInvocationHandler(SCMRatisProtocol.RequestType.CERT_STORE, |
| scmCertStore, scmRatisServer); |
| |
| return (CertificateStore) Proxy.newProxyInstance( |
| SCMHAInvocationHandler.class.getClassLoader(), |
| new Class<?>[]{CertificateStore.class}, scmhaInvocationHandler); |
| |
| } |
| } |
| } |