org.xipki.common.util.X509Util.java Source code

Java tutorial

Introduction

Here is the source code for org.xipki.common.util.X509Util.java

Source

/*
 *
 * This file is part of the XiPKI project.
 * Copyright (c) 2014 - 2015 Lijun Liao
 * Author: Lijun Liao
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * THE AUTHOR LIJUN LIAO. LIJUN LIAO DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
 * OF THIRD PARTY RIGHTS.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the XiPKI software without
 * disclosing the source code of your own applications.
 *
 * For more information, please contact Lijun Liao at this
 * address: lijun.liao@gmail.com
 */

package org.xipki.common.util;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchProviderException;
import java.security.cert.CRLException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DERUniversalString;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.asn1.x500.style.RFC4519Style;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import org.xipki.common.KeyUsage;
import org.xipki.common.ObjectIdentifiers;

/**
 * @author Lijun Liao
 */

public class X509Util {
    public static String getCommonName(final X500Name name) {
        RDN[] rdns = name.getRDNs(ObjectIdentifiers.DN_CN);
        if (rdns != null && rdns.length > 0) {
            return rdnValueToString(rdns[0].getFirst().getValue());
        }
        return null;
    }

    public static X500Name reverse(final X500Name name) {
        RDN[] orig = name.getRDNs();
        int n = orig.length;
        RDN[] _new = new RDN[n];
        for (int i = 0; i < n; i++) {
            _new[i] = orig[n - 1 - i];
        }
        return new X500Name(_new);
    }

    public static X500Name sortX509Name(final X500Name name) {
        return sortX500Name(name, false);
    }

    public static X500Name backwardSortX509Name(final X500Name name) {
        return sortX500Name(name, true);
    }

    private static X500Name sortX500Name(final X500Name name, final boolean backwards) {
        RDN[] requstedRDNs = name.getRDNs();

        List<RDN> rdns = new LinkedList<>();

        List<ASN1ObjectIdentifier> sortedDNs = backwards ? ObjectIdentifiers.getBackwardDNs()
                : ObjectIdentifiers.getForwardDNs();
        int size = sortedDNs.size();
        for (int i = 0; i < size; i++) {
            ASN1ObjectIdentifier type = sortedDNs.get(i);
            RDN[] thisRDNs = getRDNs(requstedRDNs, type);
            int n = thisRDNs == null ? 0 : thisRDNs.length;
            if (n == 0) {
                continue;
            }

            for (RDN thisRDN : thisRDNs) {
                rdns.add(thisRDN);
            }
        }

        return new X500Name(rdns.toArray(new RDN[0]));
    }

    private static RDN[] getRDNs(final RDN[] rdns, final ASN1ObjectIdentifier type) {
        List<RDN> ret = new ArrayList<>(1);
        for (int i = 0; i < rdns.length; i++) {
            RDN rdn = rdns[i];
            if (rdn.getFirst().getType().equals(type)) {
                ret.add(rdn);
            }
        }

        if (CollectionUtil.isEmpty(ret)) {
            return null;
        } else {
            return ret.toArray(new RDN[0]);
        }
    }

    private static CertificateFactory certFact;
    private static Object certFactLock = new Object();

    public static X509Certificate parseCert(final String fileName) throws IOException, CertificateException {
        return parseCert(new File(IoUtil.expandFilepath(fileName)));
    }

    public static X509Certificate parseCert(final File file) throws IOException, CertificateException {
        FileInputStream in = new FileInputStream(IoUtil.expandFilepath(file));
        try {
            return parseCert(in);
        } finally {
            in.close();
        }
    }

    public static X509Certificate parseCert(final byte[] certBytes) throws IOException, CertificateException {
        return parseCert(new ByteArrayInputStream(certBytes));
    }

    public static X509Certificate parseBase64EncodedCert(final String base64EncodedCert)
            throws IOException, CertificateException {
        return parseCert(Base64.decode(base64EncodedCert));
    }

    public static X509Certificate parseCert(final InputStream certStream) throws IOException, CertificateException {
        synchronized (certFactLock) {
            if (certFact == null) {
                try {
                    certFact = CertificateFactory.getInstance("X.509", "BC");
                } catch (NoSuchProviderException e) {
                    throw new IOException("NoSuchProviderException: " + e.getMessage());
                }
            }
        }

        return (X509Certificate) certFact.generateCertificate(certStream);
    }

