com.zimbra.cs.service.authenticator.CertUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.cs.service.authenticator.CertUtil.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 2011, 2013, 2014, 2016 Synacor, Inc.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software Foundation,
 * version 2 of the License.
 *
 * 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 General Public License for more details.
 * You should have received a copy of the GNU General Public License along with this program.
 * If not, see <https://www.gnu.org/licenses/>.
 * ***** END LICENSE BLOCK *****
 */
package com.zimbra.cs.service.authenticator;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.security.auth.x500.X500Principal;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DEREncodable;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.X509Extension;

import com.zimbra.common.util.ByteUtil;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.service.authenticator.ClientCertPrincipalMap.CertField;
import com.zimbra.cs.service.authenticator.ClientCertPrincipalMap.KnownCertField;
import com.zimbra.cs.service.authenticator.ClientCertPrincipalMap.SubjectCertField;

public class CertUtil {
    static final String LOG_PREFIX = ClientCertAuthenticator.LOG_PREFIX;

    static final String ATTR_EMAILADDRESS = "EMAILADDRESS";

    /* ObjectID for UPN in SubjectaltName for windows smart card logon */
    private static final String OID_UPN = "1.3.6.1.4.1.311.20.2.3";

    /*
     * RFC 2253 2.3
     *
     * If the AttributeType is in a published table of attribute types
     * associated with LDAP [4] (RFC 2252), then the type name string from that table
     * is used, otherwise it is encoded as the dotted-decimal encoding of
     * the AttributeType's OBJECT IDENTIFIER.
     *
     *
     */
    private static final Map<String, String> KNOWN_NON_RFC2252_ATTRS = new HashMap<String, String>();

    static {
        KNOWN_NON_RFC2252_ATTRS.put(ATTR_EMAILADDRESS, "1.2.840.113549.1.9.1");
    }

    X509Certificate cert;

    private CertUtil() {
    }

    CertUtil(X509Certificate cert) {
        this.cert = cert;
    }

    String getCertField(CertField certField) {
        if (certField instanceof KnownCertField) {
            return getKnownCertField((KnownCertField) certField);
        } else if (certField instanceof SubjectCertField) {
            return getSubjectCertField((SubjectCertField) certField);
        }
        return null;
    }

    private String getKnownCertField(KnownCertField certField) {
        String value = null;

        switch (certField.getField()) {
        case SUBJECT_DN:
            value = getSubjectDN();
            break;
        case SUBJECTALTNAME_OTHERNAME_UPN:
            value = getSubjectAltNameOtherNameUPN();
            break;
        case SUBJECTALTNAME_RFC822NAME:
            value = getSubjectAltNameRfc822Name();
            break;
        }
        return value;
    }

    private String getSubjectCertField(SubjectCertField certField) {
        String rdnAttrType = certField.getRDNAttrType();
        return getSubjectAttr(rdnAttrType, KNOWN_NON_RFC2252_ATTRS.get(rdnAttrType));
    }

    String getSubjectDN() {
        X500Principal subjectPrincipal = cert.getSubjectX500Principal();

        /*
        String CANONICAL = subjectPrincipal.getName(X500Principal.CANONICAL);
        // 1.2.840.113549.1.9.1=#161075736572314070686f6562652e6d6270,cn=user one,ou=engineering,o=example company,l=saratoga,st=california,c=us
            
        String RFC1779   = subjectPrincipal.getName(X500Principal.RFC1779);
        // OID.1.2.840.113549.1.9.1=user1@phoebe.mbp, CN=user one, OU=Engineering, O=Example Company, L=Saratoga, ST=California, C=US
            
        String RFC2253   = subjectPrincipal.getName(X500Principal.RFC2253);
        // 1.2.840.113549.1.9.1=#161075736572314070686f6562652e6d6270,CN=user one,OU=Engineering,O=Example Company,L=Saratoga,ST=California,C=US
        */

        return subjectPrincipal.getName();
    }

