org.wildfly.security.x500.cert.X509CertificateBuilder Maven / Gradle / Ivy
Go to download
This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including
all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and
JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up
with different versions on classes on the class path).
/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed 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.wildfly.security.x500.cert;
import static org.wildfly.security.x500.cert._private.ElytronMessages.log;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.security.auth.x500.X500Principal;
import org.wildfly.common.Assert;
import org.wildfly.security.asn1.ASN1;
import org.wildfly.security.asn1.DEREncoder;
/**
* A builder for X.509 certificates.
*
* @author David M. Lloyd
*/
public final class X509CertificateBuilder {
private static final ZonedDateTime LATEST_VALID = ZonedDateTime.of(9999, 12, 31, 23, 59, 59, 0, ZoneOffset.UTC);
private int version = 3;
private BigInteger serialNumber = BigInteger.ONE;
private X500Principal subjectDn;
private byte[] subjectUniqueId;
private X500Principal issuerDn;
private byte[] issuerUniqueId;
private ZonedDateTime notValidBefore = ZonedDateTime.now();
private ZonedDateTime notValidAfter = LATEST_VALID;
private final Map extensionsByOid = new LinkedHashMap<>();
private PublicKey publicKey;
private PrivateKey signingKey;
private String signatureAlgorithmName;
/**
* Construct a new uninitialized instance.
*/
public X509CertificateBuilder() {
}
/**
* Add a certificate extension. If an extension with the same OID already exists, an exception is thrown.
*
* @param extension the extension to add (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder addExtension(X509CertificateExtension extension) {
Assert.checkNotNullParam("extension", extension);
final String oid = extension.getId();
Assert.checkNotNullParam("extension.getOid()", oid);
if (extensionsByOid.putIfAbsent(oid, extension) != null) {
throw log.extensionAlreadyExists(oid);
}
return this;
}
/**
* Add or replace a certificate extension. If an extension with the same OID already exists, it is replaced
* and returned.
*
* @param extension the extension to add (must not be {@code null})
* @return the existing extension, or {@code null} if no other extension with the same OID was existent
*/
public X509CertificateExtension addOrReplaceExtension(X509CertificateExtension extension) {
Assert.checkNotNullParam("extension", extension);
final String oid = extension.getId();
Assert.checkNotNullParam("extension.getOid()", oid);
return extensionsByOid.put(oid, extension);
}
/**
* Remove the extension with the given OID, if it is registered.
*
* @param oid the OID of the extension to remove
* @return the extension, or {@code null} if no extension with the same OID was existent
*/
public X509CertificateExtension removeExtension(String oid) {
Assert.checkNotNullParam("oid", oid);
return extensionsByOid.remove(oid);
}
/**
* Get the certificate version.
*
* @return the certificate version
*/
public int getVersion() {
return version;
}
/**
* Set the certificate version.
*
* @param version the certificate version (must be between 1 and 3, inclusive)
* @return this builder instance
*/
public X509CertificateBuilder setVersion(final int version) {
Assert.checkMinimumParameter("version", 1, version);
Assert.checkMaximumParameter("version", 3, version);
this.version = version;
return this;
}
/**
* Get the serial number of the certificate being built.
*
* @return the serial number of the certificate being built (must not be {@code null})
*/
public BigInteger getSerialNumber() {
return serialNumber;
}
/**
* Set the serial number of the certificate being built. The serial number must be positive and no larger
* than 20 octets (or 2^160).
*
* @param serialNumber the serial number of the certificate being built
* @return this builder instance
*/
public X509CertificateBuilder setSerialNumber(final BigInteger serialNumber) {
Assert.checkNotNullParam("serialNumber", serialNumber);
if (BigInteger.ONE.compareTo(serialNumber) > 0) {
throw log.serialNumberTooSmall();
}
if (serialNumber.bitLength() > 20*8) {
throw log.serialNumberTooLarge();
}
this.serialNumber = serialNumber;
return this;
}
/**
* Get the subject DN.
*
* @return the subject DN
*/
public X500Principal getSubjectDn() {
return subjectDn;
}
/**
* Set the subject DN.
*
* @param subjectDn the subject DN (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setSubjectDn(final X500Principal subjectDn) {
Assert.checkNotNullParam("subjectDn", subjectDn);
this.subjectDn = subjectDn;
return this;
}
/**
* Get the subject unique ID.
*
* @return the subject unique ID
*/
public byte[] getSubjectUniqueId() {
return subjectUniqueId;
}
/**
* Set the subject unique ID.
*
* @param subjectUniqueId the subject unique ID (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setSubjectUniqueId(final byte[] subjectUniqueId) {
Assert.checkNotNullParam("subjectUniqueId", subjectUniqueId);
this.subjectUniqueId = subjectUniqueId;
return this;
}
/**
* Get the issuer DN.
*
* @return the issuer DN
*/
public X500Principal getIssuerDn() {
return issuerDn;
}
/**
* Set the issuer DN.
*
* @param issuerDn the issuer DN (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setIssuerDn(final X500Principal issuerDn) {
Assert.checkNotNullParam("issuerDn", issuerDn);
this.issuerDn = issuerDn;
return this;
}
/**
* Get the issuer unique ID.
*
* @return the issuer unique ID
*/
public byte[] getIssuerUniqueId() {
return issuerUniqueId;
}
/**
* Set the issuer unique ID.
*
* @param issuerUniqueId the issuer unique ID (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setIssuerUniqueId(final byte[] issuerUniqueId) {
Assert.checkNotNullParam("issuerUniqueId", issuerUniqueId);
this.issuerUniqueId = issuerUniqueId;
return this;
}
/**
* Get the not-valid-before date. The default is the date when this builder was constructed.
*
* @return the not-valid-before date
*/
public ZonedDateTime getNotValidBefore() {
return notValidBefore;
}
/**
* Set the not-valid-before date.
*
* @param notValidBefore the not-valid-before date (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setNotValidBefore(final ZonedDateTime notValidBefore) {
Assert.checkNotNullParam("notValidBefore", notValidBefore);
this.notValidBefore = notValidBefore;
return this;
}
/**
* Get the not-valid-after date. The default is equal to {@code 99991231235959Z} as specified in {@code RFC 5280}.
*
* @return the not-valid-after date
*/
public ZonedDateTime getNotValidAfter() {
return notValidAfter;
}
/**
* Set the not-valid-after date.
*
* @param notValidAfter the not-valid-after date (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setNotValidAfter(final ZonedDateTime notValidAfter) {
Assert.checkNotNullParam("notValidAfter", notValidAfter);
this.notValidAfter = notValidAfter;
return this;
}
/**
* Get the public key.
*
* @return the public key
*/
public PublicKey getPublicKey() {
return publicKey;
}
/**
* Set the public key.
*
* @param publicKey the public key (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setPublicKey(final PublicKey publicKey) {
Assert.checkNotNullParam("publicKey", publicKey);
this.publicKey = publicKey;
return this;
}
/**
* Get the signing key.
*
* @return the signing key
*/
public PrivateKey getSigningKey() {
return signingKey;
}
/**
* Set the signing key.
*
* @param signingKey the signing key (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setSigningKey(final PrivateKey signingKey) {
Assert.checkNotNullParam("signingKey", signingKey);
this.signingKey = signingKey;
return this;
}
/**
* Get the signature algorithm name.
*
* @return the signature algorithm name
*/
public String getSignatureAlgorithmName() {
return signatureAlgorithmName;
}
/**
* Set the signature algorithm name.
*
* @param signatureAlgorithmName the signature algorithm name (must not be {@code null})
* @return this builder instance
*/
public X509CertificateBuilder setSignatureAlgorithmName(final String signatureAlgorithmName) {
Assert.checkNotNullParam("signatureAlgorithmName", signatureAlgorithmName);
this.signatureAlgorithmName = signatureAlgorithmName;
return this;
}
/**
* Attempt to construct and sign an X.509 certificate according to the information in this builder.
*
* @return the constructed certificate
* @throws IllegalArgumentException if one or more of the builder parameters are invalid or missing
* @throws CertificateException if the certificate failed to be constructed
*/
public X509Certificate build() throws CertificateException {
byte[] tbsCertificate = getTBSBytes();
DEREncoder derEncoder = new DEREncoder();
derEncoder.startSequence(); // Certificate
derEncoder.writeEncoded(tbsCertificate);
// signatureAlgorithm
final String signatureAlgorithmName = this.signatureAlgorithmName;
final String signatureAlgorithmOid = ASN1.oidFromSignatureAlgorithm(signatureAlgorithmName);
if (signatureAlgorithmOid == null) {
throw log.asnUnrecognisedAlgorithm(signatureAlgorithmName);
}
derEncoder.startSequence(); // AlgorithmIdentifier
derEncoder.encodeObjectIdentifier(signatureAlgorithmOid);
derEncoder.endSequence(); // AlgorithmIdentifier
try {
final Signature signature = Signature.getInstance(signatureAlgorithmName);
signature.initSign(signingKey);
signature.update(tbsCertificate);
derEncoder.encodeBitString(signature.sign());
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
throw log.certSigningFailed(e);
}
derEncoder.endSequence(); // Certificate
byte[] bytes = derEncoder.getEncoded();
final CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(bytes));
}
byte[] getTBSBytes() {
final BigInteger serialNumber = this.serialNumber;
// Cache and/or validate all fields.
final int version = this.version;
final String signatureAlgorithmName = this.signatureAlgorithmName;
if (signatureAlgorithmName == null) {
throw log.noSignatureAlgorithmNameGiven();
}
final String signatureAlgorithmOid = ASN1.oidFromSignatureAlgorithm(signatureAlgorithmName);
if (signatureAlgorithmOid == null) {
throw log.unknownSignatureAlgorithmName(signatureAlgorithmName);
}
final PrivateKey signingKey = this.signingKey;
if (signingKey == null) {
throw log.noSigningKeyGiven();
}
String signingKeyAlgorithm = signingKey.getAlgorithm();
if (signingKeyAlgorithm.equals("EC")) {
signingKeyAlgorithm = "ECDSA";
}
if (! signatureAlgorithmName.endsWith("with" + signingKeyAlgorithm) || signatureAlgorithmName.contains("with" + signingKeyAlgorithm + "and")) {
throw log.signingKeyNotCompatWithSig(signingKey.getAlgorithm(), signatureAlgorithmName);
}
final ZonedDateTime notValidBefore = this.notValidBefore;
final ZonedDateTime notValidAfter = this.notValidAfter;
if (notValidBefore.compareTo(notValidAfter) > 0) {
throw log.validAfterBeforeValidBefore(notValidBefore, notValidAfter);
}
final X500Principal issuerDn = this.issuerDn;
if (issuerDn == null) {
throw log.noIssuerDnGiven();
}
final X500Principal subjectDn = this.subjectDn;
final PublicKey publicKey = this.publicKey;
if (publicKey == null) {
throw log.noPublicKeyGiven();
}
final byte[] issuerUniqueId = this.issuerUniqueId;
final byte[] subjectUniqueId = this.subjectUniqueId;
if (version < 2 && (issuerUniqueId != null || subjectUniqueId != null)) {
throw log.uniqueIdNotAllowed();
}
final Map extensionsByOid = this.extensionsByOid;
if (version < 3 && ! extensionsByOid.isEmpty()) {
throw log.extensionsNotAllowed();
}
DEREncoder derEncoder = new DEREncoder();
derEncoder.startSequence(); // TBSCertificate
derEncoder.startExplicit(0);
derEncoder.encodeInteger(version - 1);
derEncoder.endExplicit();
derEncoder.encodeInteger(serialNumber);
derEncoder.startSequence(); // AlgorithmIdentifier
derEncoder.encodeObjectIdentifier(signatureAlgorithmOid);
derEncoder.endSequence(); // AlgorithmIdentifier
derEncoder.writeEncoded(issuerDn.getEncoded()); // already a SEQUENCE of SET of SEQUENCE of { OBJECT IDENTIFIER, ANY }
derEncoder.startSequence(); // Validity
derEncoder.encodeGeneralizedTime(notValidBefore.withZoneSameInstant(ZoneOffset.UTC));
derEncoder.encodeGeneralizedTime(notValidAfter.withZoneSameInstant(ZoneOffset.UTC));
derEncoder.endSequence(); // Validity
if (subjectDn != null) derEncoder.writeEncoded(subjectDn.getEncoded()); // already a SEQUENCE of SET of SEQUENCE of { OBJECT IDENTIFIER, ANY }
final X509EncodedKeySpec keySpec;
final String publicKeyAlgorithm = publicKey.getAlgorithm();
try {
final KeyFactory keyFactory = KeyFactory.getInstance(publicKeyAlgorithm);
final Key translatedKey = keyFactory.translateKey(publicKey);
keySpec = keyFactory.getKeySpec(translatedKey, X509EncodedKeySpec.class);
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException e) {
throw log.invalidKeyForCert(publicKeyAlgorithm, e);
}
derEncoder.writeEncoded(keySpec.getEncoded()); // SubjectPublicKeyInfo
if (issuerUniqueId != null) {
derEncoder.encodeImplicit(1);
derEncoder.encodeBitString(issuerUniqueId);
}
if (subjectUniqueId != null) {
derEncoder.encodeImplicit(2);
derEncoder.encodeBitString(subjectUniqueId);
}
if (! extensionsByOid.isEmpty()) {
derEncoder.startExplicit(3);
derEncoder.startSequence();
for (X509CertificateExtension extension : extensionsByOid.values()) {
derEncoder.startSequence();
derEncoder.encodeObjectIdentifier(extension.getId());
if (extension.isCritical()) derEncoder.encodeBoolean(true);
final DEREncoder subEncoder = new DEREncoder();
extension.encodeTo(subEncoder);
derEncoder.encodeOctetString(subEncoder.getEncoded());
derEncoder.endSequence();
}
derEncoder.endSequence();
derEncoder.endExplicit();
}
derEncoder.endSequence(); // TBSCertificate
return derEncoder.getEncoded();
}
}