blob: 1fd6d7c9af64086c2b0c9dc9f01b4e8dd93e8cc6 [file] [log] [blame]
/*
* 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.certificates.utils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
import org.apache.hadoop.hdds.security.x509.SecurityConfig;
import org.apache.hadoop.hdds.security.x509.exceptions.CertificateException;
import org.apache.hadoop.util.Time;
import org.apache.logging.log4j.util.Strings;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
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.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.Date;
/**
* A Self Signed Certificate with CertificateServer basic constraint can be used
* to bootstrap a certificate infrastructure, if no external certificate is
* provided.
*/
public final class SelfSignedCertificate {
private static final String NAME_FORMAT = "CN=%s,OU=%s,O=%s";
private String subject;
private String clusterID;
private String scmID;
private LocalDate beginDate;
private LocalDate endDate;
private KeyPair key;
private SecurityConfig config;
/**
* Private Ctor invoked only via Builder Interface.
*
* @param subject - Subject
* @param scmID - SCM ID
* @param clusterID - Cluster ID
* @param beginDate - NotBefore
* @param endDate - Not After
* @param configuration - SCM Config
* @param keyPair - KeyPair
*/
private SelfSignedCertificate(String subject, String scmID, String clusterID,
LocalDate beginDate, LocalDate endDate, SecurityConfig configuration,
KeyPair keyPair) {
this.subject = subject;
this.clusterID = clusterID;
this.scmID = scmID;
this.beginDate = beginDate;
this.endDate = endDate;
config = configuration;
this.key = keyPair;
}
@VisibleForTesting
public static String getNameFormat() {
return NAME_FORMAT;
}
public static Builder newBuilder() {
return new Builder();
}
private X509CertificateHolder generateCertificate(boolean isCA)
throws OperatorCreationException, IOException {
// For the Root Certificate we form the name from Subject, SCM ID and
// Cluster ID.
String dnName = String.format(getNameFormat(), subject, scmID, clusterID);
X500Name name = new X500Name(dnName);
byte[] encoded = key.getPublic().getEncoded();
SubjectPublicKeyInfo publicKeyInfo =
SubjectPublicKeyInfo.getInstance(encoded);
ContentSigner contentSigner =
new JcaContentSignerBuilder(config.getSignatureAlgo())
.setProvider(config.getProvider()).build(key.getPrivate());
// Please note: Since this is a root certificate we use "ONE" as the
// serial number. Also note that skip enforcing locale or UTC. We are
// trying to operate at the Days level, hence Time zone is also skipped for
// now.
BigInteger serial = BigInteger.ONE;
if (!isCA) {
serial = new BigInteger(Long.toString(Time.monotonicNow()));
}
ZoneOffset zoneOffset =
beginDate.atStartOfDay(ZoneOffset.systemDefault()).getOffset();
// Valid from the Start of the day when we generate this Certificate.
Date validFrom =
Date.from(beginDate.atTime(LocalTime.MIN).toInstant(zoneOffset));
// Valid till end day finishes.
Date validTill =
Date.from(endDate.atTime(LocalTime.MAX).toInstant(zoneOffset));
X509v3CertificateBuilder builder = new X509v3CertificateBuilder(name,
serial, validFrom, validTill, name, publicKeyInfo);
if (isCA) {
builder.addExtension(Extension.basicConstraints, true,
new BasicConstraints(true));
int keyUsageFlag = KeyUsage.keyCertSign | KeyUsage.cRLSign;
KeyUsage keyUsage = new KeyUsage(keyUsageFlag);
builder.addExtension(Extension.keyUsage, false,
new DEROctetString(keyUsage));
}
return builder.build(contentSigner);
}
/**
* Builder class for Root Certificates.
*/
public static class Builder {
private String subject;
private String clusterID;
private String scmID;
private LocalDate beginDate;
private LocalDate endDate;
private KeyPair key;
private SecurityConfig config;
private boolean isCA;
public Builder setConfiguration(Configuration configuration) {
this.config = new SecurityConfig(configuration);
return this;
}
public Builder setKey(KeyPair keyPair) {
this.key = keyPair;
return this;
}
public Builder setSubject(String subjectString) {
this.subject = subjectString;
return this;
}
public Builder setClusterID(String s) {
this.clusterID = s;
return this;
}
public Builder setScmID(String s) {
this.scmID = s;
return this;
}
public Builder setBeginDate(LocalDate date) {
this.beginDate = date;
return this;
}
public Builder setEndDate(LocalDate date) {
this.endDate = date;
return this;
}
public Builder makeCA() {
isCA = true;
return this;
}
public X509CertificateHolder build()
throws SCMSecurityException, IOException {
Preconditions.checkNotNull(key, "Key cannot be null");
Preconditions.checkArgument(Strings.isNotBlank(subject), "Subject " +
"cannot be blank");
Preconditions.checkArgument(Strings.isNotBlank(clusterID), "Cluster ID " +
"cannot be blank");
Preconditions.checkArgument(Strings.isNotBlank(scmID), "SCM ID cannot " +
"be blank");
Preconditions.checkArgument(beginDate.isBefore(endDate), "Certificate " +
"begin date should be before end date");
// We just read the beginDate and EndDate as Start of the Day and
// confirm that we do not violate the maxDuration Config.
Duration certDuration = Duration.between(beginDate.atStartOfDay(),
endDate.atStartOfDay());
Duration maxDuration = config.getMaxCertificateDuration();
if (certDuration.compareTo(maxDuration) > 0) {
throw new SCMSecurityException("The cert duration violates the " +
"maximum configured value. Please check the hdds.x509.max" +
".duration config key. Current Value: " + certDuration +
" config: " + maxDuration);
}
SelfSignedCertificate rootCertificate =
new SelfSignedCertificate(this.subject,
this.scmID, this.clusterID, this.beginDate, this.endDate,
this.config, key);
try {
return rootCertificate.generateCertificate(isCA);
} catch (OperatorCreationException | CertIOException e) {
throw new CertificateException("Unable to create root certificate.",
e.getCause());
}
}
}
}