    String getSubjectAltNameOtherNameUPN() {
        Collection<List<?>> generalNames = null;
        try {
            generalNames = cert.getSubjectAlternativeNames();
        } catch (CertificateParsingException e) {
            ZimbraLog.account.warn(LOG_PREFIX + "unable to get subject alternative names", e);
        }

        if (generalNames == null) {
            return null;
        }

        ASN1InputStream decoder = null;
        try {
            // Check that the certificate includes the SubjectAltName extension
            for (List<?> generalName : generalNames) {
                Integer tag = (Integer) generalName.get(0);
                if (GeneralName.otherName == tag.intValue()) {
                    // Value is encoded using ASN.1
                    decoder = new ASN1InputStream((byte[]) generalName.toArray()[1]);
                    DEREncodable encoded = decoder.readObject();
                    DERSequence derSeq = (DERSequence) encoded;

                    DERObjectIdentifier typeId = DERObjectIdentifier.getInstance(derSeq.getObjectAt(0));
                    String oid = typeId.getId();

                    String value = null;
                    ASN1TaggedObject otherNameValue = ASN1TaggedObject.getInstance(derSeq.getObjectAt(1));
                    if (OID_UPN.equals(oid)) {
                        ASN1TaggedObject upnValue = ASN1TaggedObject.getInstance(otherNameValue.getObject());
                        DERUTF8String str = DERUTF8String.getInstance(upnValue.getObject());
                        value = str.getString();
                        return value;
                    }
                }
            }
        } catch (IOException e) {
            ZimbraLog.account.warn(LOG_PREFIX + "unable to process ASN.1 data", e);
        } finally {
            ByteUtil.closeStream(decoder);
        }

        return null;
    }

    String getSubjectAltNameRfc822Name() {
        Collection<List<?>> generalNames = null;
        try {
            generalNames = cert.getSubjectAlternativeNames();
        } catch (CertificateParsingException e) {
            ZimbraLog.account.warn(LOG_PREFIX + "unable to get subject alternative names", e);
        }

        if (generalNames == null) {
            return null;
        }

        for (List<?> generalName : generalNames) {
            Integer tag = (Integer) generalName.get(0);
            if (GeneralName.rfc822Name == tag.intValue()) {
                String value = (String) generalName.get(1);
                return value;
            }
        }

        return null;
    }

    private String getSubjectAttr(String needAttrName, String needAttrOid) {
        String subjectDN = getSubjectDN();

        try {
            LdapName dn = new LdapName(subjectDN);
            List<Rdn> rdns = dn.getRdns();

            for (Rdn rdn : rdns) {
                String type = rdn.getType();

                boolean isOid = type.contains(".");

                boolean matched = (isOid ? type.equals(needAttrOid) : type.equals(needAttrName));

                if (matched) {
                    Object value = rdn.getValue();
                    if (value == null) {
                        continue;
                    }

                    if (isOid) {
                        byte[] bytes = (byte[]) value;
                        ASN1InputStream decoder = null;
                        try {
                            decoder = new ASN1InputStream(bytes);
                            DEREncodable encoded = decoder.readObject();
                            DERIA5String str = DERIA5String.getInstance(encoded);
                            return str.getString();
                        } catch (IOException e) {
                            ZimbraLog.account.warn(LOG_PREFIX + "unable to decode " + type, e);
                        } finally {
                            ByteUtil.closeStream(decoder);
                        }

                    } else {
                        return value.toString();
                    }
                }
            }
        } catch (InvalidNameException e) {
            ZimbraLog.account.warn(LOG_PREFIX + "Invalid subject dn value" + subjectDN, e);
        }

        return null;
    }

    /*
     * ======================================================
     * Printing methods below for CLI - Not production code
     * ======================================================
     */

    private void loadCert(String certFilePath) throws Exception {
        InputStream inStream = new FileInputStream(certFilePath);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        cert = (X509Certificate) cf.generateCertificate(inStream);
        inStream.close();
    }

    private void dumpCert(String outputFlePath) throws Exception {
        outputCert(outputFlePath, true);
    }

    private void printCert(String outputFlePath) throws Exception {
        outputCert(outputFlePath, false);
    }

    private void outputCert(String outputFlePath, boolean dump) throws Exception {
        PrintStream outStream;

        if (outputFlePath != null) {
            outStream = new PrintStream(outputFlePath);
        } else {
            outStream = System.out;
        }

        try {
            if (dump) {
                outStream.println(cert.toString());
            } else {
                printCert(outStream);
            }
        } finally {
            outStream.flush();
            if (outputFlePath != null) {
                outStream.close();
            }
        }
    }

    private void printCert(PrintStream outStream) throws Exception {
        printVersion(outStream);
        printSerialNumber(outStream);
        printSigAlg(outStream);
        printIssuer(outStream);
        printValidity(outStream);
        printSubject(outStream);

        printX509V3Extension(outStream);
    }

    private void printX509V3Extension(PrintStream outStream) throws Exception {
        outStream.println();
        outStream.format("X509v3 extensions:\n");

        printSubjectAlternativeNames(outStream);
        outStream.println();

        printCRLDistributionPoints(outStream);
        outStream.println();
    }

