com.vmware.identity.idm.server.clientcert.IdmClientCertificateValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.identity.idm.server.clientcert.IdmClientCertificateValidator.java

Source

/*
 *
 *  Copyright (c) 2012-2015 VMware, Inc.  All Rights Reserved.
 *
 *  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 com.vmware.identity.idm.server.clientcert;

import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.Validate;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1StreamParser;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERTaggedObject;

import com.vmware.identity.diagnostics.DiagnosticsLoggerFactory;
import com.vmware.identity.diagnostics.IDiagnosticsLogger;
import com.vmware.identity.idm.CertificatePathBuildingException;
import com.vmware.identity.idm.CertificateRevocationCheckException;
import com.vmware.identity.idm.ClientCertPolicy;
import com.vmware.identity.idm.IDMException;
import com.vmware.identity.idm.IdmCertificateRevokedException;
import com.vmware.identity.idm.IdmClientCertificateParsingException;
import com.vmware.identity.idm.InvalidArgumentException;
import com.vmware.identity.idm.InvalidPrincipalException;

public class IdmClientCertificateValidator {
    private static final IDiagnosticsLogger logger = DiagnosticsLoggerFactory
            .getLogger(IdmClientCertificateValidator.class);

    private static final int SUBALTNAME_TYPE_OTHERNAME = 0;

    private final ClientCertPolicy certPolicy;
    private final KeyStore trustStore;
    private final String tenantName;

    public IdmClientCertificateValidator(ClientCertPolicy certPolicy, String tenantName)
            throws InvalidArgumentException {
        Validate.notNull(certPolicy, "certPolicy");
        Validate.notEmpty(tenantName, "tenantName");

        this.certPolicy = certPolicy;
        this.tenantName = tenantName;

        trustStore = getTrustedClientCaStore();
    }

    /**
     * Validate certificate path governed by the cert (validation) policy.
     *
     * @param x509Certificate
     *            Client end certificate .
     * @param authStatExt
     *            AuthStat extensions for profiling the detailed steps.
     * @throws CertificateRevocationCheckException
     * @throws InvalidArgumentException
     *             ocsp url is missing when ocsp is enabled. This condition will
     *             be allowed if we support in-cert ocsp.
     * @throws IdmCertificateRevokedException
     * @throws CertificatePathBuildingException
     */
    public void validateCertificatePath(X509Certificate x509Certificate, String siteID,
            Map<String, String> authStatExt) throws CertificateRevocationCheckException, InvalidArgumentException,
            IdmCertificateRevokedException, CertificatePathBuildingException {
        IdmCertificatePathValidator checker = new IdmCertificatePathValidator(trustStore, certPolicy,
                this.tenantName, siteID);
        checker.validate(x509Certificate, authStatExt);
    }

    /**
     * validate the subject represent in the given client certificate. The
     * process uses following information in this order: 1. UPN in SAN,KeyStore
     * 2. Subject DN 3. Other alternative names in the SAN if enabled
     *
     * @param data
     *            DER encoded data
     * @return String UPN of the subject that the cert was issued to or throw
     *         exception.
     * @throws IdmClientCertificateParsingException
     *             Not able to extract UPN from the certificate.
     */

    public String extractUPN(X509Certificate clientCert)
            throws IdmClientCertificateParsingException, InvalidPrincipalException, IDMException {

        String upn = null;
        /**
         * UPN matching
         */
        logger.info("Extract and validating subject in client certificate");

        Collection<List<?>> altNames;
        try {
            altNames = clientCert.getSubjectAlternativeNames();
        } catch (CertificateParsingException e) {
            logger.error("No subject alternative name found in the cert.");
            throw new IdmClientCertificateParsingException("Error in finding cert SAN", e);
        }

        if (altNames == null) {
            logger.error("No subject alternative name found in the cert.");
            throw new IdmClientCertificateParsingException("Empty Subject Alternative Names");
        }

        // Examine each SAN entry for UPN that map to a registered principal
        for (List<?> altName : altNames) {
            Validate.isTrue(altName.size() > 1, "Invalid certicate SAN entry");
            Object altNameVal = altName.get(1);
            /*
             * Step 1. Get candidate UPN string
             * Expect UPN defined as "OtherName" type of SAN. Only upn will be returned as a byte
             * array.
             */
            if (Integer.valueOf(IdmClientCertificateValidator.SUBALTNAME_TYPE_OTHERNAME).equals(altName.get(0))
                    && altNameVal instanceof byte[]) {

                byte[] altNameValByte = (byte[]) altNameVal;

                try {
                    upn = parseDERString(altNameValByte);
                } catch (Throwable t) {
                    throw new IdmClientCertificateParsingException(
                            "Failed to parse SAN entry with \'OtherName\' type.", t);
                }
            } else {
                /*
                 * Unknown type. we are not parsing this SAN entry
                 */
                String skippedAltName = null;
                if (altNameVal instanceof String) {
                    skippedAltName = (String) altNameVal;
                } else if (altNameVal instanceof byte[]) {
                    skippedAltName = new String((byte[]) altNameVal);
                }
                logger.debug("Skipping SAN entry of type " + altName.get(0) + " with value: " + skippedAltName);
            }

            if (upn != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Successfully extracted UPN from SAN entry:" + upn);
                }
                break;
            }
        }

        /*
         * if no UPN found in SAN. Next, TBD: should we try the Subject DN's
         * X500Principal Note: if UPN is found in SAN but matching to user
         * failed, we do not look farther.
         */
        if (upn == null) {
            throw new IdmClientCertificateParsingException("No UPN entry in Subject Alternative Names extension");
        }

        return upn;
    }

    /**
     * Parse DER-encoded bytes to locate a String object
     *
     * @param alterNameValue DER encoded data
     * @return First string found
     * @throws Throwable
     */
    private static String parseDERString(byte[] alterNameValue) throws Throwable {
        try {
            ASN1StreamParser p = new ASN1StreamParser(alterNameValue);
            ASN1Encodable d = p.readObject();
            ASN1Primitive der = d.toASN1Primitive();

            return getStringFromObject(der);
        } catch (Throwable e) {
            // Exception indicates parsing failed, skip this
            // value (most likely not UPN format)
            logger.error("Unable to extract User Principal Name: " + e.getMessage());
            throw e;
        }
    }

    /**
    * Find any DER-encoded String inside a DER Object of unknown type
    *
    * @param derObj
    * @return The string value inside this object, or null if none is found
    */
    private static String getStringFromObject(ASN1Primitive derObj) {
        if (derObj instanceof DERSequence) {
            return getStringFromSequence((DERSequence) derObj);
        } else if (derObj instanceof DERObjectIdentifier) {
            return null;
        } else if (derObj instanceof DERTaggedObject) {
            return getStringFromTaggedObject((DERTaggedObject) derObj);
        } else if (derObj instanceof ASN1String) {
            logger.trace("String of type " + derObj.getClass().getName());
            return ((ASN1String) derObj).getString();
        } else {
            logger.warn(
                    "Unexpected DER type, ignoring (" + derObj.getClass().getName() + "): " + derObj.toString());
        }
        return null;
    }

    /**
     * Find any DER-encoded String inside a Tagged Object
     *
     * @param taggedObj
     * @return The string value inside this sequence, or null if none is found
     */
    private static String getStringFromTaggedObject(DERTaggedObject taggedObj) {
        if (null == taggedObj)
            return null;

        return getStringFromObject(taggedObj.getObject());
    }

    /**
     * Find any DER-encoded String inside a DER sequence
     *
     * @param derSeq
     * @return The first string value inside this sequence, or null if none is found
     */
    private static String getStringFromSequence(DERSequence derSeq) {
        if (null == derSeq)
            return null;

        Enumeration<?> objects = derSeq.getObjects();
        while (objects.hasMoreElements()) {
            ASN1Primitive o = (ASN1Primitive) objects.nextElement();
            String retVal = getStringFromObject(o);
            if (null != retVal) {
                return retVal;
            }
        }
        return null;
    }

    /**
     *
     * @return keyStore representing that containing the trust CA certificates of the tenant
     * @throws InvalidArgumentException
     */
    private KeyStore getTrustedClientCaStore() throws InvalidArgumentException {
        KeyStore trustedClientCaStore;

        if (certPolicy == null || certPolicy.getTrustedCAs() == null) {
            throw new InvalidArgumentException("Null client certificate policy or trust ca certficagtes.");
        }
        try {
            trustedClientCaStore = KeyStore.getInstance(KeyStore.getDefaultType());
        } catch (KeyStoreException e1) {
            throw new InvalidArgumentException("Failed in creating a keyStore instance: ", e1);
        }
        try {
            trustedClientCaStore.load(null, null);
        } catch (NoSuchAlgorithmException | CertificateException | IOException e1) {
            throw new InvalidArgumentException("Failed in initializing a keyStore instance: " + e1.getMessage(),
                    e1);
        }
        for (Certificate trustCa : certPolicy.getTrustedCAs()) {
            X509Certificate x509Cert = (X509Certificate) trustCa;
            try {
                trustedClientCaStore.setCertificateEntry(x509Cert.getSubjectX500Principal().getName(), trustCa);
            } catch (KeyStoreException e) {
                throw new InvalidArgumentException("Failed in storing a ca cert to keyStore: " + e.getMessage(), e);
            }
        }
        return trustedClientCaStore;
    }

}