Java tutorial
/* * * 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.net.URL; import java.security.InvalidAlgorithmParameterException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Security; import java.security.cert.CertPath; import java.security.cert.CertPathBuilder; import java.security.cert.CertPathBuilderException; import java.security.cert.CertPathBuilderResult; import java.security.cert.CertPathValidator; import java.security.cert.CertPathValidatorException; import java.security.cert.CertPathValidatorResult; import java.security.cert.CertStore; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.PKIXBuilderParameters; import java.security.cert.PKIXParameters; import java.security.cert.X509CRL; import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.Validate; import sun.security.x509.CRLDistributionPointsExtension; import sun.security.x509.DistributionPoint; import sun.security.x509.GeneralName; import sun.security.x509.X509CertImpl; import com.vmware.identity.diagnostics.DiagnosticsLoggerFactory; import com.vmware.identity.diagnostics.IDiagnosticsLogger; import com.vmware.identity.idm.AlternativeOCSP; import com.vmware.identity.idm.AlternativeOCSPList; import com.vmware.identity.idm.CertRevocationStatusUnknownException; import com.vmware.identity.idm.CertificatePathBuildingException; import com.vmware.identity.idm.CertificateRevocationCheckException; import com.vmware.identity.idm.CrlDownloadException; import com.vmware.identity.idm.IdmCertificateRevokedException; import com.vmware.identity.idm.ClientCertPolicy; import com.vmware.identity.idm.InvalidArgumentException; import com.vmware.identity.idm.server.ThreadLocalProperties; /** * Class for client certificate validation * @author schai * */ public class IdmCertificatePathValidator { private static final IDiagnosticsLogger logger = DiagnosticsLoggerFactory .getLogger(IdmCertificatePathValidator.class); private static final String PREFIX_URI_NAME = "URIName: "; private final KeyStore trustStore; private String tenantName; private String siteID; //for alternative OCSP responder collection. private final ClientCertPolicy certPolicy; /** * @param trustStore * @param certPolicy * @param tenantName * @throws CertificateRevocationCheckException */ public IdmCertificatePathValidator(KeyStore trustStore, ClientCertPolicy certPolicy, String tenantName, String siteID) { this.trustStore = trustStore; this.siteID = siteID; Validate.notNull(certPolicy, "Cert Policy"); Validate.notNull(trustStore, "Trust Store"); Validate.notEmpty(tenantName, "tenantName"); this.tenantName = tenantName; this.certPolicy = certPolicy; } /** * Revocation check function 1. use ocsp first if it is enabled 2. fail if * the cert is revoked 3. Fall back to CRL if ocsp fails for reason other * then revoked 4. CRL validation using provided URL and in-cert URL * * Note:OCSP nonce extension appears not currently controllable in Java's * default OCSPChecker. * * @param certs * Client cert chain. It could be a leaf certificate, a partial or a * full chain including root CA. * Current implementation only relies on leaf certificate and use it to build certificate path then validate it. * @param authStatExt * AuthStat extensions for profiling the detailed steps. * @throws CertificateRevocationCheckException unable to validate revocation status. * @throws IdmCertificateRevokedException certificate revoked * @throws InvalidArgumentException * @throws CertificatePathBuildingException cert path building error of any reasons: such as expired cert, etc. */ public void validate(X509Certificate cert, Map<String, String> authStatExt) throws CertificateRevocationCheckException, IdmCertificateRevokedException, InvalidArgumentException, CertificatePathBuildingException { if (null == cert) { throw new InvalidArgumentException("No certs to validate."); } if (logger.isDebugEnabled()) { logger.debug("Certificate policy: " + this.certPolicy.toString()); logger.debug("Checking revocation for certificate: " + cert.getSubjectDN()); } // Build the certpath long startTime = System.nanoTime(); CertPath certPath = buildCertPath(cert); authStatExt.put("buildCertPath", String.format("%d Ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime))); startTime = System.nanoTime(); // Validate certpath validateCertPath(certPath); authStatExt.put("validateCertPath", String.format("%d Ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime))); logger.info("Successfully validated client certificate : " + cert.getSubjectDN()); } /** * build and validate cert path from end certificate. * * Note: the certpath return seems only include intermediate CA unless there is none in * which case the end cert is returned. * @param endCert * @return CertPath never null * @throws CertificatePathBuildingException */ private CertPath buildCertPath(X509Certificate endCert) throws CertificatePathBuildingException { CertPathBuilder cpb = null; try { cpb = CertPathBuilder.getInstance("PKIX"); } catch (NoSuchAlgorithmException e) { throw new CertificatePathBuildingException("Error building CertPathBuilder:" + e.getMessage(), e); } PKIXBuilderParameters params = CreatePKIXBuilderParameters(endCert); CertPathBuilderResult cpbResult; try { cpbResult = cpb.build(params); } catch (CertPathBuilderException e) { throw new CertificatePathBuildingException(e.getMessage(), e.getCause()); } catch (InvalidAlgorithmParameterException e) { throw new CertificatePathBuildingException(e.getMessage(), e); } CertPath cp = cpbResult.getCertPath(); return cp; } /** * Create and init PKIXBuilderParameters for CertPathBuilder. * * @param endCert * the target user certificate to use for building certificate * path * @return * @throws CertificatePathBuildingException */ private PKIXBuilderParameters CreatePKIXBuilderParameters(X509Certificate endCert) throws CertificatePathBuildingException { X509CertSelector targetConstraints = new X509CertSelector(); targetConstraints.setCertificate(endCert); PKIXBuilderParameters params; try { params = new PKIXBuilderParameters(trustStore, targetConstraints); // Do not validate the certificate at cert path building stage. // This would result in unknown failures. params.setRevocationEnabled(false); } catch (KeyStoreException e) { throw new CertificatePathBuildingException( "Error creating PKIXBuilderParameters: Please check trust store" + e.getMessage(), e); } catch (InvalidAlgorithmParameterException e) { throw new CertificatePathBuildingException("Error creating PKIXBuilderParameters:" + e.getMessage(), e); } catch (Throwable e) { // have this block in case a new type of error was thrown throw new CertificatePathBuildingException("Error creating PKIXBuilderParameters:" + e.getMessage(), e); } Collection<Object> certCollection = new ArrayList<Object>(); // add trusted CAs to the collection addCertificateCandidates(endCert, certCollection); if (!certCollection.isEmpty()) { try { CertStore certStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(certCollection)); params.addCertStore(certStore); } catch (InvalidAlgorithmParameterException e) { throw new CertificatePathBuildingException( "Error creating CertStore for PKIXBuilderParameters:" + e.getMessage(), e); } catch (NoSuchAlgorithmException e) { throw new CertificatePathBuildingException( "Error creating CertStore for PKIXBuilderParameters:" + e.getMessage(), e); } } else { logger.debug("Revocation check: CRL list empty"); } return params; } /** * Adding potential certificates to the collection to be used to build CertStore which * is used in building certificate path. * * @param userCert * @param certCollection certificates to be used for cert path building. * @throws CertificateRevocationCheckException */ private void addCertificateCandidates(X509Certificate userCert, Collection<Object> certCollection) throws CertificatePathBuildingException { // adding end user cert provided certCollection.add(userCert); // adding trusted ca certificats in the trust store. try { Enumeration<String> certAliases = this.trustStore.aliases(); while (certAliases.hasMoreElements()) { String alias = certAliases.nextElement(); certCollection.add(this.trustStore.getCertificate(alias)); } } catch (KeyStoreException e) { throw new CertificatePathBuildingException("Bad trustStore!", e); } } /** * Set a ThreadLocal property to System property. * * @param key * @param val * @throws CertificateRevocationCheckException */ private void setThreadLocalSystemProperty(String key, String val) throws CertificateRevocationCheckException { if (System.getProperties() instanceof ThreadLocalProperties) { ThreadLocalProperties properties = (ThreadLocalProperties) System.getProperties(); properties.setThreadLocalProperty(key, val); System.setProperties(properties); } else { throw new CertificateRevocationCheckException( "System properties was not initialized to ThreadLocalProperties"); } } /** * Perform certificate path validation for the given certificate path. * * @param certPath certificate path to be validated. * @param params PKIX Parameters for CertPathValidator * @throws CertificateRevocationCheckException error that prevent the function return the status * of the cert * @throws IdmCertificateRevokedException CRL report the cert is revoked */ private void validateCertPath(CertPath certPath) throws CertificateRevocationCheckException, IdmCertificateRevokedException { //loop each alternative OCSP configured for the site HashMap<String, AlternativeOCSPList> ocspSiteMap = this.certPolicy.get_siteOCSPList(); AlternativeOCSPList altOCSPList = null; if (null != ocspSiteMap) { Validate.notEmpty(this.siteID, "siteID"); altOCSPList = ocspSiteMap.get(this.siteID); } List<AlternativeOCSP> ocspCollection = null; if (null != altOCSPList) { ocspCollection = altOCSPList.get_ocspList(); } Collection<Object> crlCollection = new ArrayList<Object>(); setupCRLOptions(crlCollection, certPath); //Creates CertStore that contains all alternative OCSP responder signing certificates for PKIXParameter to consume. CertStore certStore = createCertStoreForRevChecking(ocspCollection); if (null != ocspCollection && ocspCollection.size() > 0 && this.certPolicy.useOCSP()) { Iterator<AlternativeOCSP> iter = ocspCollection.iterator(); while (iter.hasNext()) { AlternativeOCSP altOCSP = iter.next(); try { validateCertPath(certPath, crlCollection, certStore, altOCSP); break; //certificate is validated. } catch (CertRevocationStatusUnknownException e) { if (!iter.hasNext()) { throw e; //rethrow if no more alternative responder in the collection. } //else continue } } } else { validateCertPath(certPath, crlCollection, certStore, null); } } /** * Add alternative OCSP signing certs to the give collection. * @param certCollection * @param ocspCollection * @throws CertificateRevocationCheckException */ private CertStore createCertStoreForRevChecking(Collection<AlternativeOCSP> ocspCollection) throws CertificateRevocationCheckException { Collection<Object> certCollection = new ArrayList<Object>(); if (null != ocspCollection) { for (AlternativeOCSP altOCSP : ocspCollection) { X509Certificate cert = altOCSP.get_responderSigningCert(); if (null != cert) { certCollection.add(cert); } } } else { //look for old place X509Certificate cert = this.certPolicy.getOCSPResponderSigningCert(); if (null != cert) { certCollection.add(cert); } } try { return CertStore.getInstance("Collection", new CollectionCertStoreParameters(certCollection)); } catch (Exception e) { throw new CertificateRevocationCheckException("Unable to create cert store." + e.getMessage(), e); } } /** * Validate the certificate path using a provided OCSP responder configuration. * * @param certPath required * @param crlCollection * @param certStore null possible cert store for PKIX param * @param altOCSP null possible * @throws CertificateRevocationCheckException * @throws IdmCertificateRevokedException */ private void validateCertPath(CertPath certPath, Collection<Object> crlCollection, CertStore certStore, AlternativeOCSP altOCSP) throws CertificateRevocationCheckException, IdmCertificateRevokedException { setupOCSPOptions(certPath, altOCSP); PKIXParameters params = createPKIXParameters(crlCollection); if (null != certStore) { params.addCertStore(certStore); } CertPathValidator certPathValidator; try { certPathValidator = CertPathValidator.getInstance("PKIX"); } catch (NoSuchAlgorithmException e) { throw new CertificateRevocationCheckException("Error getting PKIX validator instance:" + e.getMessage(), e); } try { String pkiParam = params.toString(); logger.trace("**Certificate Path Validation Parameters trust anchors **\n" + params.getTrustAnchors().toString() + "\n"); logger.trace("**Certificate Path Validation Parameters **\n" + pkiParam + "\n"); CertPathValidatorResult result = certPathValidator.validate(certPath, params); logger.trace("**Certificate Path Validation Result **\n" + result.toString() + "\n"); } catch (CertPathValidatorException e) { if (e.getReason() == CertPathValidatorException.BasicReason.REVOKED) { throw new IdmCertificateRevokedException("CRL shows certificate status as revoked"); } else if (e.getReason() == CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS) { throw new CertRevocationStatusUnknownException( "CRL checking could not determine certificate status."); } throw new CertificateRevocationCheckException("Certificate path validation failed:" + e.getMessage(), e); } catch (InvalidAlgorithmParameterException e) { throw new CertificateRevocationCheckException( "Certificate validation parameters invalid, could not validate certificate path:" + e.getMessage(), e); } } /** * Set up sun validator OCSP options for revocation checking. * * The following options will be set here: * "ocsp.enable" * "ocsp.responderURL" * "ocsp.responderCertIssuerName" * "ocsp.responderCertSerialNumber" * * * The ocsp controls currently are not thread-safe in the sense that multi-tenant usage of the feature could override * each other. Note: DOD does not ask for multitenancy. PR 1417152. * @param CertPath target certificate path for validation. * @param AlternativeOCSP alternative responder replacing default certificate OCSP. * * @throws CertificateRevocationCheckException */ private void setupOCSPOptions(CertPath certPath, AlternativeOCSP altOcsp) throws CertificateRevocationCheckException { if (this.certPolicy.revocationCheckEnabled()) { if (this.certPolicy.useOCSP()) { Security.setProperty("ocsp.enable", "true"); if (altOcsp != null) { setupOCSPResonderConfig(altOcsp.get_responderURL(), altOcsp.get_responderSigningCert()); } else { //backward compatibility handling setupOCSPResonderConfig(this.certPolicy.getCRLUrl(), this.certPolicy.getOCSPResponderSigningCert()); } } else { Security.setProperty("ocsp.enable", "false"); } } //else none of these setting matters. } /** * Set up sun validator CRL options for revocation checking. * * If revocation is turned on, the following options will be set here: * "com.sun.security.enableCRLDP" to false. Also this function adds CRL's to the provided collection. * * @param certCollection in/out. CRL to be set up for validating the status of the certificate. * @param CertPath target certificate path for validation. * * @throws CertificateRevocationCheckException */ private void setupCRLOptions(Collection<Object> crlCollection, CertPath certPath) throws CertificateRevocationCheckException { /** * Extract or add CRLs to working list of CRLimpl Setup * up revocation check related java property */ if (this.certPolicy.revocationCheckEnabled()) { boolean enableCRLChecking = (this.certPolicy.useOCSP() && !this.certPolicy.useCRLAsFailOver()) ? false : true; //get custom CRL URL customCrlUri = this.certPolicy.getCRLUrl(); if (enableCRLChecking == true && customCrlUri != null) { try { addCRLToWorkingList(customCrlUri.toString(), crlCollection); } catch (CrlDownloadException e) { throw new CertificateRevocationCheckException( "Failed to download CRL from custom CRL URI: " + customCrlUri.toString(), e); } } //get CRLs from certpath CRLDP, use cache if available. Turn off sun security CRLDP checking since we extract them as override if (enableCRLChecking && this.certPolicy.useCertCRL()) { List<? extends java.security.cert.Certificate> certList = certPath.getCertificates(); if (certList == null) { return; } Iterator<? extends java.security.cert.Certificate> it = certList.iterator(); while (it.hasNext()) { try { addCertCRLsToWorkingList((X509Certificate) it.next(), crlCollection); } catch (CertificateRevocationCheckException e) { //Not able to get any of CRLDP from this certificate. Throw if no custom CRL and OCSP is not enabled if (customCrlUri == null && this.certPolicy.useOCSP() == false) { throw new CertificateRevocationCheckException("CRL download failure. ", e); } } } } setThreadLocalSystemProperty("com.sun.security.enableCRLDP", "false"); } //else none of these setting matters. } private void setupOCSPResonderConfig(URL ocspURL, X509Certificate signingCert) { if (ocspURL != null) { Security.setProperty("ocsp.responderURL", ocspURL.toString()); } if (signingCert != null) { //Setup ocsp.responderCertSubjectName if (null != signingCert) { String subjectDN = signingCert.getSubjectX500Principal().getName(); Validate.notEmpty(subjectDN, "Null or empty SubjectX500Principal name extracted from alternative OCSP responder signing cert."); Security.setProperty("ocsp.responderCertSubjectName", subjectDN); } } } /** * Create parameters for CertPathValidator using PKIX algorithm. * * The parameter object was defined with given trustStore and CRL collection * @param trustStore2 * @return non-null PKIXParameters * @throws CertificateRevocationCheckException */ private PKIXParameters createPKIXParameters(Collection<Object> crlCollection) throws CertificateRevocationCheckException { PKIXParameters params = null; try { Validate.notNull(trustStore, "TrustStore can not be null."); params = new PKIXParameters(trustStore); if (this.certPolicy.revocationCheckEnabled()) { params.setRevocationEnabled(true); } else { params.setRevocationEnabled(false); } } catch (KeyStoreException e) { throw new CertificateRevocationCheckException( "Error creating validator parameters: Please check trust store" + e.getMessage(), e); } catch (InvalidAlgorithmParameterException e) { throw new CertificateRevocationCheckException("Error creating validator parameters:" + e.getMessage(), e); } catch (Throwable e) { //have this block in case a new type of error was thrown throw new CertificateRevocationCheckException("Error creating validator parameters:" + e.getMessage(), e); } if (!crlCollection.isEmpty()) { try { CertStore crlStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(crlCollection)); params.addCertStore(crlStore); } catch (InvalidAlgorithmParameterException e) { throw new CertificateRevocationCheckException( "Error adding CRLs to validating parameters:" + e.getMessage(), e); } catch (NoSuchAlgorithmException e) { throw new CertificateRevocationCheckException( "Error adding CRLs to validating parameters:" + e.getMessage(), e); } } else { logger.debug("Revocation check: CRL list empty"); } // setup certificate policy white list String[] oidWhiteList = this.certPolicy.getOIDs(); if (oidWhiteList != null && oidWhiteList.length > 0) { Set<String> oidSet = new HashSet<String>(); for (String oid : oidWhiteList) { oidSet.add(oid); } params.setInitialPolicies(oidSet); params.setExplicitPolicyRequired(true); } return params; } /** * Extract certificate CRLs and adding them to the working map of CRLImpl for cert path * validation. Use cached copy if available. * * @param leafCert * @param crlCollection crl collection that to contain any CRL found from the certificate or cached copy * @return void if at least one CRLDP URI result in accessible CRL. * @throws CertificateRevocationCheckException This exception is throw if one of the following occurs: * a) Failure in retrieve all f non-LDAP CRLDP (no cached copy and unable to download in realtime). * b) CRLDistributionPointsExtension.get fails. currently this happens only if we had passed a invalid * access point constant. We propagate this error. */ // @SuppressWarnings("unchecked") private void addCertCRLsToWorkingList(X509Certificate leafCert, Collection<Object> crlCollection) throws CertificateRevocationCheckException { if (logger.isDebugEnabled()) { logger.debug("IdmCertificatePathValidator.addCertCRLsToWorkingList(): Adding CRLs from CRLDP"); } String error = null; boolean atLeastOneCrlAdded = false; X509CertImpl certImpl = (X509CertImpl) leafCert; CRLDistributionPointsExtension crlDistributionPointsExt = certImpl.getCRLDistributionPointsExtension(); if (null == crlDistributionPointsExt) { //no distribution points found return; } try { for (DistributionPoint distribPoint : (List<DistributionPoint>) crlDistributionPointsExt .get(CRLDistributionPointsExtension.POINTS)) { for (GeneralName crlGeneralName : distribPoint.getFullName().names()) { String crlGeneralNameString = crlGeneralName.toString(); if (crlGeneralNameString.startsWith(PREFIX_URI_NAME)) { String crlURLString = crlGeneralNameString.substring(PREFIX_URI_NAME.length()); try { addCRLToWorkingList(crlURLString, crlCollection); atLeastOneCrlAdded = true; } catch (CrlDownloadException e) { if (logger.isDebugEnabled()) { logger.debug("No cached copy and failed to download CRL" + e.getMessage()); } //continue fetching remaining crl in case of error if (error == null) { error = String.format( "Unable to obtain CRL from certificate at following distribution points: %s", crlURLString); } else { error += String.format(error + ", %s", crlURLString); } } } } } } catch (IOException e) { logger.error("IOException in accessing CRLDP" + e.getMessage()); throw new CertificateRevocationCheckException( "IOException in calling CRLDistributionPointsExtension.get()"); } if (error != null) { logger.warn(error); if (!atLeastOneCrlAdded) { throw new CertificateRevocationCheckException(error); } } } /** * * Adding a CRL to the working list to be used for certificate path validation. * * Use the cached copy if available. Otherwise, download and add to CRL cache. LDAP URI is not supported. * Note: TenantCrlCache refresh cached CRL periodically. Authentication thread does not download CRL if there is a cached copy. * * @param crlURLString * @param crlCollection Crl collection passed in to contains accumulative result. * @throws CrlDownloadException if no crl was added due to downloading failure * @return void */ private void addCRLToWorkingList(String crlURLString, Collection<Object> crlCollection) throws CrlDownloadException { if (logger.isDebugEnabled()) { logger.debug("Adding CRL: " + crlURLString); } X509CRL crlImpl = null; IdmCrlCache crlCache = TenantCrlCache.get().get(this.tenantName); //add crl cache for the tenant if it does not exist. if (crlCache == null) { crlCache = new IdmCrlCache(); TenantCrlCache.get().put(this.tenantName, crlCache); } crlImpl = crlCache.get(crlURLString); //If the crl is not cached, we download one and then add to the cache. if (crlImpl == null) { crlImpl = IdmCrlCache.downloadCrl(crlURLString); if (null != crlImpl) { crlCache.put(crlURLString, crlImpl); } } if (crlImpl != null) { crlCollection.add(crlImpl); } } }