    public static X509CRL parseCRL(final String f) throws IOException, CertificateException, CRLException {
        return parseCRL(new FileInputStream(IoUtil.expandFilepath(f)));
    }

    public static X509CRL parseCRL(final InputStream crlStream)
            throws IOException, CertificateException, CRLException {
        try {
            if (certFact == null) {
                certFact = CertificateFactory.getInstance("X.509", "BC");
            }
            return (X509CRL) certFact.generateCRL(crlStream);
        } catch (NoSuchProviderException e) {
            throw new IOException("NoSuchProviderException: " + e.getMessage());
        }
    }

    public static String getRFC4519Name(final X500Principal name) {
        return getRFC4519Name(X500Name.getInstance(name.getEncoded()));
    }

    public static String getRFC4519Name(final X500Name name) {
        return RFC4519Style.INSTANCE.toString(name);
    }

    /**
     * First canonicalized the name, and then compute the SHA-1 finger-print over the
     * canonicalized subject string.
     */
    public static String sha1sum_canonicalized_name(final X500Principal prin) {
        X500Name x500Name = X500Name.getInstance(prin.getEncoded());
        return sha1sum_canonicalized_name(x500Name);
    }

    public static String sha1sum_canonicalized_name(final X500Name name) {
        String canonicalizedName = canonicalizName(name);
        byte[] encoded;
        try {
            encoded = canonicalizedName.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            encoded = canonicalizedName.getBytes();
        }
        return SecurityUtil.sha1sum(encoded);
    }

    public static String canonicalizName(final X500Principal prin) {
        X500Name x500Name = X500Name.getInstance(prin.getEncoded());
        return canonicalizName(x500Name);
    }

    public static String canonicalizName(final X500Name name) {
        ASN1ObjectIdentifier[] _types = name.getAttributeTypes();
        int n = _types.length;
        List<String> types = new ArrayList<>(n);
        for (ASN1ObjectIdentifier type : _types) {
            types.add(type.getId());
        }

        Collections.sort(types);

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            String type = types.get(i);
            if (i > 0) {
                sb.append(",");
            }
            sb.append(type).append("=");
            RDN[] rdns = name.getRDNs(new ASN1ObjectIdentifier(type));

            for (int j = 0; j < rdns.length; j++) {
                if (j > 0) {
                    sb.append(";");
                }
                RDN rdn = rdns[j];
                String textValue = IETFUtils.valueToString(rdn.getFirst().getValue()).toLowerCase();
                sb.append(textValue);
            }
        }

