/* * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package sun.security.testlibrary; import java.io.*; import java.util.*; import java.security.*; import java.security.cert.X509Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.Extension; import javax.security.auth.x500.X500Principal; import java.math.BigInteger; import sun.security.util.DerOutputStream; import sun.security.util.DerValue; import sun.security.util.ObjectIdentifier; import sun.security.x509.AccessDescription; import sun.security.x509.AlgorithmId; import sun.security.x509.AuthorityInfoAccessExtension; import sun.security.x509.AuthorityKeyIdentifierExtension; import sun.security.x509.SubjectKeyIdentifierExtension; import sun.security.x509.BasicConstraintsExtension; import sun.security.x509.ExtendedKeyUsageExtension; import sun.security.x509.DNSName; import sun.security.x509.GeneralName; import sun.security.x509.GeneralNames; import sun.security.x509.KeyUsageExtension; import sun.security.x509.SerialNumber; import sun.security.x509.SubjectAlternativeNameExtension; import sun.security.x509.URIName; import sun.security.x509.KeyIdentifier; /** * Helper class that builds and signs X.509 certificates. * * A CertificateBuilder is created with a default constructor, and then * uses additional public methods to set the public key, desired validity * dates, serial number and extensions. It is expected that the caller will * have generated the necessary key pairs prior to using a CertificateBuilder * to generate certificates. * * The following methods are mandatory before calling build(): * <UL> * <LI>{@link #setSubjectName(java.lang.String)} * <LI>{@link #setPublicKey(java.security.PublicKey)} * <LI>{@link #setNotBefore(java.util.Date)} and * {@link #setNotAfter(java.util.Date)}, or * {@link #setValidity(java.util.Date, java.util.Date)} * <LI>{@link #setSerialNumber(java.math.BigInteger)} * </UL><BR> * * Additionally, the caller can either provide a {@link List} of * {@link Extension} objects, or use the helper classes to add specific * extension types. * * When all required and desired parameters are set, the * {@link #build(java.security.cert.X509Certificate, java.security.PrivateKey, * java.lang.String)} method can be used to create the {@link X509Certificate} * object. * * Multiple certificates may be cut from the same settings using subsequent * calls to the build method. Settings may be cleared using the * {@link #reset()} method. */ public class CertificateBuilder { private final CertificateFactory factory; private X500Principal subjectName = null; private BigInteger serialNumber = null; private PublicKey publicKey = null; private Date notBefore = null; private Date notAfter = null; private final Map<String, Extension> extensions = new HashMap<>(); private byte[] tbsCertBytes; private byte[] signatureBytes; /** * Default constructor for a {@code CertificateBuilder} object. * * @throws CertificateException if the underlying {@link CertificateFactory} * cannot be instantiated. */ public CertificateBuilder() throws CertificateException { factory = CertificateFactory.getInstance("X.509"); } /** * Set the subject name for the certificate. * * @param name An {@link X500Principal} to be used as the subject name * on this certificate. */ public void setSubjectName(X500Principal name) { subjectName = name; } /** * Set the subject name for the certificate. * * @param name The subject name in RFC 2253 format */ public void setSubjectName(String name) { subjectName = new X500Principal(name); } /** * Set the public key for this certificate. * * @param pubKey The {@link PublicKey} to be used on this certificate. */ public void setPublicKey(PublicKey pubKey) { publicKey = Objects.requireNonNull(pubKey, "Caught null public key"); } /** * Set the NotBefore date on the certificate. * * @param nbDate A {@link Date} object specifying the start of the * certificate validity period. */ public void setNotBefore(Date nbDate) { Objects.requireNonNull(nbDate, "Caught null notBefore date"); notBefore = (Date)nbDate.clone(); } /** * Set the NotAfter date on the certificate. * * @param naDate A {@link Date} object specifying the end of the * certificate validity period. */ public void setNotAfter(Date naDate) { Objects.requireNonNull(naDate, "Caught null notAfter date"); notAfter = (Date)naDate.clone(); } /** * Set the validity period for the certificate * * @param nbDate A {@link Date} object specifying the start of the * certificate validity period. * @param naDate A {@link Date} object specifying the end of the * certificate validity period. */ public void setValidity(Date nbDate, Date naDate) { setNotBefore(nbDate); setNotAfter(naDate); } /** * Set the serial number on the certificate. * * @param serial A serial number in {@link BigInteger} form. */ public void setSerialNumber(BigInteger serial) { Objects.requireNonNull(serial, "Caught null serial number"); serialNumber = serial; } /** * Add a single extension to the certificate. * * @param ext The extension to be added. */ public void addExtension(Extension ext) { Objects.requireNonNull(ext, "Caught null extension"); extensions.put(ext.getId(), ext); } /** * Add multiple extensions contained in a {@code List}. * * @param extList The {@link List} of extensions to be added to * the certificate. */ public void addExtensions(List<Extension> extList) { Objects.requireNonNull(extList, "Caught null extension list"); for (Extension ext : extList) { extensions.put(ext.getId(), ext); } } /** * Helper method to add DNSName types for the SAN extension * * @param dnsNames A {@code List} of names to add as DNSName types * * @throws IOException if an encoding error occurs. */ public void addSubjectAltNameDNSExt(List<String> dnsNames) throws IOException { if (!dnsNames.isEmpty()) { GeneralNames gNames = new GeneralNames(); for (String name : dnsNames) { gNames.add(new GeneralName(new DNSName(name))); } addExtension(new SubjectAlternativeNameExtension(false, gNames)); } } /** * Helper method to add one or more OCSP URIs to the Authority Info Access * certificate extension. * * @param locations A list of one or more OCSP responder URIs as strings * * @throws IOException if an encoding error occurs. */ public void addAIAExt(List<String> locations) throws IOException { if (!locations.isEmpty()) { List<AccessDescription> acDescList = new ArrayList<>(); for (String ocspUri : locations) { acDescList.add(new AccessDescription( AccessDescription.Ad_OCSP_Id, new GeneralName(new URIName(ocspUri)))); } addExtension(new AuthorityInfoAccessExtension(acDescList)); } } /** * Set a Key Usage extension for the certificate. The extension will * be marked critical. * * @param bitSettings Boolean array for all nine bit settings in the order * documented in RFC 5280 section 4.2.1.3. * * @throws IOException if an encoding error occurs. */ public void addKeyUsageExt(boolean[] bitSettings) throws IOException { addExtension(new KeyUsageExtension(bitSettings)); } /** * Set the Basic Constraints Extension for a certificate. * * @param crit {@code true} if critical, {@code false} otherwise * @param isCA {@code true} if the extension will be on a CA certificate, * {@code false} otherwise * @param maxPathLen The maximum path length issued by this CA. Values * less than zero will omit this field from the resulting extension and * no path length constraint will be asserted. * * @throws IOException if an encoding error occurs. */ public void addBasicConstraintsExt(boolean crit, boolean isCA, int maxPathLen) throws IOException { addExtension(new BasicConstraintsExtension(crit, isCA, maxPathLen)); } /** * Add the Authority Key Identifier extension. * * @param authorityCert The certificate of the issuing authority. * * @throws IOException if an encoding error occurs. */ public void addAuthorityKeyIdExt(X509Certificate authorityCert) throws IOException { addAuthorityKeyIdExt(authorityCert.getPublicKey()); } /** * Add the Authority Key Identifier extension. * * @param authorityKey The public key of the issuing authority. * * @throws IOException if an encoding error occurs. */ public void addAuthorityKeyIdExt(PublicKey authorityKey) throws IOException { KeyIdentifier kid = new KeyIdentifier(authorityKey); addExtension(new AuthorityKeyIdentifierExtension(kid, null, null)); } /** * Add the Subject Key Identifier extension. * * @param subjectKey The public key to be used in the resulting certificate * * @throws IOException if an encoding error occurs. */ public void addSubjectKeyIdExt(PublicKey subjectKey) throws IOException { byte[] keyIdBytes = new KeyIdentifier(subjectKey).getIdentifier(); addExtension(new SubjectKeyIdentifierExtension(keyIdBytes)); } /** * Add the Extended Key Usage extension. * * @param ekuOids A {@link List} of object identifiers in string form. * * @throws IOException if an encoding error occurs. */ public void addExtendedKeyUsageExt(List<String> ekuOids) throws IOException { if (!ekuOids.isEmpty()) { Vector<ObjectIdentifier> oidVector = new Vector<>(); for (String oid : ekuOids) { oidVector.add(new ObjectIdentifier(oid)); } addExtension(new ExtendedKeyUsageExtension(oidVector)); } } /** * Clear all settings and return the {@code CertificateBuilder} to * its default state. */ public void reset() { extensions.clear(); subjectName = null; notBefore = null; notAfter = null; serialNumber = null; publicKey = null; signatureBytes = null; tbsCertBytes = null; } /** * Build the certificate. * * @param issuerCert The certificate of the issuing authority, or * {@code null} if the resulting certificate is self-signed. * @param issuerKey The private key of the issuing authority * @param algName The signature algorithm name * * @return The resulting {@link X509Certificate} * * @throws IOException if an encoding error occurs. * @throws CertificateException If the certificate cannot be generated * by the underlying {@link CertificateFactory} * @throws NoSuchAlgorithmException If an invalid signature algorithm * is provided. */ public X509Certificate build(X509Certificate issuerCert, PrivateKey issuerKey, String algName) throws IOException, CertificateException, NoSuchAlgorithmException { // TODO: add some basic checks (key usage, basic constraints maybe) AlgorithmId signAlg = AlgorithmId.get(algName); byte[] encodedCert = encodeTopLevel(issuerCert, issuerKey, signAlg); ByteArrayInputStream bais = new ByteArrayInputStream(encodedCert); return (X509Certificate)factory.generateCertificate(bais); } /** * Encode the contents of the outer-most ASN.1 SEQUENCE: * * <PRE> * Certificate ::= SEQUENCE { * tbsCertificate TBSCertificate, * signatureAlgorithm AlgorithmIdentifier, * signatureValue BIT STRING } * </PRE> * * @param issuerCert The certificate of the issuing authority, or * {@code null} if the resulting certificate is self-signed. * @param issuerKey The private key of the issuing authority * @param signAlg The signature algorithm object * * @return The DER-encoded X.509 certificate * * @throws CertificateException If an error occurs during the * signing process. * @throws IOException if an encoding error occurs. */ private byte[] encodeTopLevel(X509Certificate issuerCert, PrivateKey issuerKey, AlgorithmId signAlg) throws CertificateException, IOException { DerOutputStream outerSeq = new DerOutputStream(); DerOutputStream topLevelItems = new DerOutputStream(); tbsCertBytes = encodeTbsCert(issuerCert, signAlg); topLevelItems.write(tbsCertBytes); try { signatureBytes = signCert(issuerKey, signAlg); } catch (GeneralSecurityException ge) { throw new CertificateException(ge); } signAlg.derEncode(topLevelItems); topLevelItems.putBitString(signatureBytes); outerSeq.write(DerValue.tag_Sequence, topLevelItems); return outerSeq.toByteArray(); } /** * Encode the bytes for the TBSCertificate structure: * <PRE> * TBSCertificate ::= SEQUENCE { * version [0] EXPLICIT Version DEFAULT v1, * serialNumber CertificateSerialNumber, * signature AlgorithmIdentifier, * issuer Name, * validity Validity, * subject Name, * subjectPublicKeyInfo SubjectPublicKeyInfo, * issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, * -- If present, version MUST be v2 or v3 * subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, * -- If present, version MUST be v2 or v3 * extensions [3] EXPLICIT Extensions OPTIONAL * -- If present, version MUST be v3 * } * * @param issuerCert The certificate of the issuing authority, or * {@code null} if the resulting certificate is self-signed. * @param signAlg The signature algorithm object * * @return The DER-encoded bytes for the TBSCertificate structure * * @throws IOException if an encoding error occurs. */ private byte[] encodeTbsCert(X509Certificate issuerCert, AlgorithmId signAlg) throws IOException { DerOutputStream tbsCertSeq = new DerOutputStream(); DerOutputStream tbsCertItems = new DerOutputStream(); // Hardcode to V3 byte[] v3int = {0x02, 0x01, 0x02}; tbsCertItems.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte)0), v3int); // Serial Number SerialNumber sn = new SerialNumber(serialNumber); sn.encode(tbsCertItems); // Algorithm ID signAlg.derEncode(tbsCertItems); // Issuer Name if (issuerCert != null) { tbsCertItems.write( issuerCert.getSubjectX500Principal().getEncoded()); } else { // Self-signed tbsCertItems.write(subjectName.getEncoded()); } // Validity period (set as UTCTime) DerOutputStream valSeq = new DerOutputStream(); valSeq.putUTCTime(notBefore); valSeq.putUTCTime(notAfter); tbsCertItems.write(DerValue.tag_Sequence, valSeq); // Subject Name tbsCertItems.write(subjectName.getEncoded()); // SubjectPublicKeyInfo tbsCertItems.write(publicKey.getEncoded()); // TODO: Extensions! encodeExtensions(tbsCertItems); // Wrap it all up in a SEQUENCE and return the bytes tbsCertSeq.write(DerValue.tag_Sequence, tbsCertItems); return tbsCertSeq.toByteArray(); } /** * Encode the extensions segment for an X.509 Certificate: * * <PRE> * Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension * * Extension ::= SEQUENCE { * extnID OBJECT IDENTIFIER, * critical BOOLEAN DEFAULT FALSE, * extnValue OCTET STRING * -- contains the DER encoding of an ASN.1 value * -- corresponding to the extension type identified * -- by extnID * } * </PRE> * * @param tbsStream The {@code DerOutputStream} that holds the * TBSCertificate contents. * * @throws IOException if an encoding error occurs. */ private void encodeExtensions(DerOutputStream tbsStream) throws IOException { DerOutputStream extSequence = new DerOutputStream(); DerOutputStream extItems = new DerOutputStream(); for (Extension ext : extensions.values()) { ext.encode(extItems); } extSequence.write(DerValue.tag_Sequence, extItems); tbsStream.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte)3), extSequence); } /** * Digitally sign the X.509 certificate. * * @param issuerKey The private key of the issuing authority * @param signAlg The signature algorithm object * * @return The digital signature bytes. * * @throws GeneralSecurityException If any errors occur during the * digital signature process. */ private byte[] signCert(PrivateKey issuerKey, AlgorithmId signAlg) throws GeneralSecurityException { Signature sig = Signature.getInstance(signAlg.getName()); sig.initSign(issuerKey); sig.update(tbsCertBytes); return sig.sign(); } }