    private void printVersion(PrintStream outStream) {
        int version = cert.getVersion();

        outStream.format("Version: %d (0x%x)\n", version, version);
    }

    private void printSerialNumber(PrintStream outStream) {
        BigInteger serialNumber = cert.getSerialNumber();

        outStream.format("Serial Number: %s (0x%x)\n", serialNumber.toString(), serialNumber);
    }

    private void printSigAlg(PrintStream outStream) throws Exception {
        String sigAlgName = cert.getSigAlgName();
        String sigAlgOID = cert.getSigAlgOID();

        outStream.format("Signature Algorithm: %s (%s)\n", sigAlgName, sigAlgOID);

        /*
        byte[] sigAlgParams = cert.getSigAlgParams();
            
        AlgorithmParameters algParams = AlgorithmParameters.getInstance(sigAlgName);
        algParams.init(sigAlgParams);
        outStream.format("Signature Algorithm Params: %s", algParams.toString());
        */
    }

    private void printIssuer(PrintStream outStream) {
        X500Principal issuerPrincipal = cert.getIssuerX500Principal();
        outStream.format("Issuer: %s\n", issuerPrincipal.getName());
    }

    private void printValidity(PrintStream outStream) {
        Date notBefore = cert.getNotBefore();
        Date notAfter = cert.getNotAfter();

        outStream.format("Validity\n");
        outStream.format("    Not Before: %s\n", notBefore.toGMTString());
        outStream.format("    Not After : %s\n", notAfter.toGMTString());
    }

    private void printSubject(PrintStream outStream) {
        X500Principal subjectPrincipal = cert.getSubjectX500Principal();
        outStream.format("Subject: %s\n", subjectPrincipal.getName());
    }

    private void printSubjectAlternativeNames(PrintStream outStream) throws Exception {

        final String UPN_DISPLAY = "Principal Name";
        final String RFC822NAME_DISPLAY = "RFC822 Name";
        final String DNSNAME_DISPLAY = "DNS Name";

        outStream.format("X509v3 Subject Alternative Name: \n");

        ASN1InputStream decoder = null;
        try {
            Collection<List<?>> generalNames = cert.getSubjectAlternativeNames();
            // Check that the certificate includes the SubjectAltName extension
            if (generalNames == null) {
                return;
            }

            /*
               OtherName ::= SEQUENCE {
              type-id    OBJECT IDENTIFIER,
              value      [0] EXPLICIT ANY DEFINED BY type-id }
             */

            for (List<?> generalName : generalNames) {
                Integer tag = (Integer) generalName.get(0);
                if (GeneralName.otherName == tag.intValue()) {
                    // Value is encoded using ASN.1
                    decoder = new ASN1InputStream((byte[]) generalName.toArray()[1]);
                    DEREncodable encoded = decoder.readObject();
                    DERSequence derSeq = (DERSequence) encoded;

                    DERObjectIdentifier typeId = DERObjectIdentifier.getInstance(derSeq.getObjectAt(0));
                    String oid = typeId.getId();

                    String value = null;
                    ASN1TaggedObject otherNameValue = ASN1TaggedObject.getInstance(derSeq.getObjectAt(1));
                    if (OID_UPN.equals(oid)) {
                        ASN1TaggedObject upnValue = ASN1TaggedObject.getInstance(otherNameValue.getObject());
                        DERUTF8String str = DERUTF8String.getInstance(upnValue.getObject());
                        value = str.getString();
                    }

                    outStream.format("    [%d] %s(%s) = %s\n", tag, oid, UPN_DISPLAY, value);
                } else if (GeneralName.rfc822Name == tag.intValue()) {
                    String value = (String) generalName.get(1);
                    outStream.format("    [%d] %s = %s\n", tag, RFC822NAME_DISPLAY, value);
                } else if (GeneralName.dNSName == tag.intValue()) {
                    String value = (String) generalName.get(1);
                    outStream.format("    [%d] %s = %s\n", tag, DNSNAME_DISPLAY, value);
                } else {
                    outStream.format("    [%d] - not yet supported\n", tag);
                }

            }
        } catch (CertificateParsingException e) {
            e.printStackTrace();
        } finally {
            ByteUtil.closeStream(decoder);
        }
    }