        return sb.toString();
    }

    public static byte[] extractSKI(final X509Certificate cert) throws CertificateEncodingException {
        byte[] extValue = getCoreExtValue(cert, Extension.subjectKeyIdentifier);
        if (extValue == null) {
            return null;
        }

        try {
            return ASN1OctetString.getInstance(extValue).getOctets();
        } catch (IllegalArgumentException e) {
            throw new CertificateEncodingException(e.getMessage());
        }
    }

    public static byte[] extractSKI(final org.bouncycastle.asn1.x509.Certificate cert)
            throws CertificateEncodingException {
        Extension encodedSkiValue = cert.getTBSCertificate().getExtensions()
                .getExtension(Extension.subjectKeyIdentifier);
        if (encodedSkiValue == null) {
            return null;
        }

        try {
            return ASN1OctetString.getInstance(encodedSkiValue.getParsedValue()).getOctets();
        } catch (IllegalArgumentException e) {
            throw new CertificateEncodingException("invalid extension SubjectKeyIdentifier: " + e.getMessage());
        }
    }

    public static byte[] extractAKI(final X509Certificate cert) throws CertificateEncodingException {
        byte[] extValue = getCoreExtValue(cert, Extension.authorityKeyIdentifier);
        if (extValue == null) {
            return null;
        }

        try {
            AuthorityKeyIdentifier aki = AuthorityKeyIdentifier.getInstance(extValue);
            return aki.getKeyIdentifier();
        } catch (IllegalArgumentException e) {
            throw new CertificateEncodingException("invalid extension AuthorityKeyIdentifier: " + e.getMessage());
        }
    }

    public static List<String> extractOCSPUrls(final X509Certificate cert) throws CertificateEncodingException {
        byte[] extValue = getCoreExtValue(cert, Extension.authorityInfoAccess);
        if (extValue == null) {
            return Collections.emptyList();
        }

        AuthorityInformationAccess iAIA = AuthorityInformationAccess.getInstance(extValue);

        AccessDescription[] iAccessDescriptions = iAIA.getAccessDescriptions();
        List<AccessDescription> iOCSPAccessDescriptions = new LinkedList<>();
        for (AccessDescription iAccessDescription : iAccessDescriptions) {
            if (iAccessDescription.getAccessMethod().equals(X509ObjectIdentifiers.id_ad_ocsp)) {
                iOCSPAccessDescriptions.add(iAccessDescription);
            }
        }

        int n = iOCSPAccessDescriptions.size();
        List<String> OCSPUris = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            GeneralName iAccessLocation = iOCSPAccessDescriptions.get(i).getAccessLocation();
            if (iAccessLocation.getTagNo() == GeneralName.uniformResourceIdentifier) {
                String iOCSPUri = ((ASN1String) iAccessLocation.getName()).getString();
                OCSPUris.add(iOCSPUri);
            }
        }

        return OCSPUris;
    }

    public static byte[] extractAKI(final org.bouncycastle.asn1.x509.Certificate cert)
            throws CertificateEncodingException {
        try {
            AuthorityKeyIdentifier aki = AuthorityKeyIdentifier
                    .fromExtensions(cert.getTBSCertificate().getExtensions());
            return aki == null ? null : aki.getKeyIdentifier();
        } catch (IllegalArgumentException e) {
            throw new CertificateEncodingException("invalid extension AuthorityKeyIdentifier: " + e.getMessage());
        }
    }

    public static String rdnValueToString(final ASN1Encodable value) {
        if (value instanceof ASN1String && !(value instanceof DERUniversalString)) {
            return ((ASN1String) value).getString();
        } else {
            try {
                return "#" + bytesToString(Hex.encode(value.toASN1Primitive().getEncoded(ASN1Encoding.DER)));
            } catch (IOException e) {
                throw new IllegalArgumentException("other value has no encoded form");
            }
        }
    }

    private static String bytesToString(final byte[] data) {
        char[] cs = new char[data.length];

        for (int i = 0; i != cs.length; i++) {
            cs[i] = (char) (data[i] & 0xff);
        }

        return new String(cs);
    }

    public static org.bouncycastle.asn1.x509.KeyUsage createKeyUsage(final Set<KeyUsage> usages) {
        if (CollectionUtil.isEmpty(usages)) {
            return null;
        }

        int usage = 0;
        for (KeyUsage keyUsage : usages) {
            usage |= keyUsage.getBcUsage();
        }

        return new org.bouncycastle.asn1.x509.KeyUsage(usage);
    }

    public static ExtendedKeyUsage createExtendedUsage(final Set<ASN1ObjectIdentifier> usages) {
        if (CollectionUtil.isEmpty(usages)) {
            return null;
        }

        KeyPurposeId[] kps = new KeyPurposeId[usages.size()];

        int i = 0;
        for (ASN1ObjectIdentifier oid : usages) {
            kps[i++] = KeyPurposeId.getInstance(oid);
        }

        return new ExtendedKeyUsage(kps);
    }

    public static boolean hasKeyusage(final X509Certificate cert, final KeyUsage usage) {
        boolean[] keyusage = cert.getKeyUsage();
        if (keyusage != null && keyusage.length > usage.getBit()) {
            return keyusage[usage.getBit()];
        }
        return false;
    }

    private static byte[] getCoreExtValue(final X509Certificate cert, final ASN1ObjectIdentifier type)
            throws CertificateEncodingException {
        byte[] fullExtValue = cert.getExtensionValue(type.getId());
        if (fullExtValue == null) {
            return null;
        }
        try {
            return ASN1OctetString.getInstance(fullExtValue).getOctets();
        } catch (IllegalArgumentException e) {
            throw new CertificateEncodingException("invalid extension " + type.getId() + ": " + e.getMessage());
        }
    }

    /**
     * Cross certificate will not be considered
     */
    public static X509Certificate[] buildCertPath(final X509Certificate cert,
            final Set<? extends Certificate> certs) {
        List<X509Certificate> certChain = new LinkedList<>();
        certChain.add(cert);
        try {
            if (certs != null && isSelfSigned(cert) == false) {
                while (true) {
                    X509Certificate caCert = getCaCertOf(certChain.get(certChain.size() - 1), certs);
                    if (caCert == null) {
                        break;
                    }
                    certChain.add(caCert);
                    if (isSelfSigned(caCert)) {
                        // reaches root self-signed certificate
                        break;
                    }
                }
            }
        } catch (CertificateEncodingException e) {
        }

        final int n = certChain.size();
        int len = n;
        if (n > 1) {
            for (int i = 1; i < n; i++) {
                int pathLen = certChain.get(i).getBasicConstraints();
                if (pathLen < 0 || pathLen < i) {
                    len = i;
                    break;
                }
            }
        }

        if (len == n) {
            return certChain.toArray(new X509Certificate[0]);
        } else {
            X509Certificate[] ret = new X509Certificate[len];
            for (int i = 0; i < len; i++) {
                ret[i] = certChain.get(i);
            }
            return ret;
        }
    }

    public static X509Certificate[] buildCertPath(final X509Certificate cert, final Certificate[] certs) {
        Set<Certificate> setOfCerts = new HashSet<>();
        for (Certificate m : certs) {
            setOfCerts.add(m);
        }

        return buildCertPath(cert, setOfCerts);
    }

    public static X509Certificate getCaCertOf(final X509Certificate cert, final Set<? extends Certificate> caCerts)
            throws CertificateEncodingException {
        if (isSelfSigned(cert)) {
            return null;
        }

        for (Certificate caCert : caCerts) {
            if (caCert instanceof X509Certificate == false) {
                continue;
            }

            X509Certificate x509CaCert = (X509Certificate) caCert;
            if (issues(x509CaCert, cert) == false) {
                continue;
            }

            try {
                cert.verify(x509CaCert.getPublicKey());
                return x509CaCert;
            } catch (Exception e) {
            }
        }

        return null;
    }

    public static boolean isSelfSigned(final X509Certificate cert) throws CertificateEncodingException {
        boolean equals = cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal());
        if (equals) {
            byte[] ski = X509Util.extractSKI(cert);
            byte[] aki = X509Util.extractAKI(cert);
            if (ski != null && aki != null) {
                equals = Arrays.equals(ski, aki);
            }
        }
        return equals;
    }

    public static boolean issues(final X509Certificate issuerCert, final X509Certificate cert)
            throws CertificateEncodingException {
        boolean isCA = issuerCert.getBasicConstraints() >= 0;
        if (isCA == false) {
            return false;
        }

        boolean issues = issuerCert.getSubjectX500Principal().equals(cert.getIssuerX500Principal());
        if (issues) {
            byte[] ski = X509Util.extractSKI(issuerCert);
            byte[] aki = X509Util.extractAKI(cert);
            if (ski != null) {
                issues = Arrays.equals(ski, aki);
            }
        }

        if (issues) {
            long issuerNotBefore = issuerCert.getNotBefore().getTime();
            long issuerNotAfter = issuerCert.getNotAfter().getTime();
            long notBefore = cert.getNotBefore().getTime();
            issues = notBefore <= issuerNotAfter && notBefore >= issuerNotBefore;
        }

        return issues;
    }

    public static SubjectPublicKeyInfo toRfc3279Style(final SubjectPublicKeyInfo publicKeyInfo)
            throws InvalidKeySpecException {
        // TODO: add support of other algorithms
        ASN1ObjectIdentifier algOid = publicKeyInfo.getAlgorithm().getAlgorithm();
        ASN1Encodable keyParameters = publicKeyInfo.getAlgorithm().getParameters();

        if (PKCSObjectIdentifiers.rsaEncryption.equals(algOid)) {
            if (DERNull.INSTANCE.equals(keyParameters)) {
                return publicKeyInfo;
            } else {
                AlgorithmIdentifier keyAlgId = new AlgorithmIdentifier(algOid, DERNull.INSTANCE);
                return new SubjectPublicKeyInfo(keyAlgId, publicKeyInfo.getPublicKeyData().getBytes());
            }
        } else {
            return publicKeyInfo;
        }
    }

}