Java tutorial
/* * The contents of this file are subject to the Mozilla Public * License Version 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express * or implied. See the License for the specific language governing * rights and limitations under the License. * * * The Original Code is OIOSAML Java Service Provider. * * The Initial Developer of the Original Code is Trifork A/S. Portions * created by Trifork A/S are Copyright (C) 2008 Danish National IT * and Telecom Agency (http://www.itst.dk). All Rights Reserved. * * Contributor(s): * Joakim Recht <jre@trifork.com> * Rolf Njor Jensen <rolf@trifork.com> * Aage Nielsen <ani@openminds.dk> * Carsten Larsen <cas@schultz.dk> * Kasper Vestergaard Mller<kvm@schultz.dk> */ package dk.itst.oiosaml.sp.metadata; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.InvalidAlgorithmParameterException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Security; import java.security.cert.*; import java.util.*; import java.util.concurrent.Callable; import dk.itst.oiosaml.logging.Logger; import dk.itst.oiosaml.logging.LoggerFactory; import org.apache.commons.configuration.Configuration; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.x509.*; import org.bouncycastle.asn1.x509.X509Extension; import org.bouncycastle.i18n.filter.UntrustedUrlInput; import org.bouncycastle.x509.extension.X509ExtensionUtil; import org.fishwife.jrugged.*; import org.opensaml.xml.security.x509.X509Credential; import dk.itst.oiosaml.configuration.SAMLConfigurationFactory; import dk.itst.oiosaml.error.Layer; import dk.itst.oiosaml.error.WrappedException; import dk.itst.oiosaml.logging.Audit; import dk.itst.oiosaml.logging.Operation; import dk.itst.oiosaml.security.CredentialRepository; import dk.itst.oiosaml.sp.metadata.IdpMetadata.Metadata; import dk.itst.oiosaml.sp.service.util.Constants; /** * Revocation of certificates are done using the follow methods. * * OCSP with Distribution Point from configuration. OCSP with Distribution Point * from certificates. CRL with Distribution Point from configuration. CRL with * Distribution Point from certificates. * * Methods are evaluated from top to bottom until a suitable method is found. In * case none of the methods are applicable a log entry will be generated * specifying the lack of CLR validation. * */ public class CRLChecker { private static final Logger log = LoggerFactory.getLogger(CRLChecker.class); private static final String AUTH_INFO_ACCESS = X509Extension.authorityInfoAccess.getId(); private static final CircuitBreakerFactory CIRCUIT_BREAKER_FACTORY = new CircuitBreakerFactory(); private Timer timer; public void checkCertificates(IdpMetadata metadata, final Configuration conf) { final long resetTime = conf.getLong(Constants.PROP_CIRCUIT_BREAKER_RESET_TIME_IN_SECONDS) * 1000L; final int attemptsBeforeOpening = conf.getInt(Constants.PROP_CIRCUIT_BREAKER_ATTEMPTS_BEFORE_OPENING); final long attemptsWithin = conf.getLong(Constants.PROP_CIRCUIT_BREAKER_ATTEMPTS_WITHIN_IN_SECONDS) * 1000L; final long delayBetweenAttempts = conf .getLong(Constants.PROP_CIRCUIT_BREAKER_DELAY_BETWEEN_ATTEMPTS_IN_SECONDS) * 1000L; final long certificatesRemainValidPeriod = conf .getLong(Constants.PROP_CERTIFICATES_REMAIN_VALID_PERIOD_IN_SECONDS) * 1000L; for (final String entityId : metadata.getEntityIDs()) { final Metadata md = metadata.getMetadata(entityId); for (final X509Certificate certificate : md.getAllCertificates()) { // Close circuit after 5 minutes. Open circuit if four or more attempts fails within 1 minute. CircuitBreaker circuitBreaker = CIRCUIT_BREAKER_FACTORY.createCircuitBreaker( certificate.getSubjectDN().toString(), new CircuitBreakerConfig(resetTime, new DefaultFailureInterpreter(attemptsBeforeOpening, attemptsWithin))); boolean errorState = false; // Always check once ... and continue to check if errors occur. Stop checking when circuit is open. do { try { if (circuitBreaker.invoke(new Callable<Boolean>() { public Boolean call() { return checkCertificate(conf, entityId, md, certificate); } })) { md.setCertificateValid(certificate, true); log.debug("Certificate validated successfully: " + certificate.getSubjectDN()); } else { md.setCertificateValid(certificate, false); log.debug("Certificate did not validate: " + certificate.getSubjectDN()); } errorState = false; // Stop while loop because call was successful. This is necessary if an exception first has been thrown. } catch (CircuitBreakerException cbe) { RevokeCertificateIfRemainValidPeriodIsExpired(md, certificate, cbe, certificatesRemainValidPeriod); errorState = false; // Stop while loop because circuit is open. } catch (Exception e) { RevokeCertificateIfRemainValidPeriodIsExpired(md, certificate, e, certificatesRemainValidPeriod); errorState = true; // Continue to try checking certificate. try { Thread.sleep(delayBetweenAttempts); // Wait 5 seconds and try again. } catch (InterruptedException e1) { // Do nothing } } } while (errorState); } } } private void RevokeCertificateIfRemainValidPeriodIsExpired(Metadata md, X509Certificate certificate, Exception e, long certificatesRemainValidPeriod) { final Date lastTimeForCertificationValidation = md.getLastTimeForCertificationValidation(certificate); // No need to check if certificate should be revoked if it is not in the valid certificates list. if (lastTimeForCertificationValidation != null) { final Date lastTimeForCertificationValidationPlusConfiguiredRemainValidPeriod = new Date( lastTimeForCertificationValidation.getTime() + certificatesRemainValidPeriod); final Date currentTime = new Date(); if (currentTime.before(lastTimeForCertificationValidationPlusConfiguiredRemainValidPeriod)) log.warn("Unexpected error while checking revocation of certificate. Certificate " + certificate.getSubjectDN() + " will remain valid until " + lastTimeForCertificationValidationPlusConfiguiredRemainValidPeriod + " if not validated successfully before.", e); else { log.error("Unexpected error while checking revocation of certificate. Certificate " + certificate.getSubjectDN() + " has been marked as revoked!!", e); md.setCertificateValid(certificate, false); } } } /** * First attempt is an OCSP check. If this fail the CRL check is used as fail over. * @param conf * @param entityId * @param md * @param certificate * @return */ private Boolean checkCertificate(Configuration conf, String entityId, Metadata md, X509Certificate certificate) { boolean validated = false; Exception error = null; try { log.debug("Checking if certificate with the following subject is revoked using OCSP: " + certificate.getSubjectDN()); validated = doOCSPCheck(conf, entityId, md, certificate); if (validated) log.info("Certificate with the following subject IS NOT marked as revoked using OCSP: " + certificate.getSubjectDN()); else log.info("Certificate with the following subject IS revoked using OCSP: " + certificate.getSubjectDN()); } catch (Exception e) { // OCSP check failed. Try CRL check. log.warn("Unexpected error while validating certificate using OCSP.", e); try { log.debug("Checking if certificate with the following subject is revoked using CRL: " + certificate.getSubjectDN()); validated = doCRLCheck(conf, entityId, md, certificate); if (validated) log.info("Certificate with the following subject IS NOT marked as revoked using CRL: " + certificate.getSubjectDN()); else log.info("Certificate with the following subject IS revoked using CRL: " + certificate.getSubjectDN()); } catch (Exception ex) { log.warn("Unexpected error while validating certificate using CRL.", e); error = ex; } } if (error != null) throw new WrappedException(Layer.BUSINESS, error); else return validated; } /** * Check the revocation status of a public key certificate using OCSP. * * @param conf * @param entityId * @param md * @param certificate * @return true if an OCSP check was completed, otherwise false. * @throws CertificateException */ private boolean doOCSPCheck(Configuration conf, String entityId, Metadata md, X509Certificate certificate) throws CertificateException, CertPathValidatorException, InvalidAlgorithmParameterException, IOException, NoSuchAlgorithmException { boolean revoked; String ocspServer = getOCSPUrl(conf, entityId, certificate); if (ocspServer == null) { final String message = "No OCSP access location could be found for " + entityId; log.debug(message); throw new RuntimeException(message); } log.debug("Starting OCSP validation of certificate " + certificate.getSubjectDN()); X509Certificate ca = getCertificateCA(conf); if (ca == null) { throw new RuntimeException("CA Certificate for OCSP check could not be retrieved!"); } // Create certificate chain List<X509Certificate> certList = new ArrayList<X509Certificate>(); certList.add(certificate); // certList.add(ca); CertPath cp; CertificateFactory cf; cf = CertificateFactory.getInstance("X.509"); cp = cf.generateCertPath(certList); // Enable OCSP Security.setProperty("ocsp.enable", "true"); Security.setProperty("ocsp.responderURL", ocspServer); try { TrustAnchor anchor = new TrustAnchor(ca, null); PKIXParameters params = new PKIXParameters(Collections.singleton(anchor)); params.setRevocationEnabled(true); // Validate and obtain results CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); cpv.validate(cp, params); log.debug("Certificate successfully validated during OCSP check."); revoked = false; } catch (CertPathValidatorException cpve) { if ("Certificate has been revoked".equals(cpve.getMessage())) { revoked = true; log.info("Certificate revoked, cert[" + cpve.getIndex() + "] :" + cpve.getMessage()); } else { log.error("Validation failure, cert[" + cpve.getIndex() + "] :" + cpve.getMessage()); throw cpve; } } if (!revoked) Audit.log(Operation.OCSPCHECK, false, entityId, "Revoked: NO"); else Audit.log(Operation.OCSPCHECK, false, entityId, "Revoked: YES"); return !revoked; } private X509Certificate getCertificateCA(Configuration conf) throws CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate ca = null; InputStream is = null; String caPath = conf.getString(Constants.PROP_OCSP_CA); try { if (caPath == null) { log.debug("CA certificate path is not configured"); return null; } log.debug("Fetching CA certificate located at: " + caPath); URL u = new URL(caPath); is = u.openStream(); ca = (X509Certificate) cf.generateCertificate(is); is.close(); } catch (CertificateException e) { log.error("Unable to read CA certficate from: " + caPath, e); return null; } catch (Exception e) { log.error("Unexpected error while reading CA certficate from: " + caPath, e); return null; } finally { if (is != null) { try { is.close(); } catch (IOException e) { } } } return ca; } /** * Gets an URL to use when performing an OCSP validation of a certificate. * * @param conf * @param entityId * @param certificate * @return the URL to use. * @see <a href="http://oid-info.com/get/1.3.6.1.5.5.7.48.1">http://oid-info.com/get/1.3.6.1.5.5.7.48.1</a> */ private String getOCSPUrl(Configuration conf, String entityId, X509Certificate certificate) { String url = conf.getString(Constants.PROP_OCSP_RESPONDER); if (url != null) { return url; } log.debug("No OCSP configured for " + entityId + " attempting to extract OCSP location from certificate " + certificate.getSubjectDN()); AuthorityInformationAccess authInfoAcc = null; ASN1InputStream aIn = null; try { byte[] bytes = certificate.getExtensionValue(AUTH_INFO_ACCESS); aIn = new ASN1InputStream(bytes); ASN1OctetString octs = (ASN1OctetString) aIn.readObject(); aIn = new ASN1InputStream(octs.getOctets()); ASN1Primitive auth_info_acc = aIn.readObject(); if (auth_info_acc != null) { authInfoAcc = AuthorityInformationAccess.getInstance(auth_info_acc); } } catch (Exception e) { log.debug("Cannot extract access location of OCSP responder.", e); return null; } finally { if (aIn != null) { try { aIn.close(); } catch (IOException e) { } } } List<String> ocspUrls = getOCSPUrls(authInfoAcc); Iterator<String> urlIt = ocspUrls.iterator(); while (urlIt.hasNext()) { // Just return the first URL Object ocspUrl = new UntrustedUrlInput(urlIt.next()); url = ocspUrl.toString(); } return url; } private List<String> getOCSPUrls(AuthorityInformationAccess authInfoAccess) { List<String> urls = new ArrayList<String>(); if (authInfoAccess != null) { AccessDescription[] ads = authInfoAccess.getAccessDescriptions(); for (int i = 0; i < ads.length; i++) { if (ads[i].getAccessMethod().equals(AccessDescription.id_ad_ocsp)) { GeneralName name = ads[i].getAccessLocation(); if (name.getTagNo() == GeneralName.uniformResourceIdentifier) { String url = ((DERIA5String) name.getName()).getString(); urls.add(url); } } } } return urls; } /** * Perform revocation check using CRL. * * @param conf * @param entityId * @param md * @param certificate * @return true if CRL check was completed and the certificate is not * revoked. */ private boolean doCRLCheck(Configuration conf, String entityId, Metadata md, X509Certificate certificate) throws IOException, CertificateException, CRLException, KeyStoreException, NoSuchAlgorithmException { boolean revoked = true; String url = getCRLUrl(conf, entityId, certificate); if (url == null) { final String message = "No CRL url could be found for " + entityId; log.debug(message); throw new RuntimeException(message); } InputStream is = null; try { URL u = new URL(url); is = u.openStream(); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509CRL crl = (X509CRL) cf.generateCRL(is); log.debug("CRL for " + url + ": " + crl); if (!checkCRLSignature(crl, certificate, conf)) { final String message = "CRL Signature could not be validated!!!"; Audit.log(Operation.CRLCHECK, false, entityId, message); throw new RuntimeException(message); } X509CRLEntry revokedCertificate = crl.getRevokedCertificate(certificate.getSerialNumber()); if (revokedCertificate != null) { log.debug("Certificate found in revocation list " + certificate.getSubjectDN()); revoked = true; } else revoked = false; } finally { if (is != null) { try { is.close(); } catch (IOException e) { } } } if (!revoked) Audit.log(Operation.CRLCHECK, false, entityId, "Revoked: NO"); else Audit.log(Operation.CRLCHECK, false, entityId, "Revoked: YES"); return !revoked; } /** * Get an URL to use when downloading CRL * * @param conf * @param entityId * @param certificate * @return the URL to use */ private String getCRLUrl(Configuration conf, String entityId, X509Certificate certificate) { String url = conf.getString(Constants.PROP_CRL + entityId); if (url != null) { return url; } log.debug("No CRL configured for " + entityId + " attempting to extract distribution point from certificate " + certificate.getSubjectDN()); byte[] val = certificate.getExtensionValue("2.5.29.31"); if (val != null) { try { CRLDistPoint point = CRLDistPoint.getInstance(X509ExtensionUtil.fromExtensionValue(val)); for (DistributionPoint dp : point.getDistributionPoints()) { if (dp.getDistributionPoint() == null) continue; if (dp.getDistributionPoint().getName() instanceof GeneralNames) { GeneralNames gn = (GeneralNames) dp.getDistributionPoint().getName(); for (GeneralName g : gn.getNames()) { if (g.getName() instanceof DERIA5String) { url = ((DERIA5String) g.getName()).getString(); } } } } } catch (IOException e) { log.debug("Cannot extract distribution point for certificate.", e); throw new RuntimeException(e); } } return url; } /** * Check whether a certificate revocation list (CRL) has a valid signature. * * @param crl * @param certificate * @param conf * @return true if signature is valid, otherwise false. * @throws IOException * @throws KeyStoreException * @throws IllegalStateException * @throws CertificateException * @throws NoSuchAlgorithmException * @throws WrappedException */ private boolean checkCRLSignature(X509CRL crl, X509Certificate certificate, Configuration conf) throws WrappedException, NoSuchAlgorithmException, CertificateException, IllegalStateException, KeyStoreException, IOException { if (conf.getString(Constants.PROP_CRL_TRUSTSTORE, null) == null) return true; CredentialRepository cr = new CredentialRepository(); cr.getCertificate(SAMLConfigurationFactory.getConfiguration().getKeystore(), conf.getString(Constants.PROP_CRL_TRUSTSTORE_PASSWORD), null); for (X509Credential cred : cr.getCredentials()) { try { crl.verify(cred.getPublicKey()); } catch (Exception e) { log.debug("CRL not signed by " + cred); return false; } } return true; } public void startChecker(long period, final IdpMetadata metadata, final Configuration conf) { if (timer != null) return; String proxyHost = conf.getString(Constants.PROP_HTTP_PROXY_HOST); String proxyPort = conf.getString(Constants.PROP_HTTP_PROXY_PORT); if (proxyHost != null && proxyPort != null) { log.debug("Enabling use of proxy " + proxyHost + " port " + proxyPort + " when checking revocation of certificates."); System.setProperty("http.proxyHost", proxyHost); System.setProperty("http.proxyPort", proxyPort); } log.info("Starting CRL checker, running with " + period + " seconds interval. Checking " + metadata.getEntityIDs().size() + " certificates"); timer = new Timer("CRLChecker"); timer.schedule(new TimerTask() { @Override public void run() { log.debug("Running CRL checker task"); try { checkCertificates(metadata, conf); } catch (Exception e) { log.error("Unable to run CRL checker", e); } } }, 1000L, 1000L * period); } public void stopChecker() { if (timer != null) { log.info("Stopping CRL checker"); timer.cancel(); timer = null; } } /** * Marks all certificates as valid without making any certificate check. * @param metadata contains the list of IdP certificates. */ public void setAllCertificatesValid(IdpMetadata metadata) { for (final String entityId : metadata.getEntityIDs()) { final Metadata md = metadata.getMetadata(entityId); for (final X509Certificate certificate : md.getAllCertificates()) { md.setCertificateValid(certificate, true); } } } }