    private void printCRLDistributionPoints(PrintStream outStream) throws Exception {

        outStream.format("X509v3 CRL Distribution Points: \n");

        String extOid = X509Extension.cRLDistributionPoints.getId(); // 2.5.29.31
        byte[] extVal = cert.getExtensionValue(extOid);
        if (extVal == null) {
            return;
        }

        /* http://download.oracle.com/javase/6/docs/api/java/security/cert/X509Extension.html#getExtensionValue(java.lang.String)
         *
           The ASN.1 definition for this is:
            
         Extensions  ::=  SEQUENCE SIZE (1..MAX) OF Extension
            
         Extension  ::=  SEQUENCE  {
             extnId        OBJECT IDENTIFIER,
             critical      BOOLEAN DEFAULT FALSE,
             extnValue     OCTET STRING
                           -- contains a DER encoding of a value
                           -- of the type registered for use with
                           -- the extnId object identifier value
         }
         */

        byte[] extnValue = DEROctetString.getInstance(ASN1Object.fromByteArray(extVal)).getOctets();

        CRLDistPoint crlDistPoint = CRLDistPoint.getInstance(ASN1Object.fromByteArray(extnValue));
        DistributionPoint[] distPoints = crlDistPoint.getDistributionPoints();

        for (DistributionPoint distPoint : distPoints) {
            DistributionPointName distPointName = distPoint.getDistributionPoint();
            int type = distPointName.getType();

            if (DistributionPointName.FULL_NAME == type) {
                outStream.format("Full Name: \n");
                GeneralNames generalNames = GeneralNames.getInstance(distPointName.getName());
                GeneralName[] names = generalNames.getNames();
                for (GeneralName generalname : names) {
                    int tag = generalname.getTagNo();
                    if (GeneralName.uniformResourceIdentifier == tag) {
                        DEREncodable name = generalname.getName();
                        DERIA5String str = DERIA5String.getInstance(name);
                        String value = str.getString();
                        outStream.format("    %s\n", value);
                    } else {
                        outStream.format("tag %d not yet implemented", tag);
                    }
                }
            } else {
                outStream.format("type %d not yet implemented", type);
            }
        }
    }

    private static int EXIT_CODE_GOOD = 0;
    private static int EXIT_CODE_BAD = 0;

    private static String O_CERT = "c";
    private static String O_DUMP = "d";
    private static String O_GET = "g";
    private static String O_HELP = "h";
    private static String O_PRINT = "p";

    private static void usage(Options options, String msg) {
        System.out.println("\n");
        System.out.println(msg);
        usage(options);
    }

    private static void usage(Options options) {
        System.out.println("\n");
        PrintWriter pw = new PrintWriter(System.out, true);
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp(pw, formatter.getWidth(), "zmjava " + CertUtil.class.getCanonicalName() + " [options]",
                null, options, formatter.getLeftPadding(), formatter.getDescPadding(), null);
        System.out.println("\n");
        pw.flush();
    }

    /*
     *  zmjava com.zimbra.cs.service.authenticator.CertUtil [options]
     */
    public static void main(String[] args) {

        Options options = new Options();
        options.addOption(O_CERT, true, "file path of the certificate");
        options.addOption(O_DUMP, false, "dump the certificate (print toString() value of the certificate)");
        options.addOption(O_GET, true, "get a field in the certificate, valid fields:" + KnownCertField.names()
                + "|" + SubjectCertField.names());
        options.addOption(O_HELP, false, "print usage");
        options.addOption(O_PRINT, false, "print the certificate(print each parsed certificate fields)");

        CommandLine cl = null;
        try {
            CommandLineParser parser = new GnuParser();
            cl = parser.parse(options, args);
            if (cl == null) {
                throw new ParseException("");
            }
        } catch (ParseException e) {
            usage(options);
            e.printStackTrace();
            System.exit(EXIT_CODE_BAD);
        }

        if (cl.hasOption(O_HELP)) {
            usage(options);
            System.exit(EXIT_CODE_GOOD);
        }

        String certFilePath = null;

        if (cl.hasOption(O_CERT)) {
            certFilePath = cl.getOptionValue(O_CERT);
        } else {
            usage(options, "missing cert path");
            System.exit(EXIT_CODE_BAD);
        }

        try {
            CertUtil certUtil = new CertUtil();
            certUtil.loadCert(certFilePath);

            if (cl.hasOption(O_DUMP)) {
                certUtil.dumpCert((String) null);
            } else if (cl.hasOption(O_PRINT)) {
                certUtil.printCert((String) null);
            } else if (cl.hasOption(O_GET)) {
                String field = cl.getOptionValue(O_GET);
                CertField certField = ClientCertPrincipalMap.parseCertField(field);
                String value = certUtil.getCertField(certField);
                System.out.println(field + ": " + value);
            }

        } catch (Exception e) {
            e.printStackTrace();
            System.exit(EXIT_CODE_BAD);
        }
    }
}