/* * 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(): *
* * 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 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 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 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 locations) throws IOException { if (!locations.isEmpty()) { List 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 ekuOids) throws IOException { if (!ekuOids.isEmpty()) { Vector 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: * *
     *  Certificate  ::=  SEQUENCE  {
     *      tbsCertificate       TBSCertificate,
     *      signatureAlgorithm   AlgorithmIdentifier,
     *      signatureValue       BIT STRING  }
     * 
* * @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: *
     *  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:
     *
     * 
     *  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
     *      }
     * 
* * @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(); } }