/* * 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 Java RASP toolkit. * * The Initial Developer of the Original Code is Lenio. Portions * created by Lenio are Copyright (C) 2007 Danish National IT and * Telecom Agency (http://www.itst.dk). All Rights Reserved. */ package dk.gov.oiosi.security.revocation.ocsp; import dk.gov.oiosi.common.OutVariable; import dk.gov.oiosi.common.cache.ICache; import dk.gov.oiosi.configuration.CacheFactory; import dk.gov.oiosi.configuration.ConfigurationException; import dk.gov.oiosi.configuration.ConfigurationHandler; import dk.gov.oiosi.security.CertificateHandlingException; import dk.gov.oiosi.security.oces.CertificateUtil; import dk.gov.oiosi.security.revocation.IRevocationLookup; import dk.gov.oiosi.security.revocation.RevocationException; import dk.gov.oiosi.security.revocation.RevocationResponse; import dk.gov.oiosi.security.revocation.crl.CrlLookup; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; import org.bouncycastle.asn1.x509.*; import org.bouncycastle.ocsp.*; import javax.security.auth.x500.X500Principal; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URISyntaxException; import java.net.URL; import java.security.NoSuchProviderException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; /** * Class for checking certificate revocation status against an OCSP server. *

* http://www.nets.eu/dk-da/Service/kundeservice/nemid-tu/NemID-tjenesteudbyderpakken-okt-2014/Documents/Specifikationsdokument%20for%20OCSP.pdf *

* TODO DLK: A lot of code in this class uses deprecated API. We need to review it, usually it is a bad sign. */ @SuppressWarnings("deprecation") public class OcspLookup implements IRevocationLookup { private final Log log = LogFactory.getLog(OcspLookup.class); /** * Root certificates. */ private final HashMap rootCertificateMap; /** * The ocsp cache */ // Set cache time to one hour private final ICache ocspCache = CacheFactory.getInstance().getOcspLookupCache(); private OcspConfig configuration; private CertificateUtil certificateUtil; /** * Default constructor. Attempts to load configuration from configuration file. * * @throws ConfigurationException On error... */ public OcspLookup() throws ConfigurationException { try { configuration = ConfigurationHandler.getInstance().getOcspConfig(); } catch (URISyntaxException e) { throw new ConfigurationException(e.getMessage()); } rootCertificateMap = new HashMap<>(); try { List list = configuration.getDefaultOcesRootCertificateCollectionFromStore(); for (X509Certificate rootCertificate : list) { rootCertificateMap.put(rootCertificate.getIssuerDN().getName(), rootCertificate); } } catch (CertificateHandlingException e) { log.error(e.getMessage(), e); } certificateUtil = new CertificateUtil(); } /** * Instantiates OcspLookup and loads the OCES default root certificate. * * @param configuration Configuration parameters */ public OcspLookup(OcspConfig configuration) { this.configuration = configuration; rootCertificateMap = new HashMap<>(); try { List list = this.configuration.getDefaultOcesRootCertificateCollectionFromStore(); for (X509Certificate rootCertificate : list) { rootCertificateMap.put(rootCertificate.getIssuerDN().getName(), rootCertificate); } } catch (CertificateHandlingException e) { log.error(e.getMessage(), e); } certificateUtil = new CertificateUtil(); } /** * Creates a new OcspLookup class * * @param conf The configuration to use in this lookup * @param defaultRootCertificate The root certificate */ public OcspLookup(OcspConfig conf, X509Certificate defaultRootCertificate) { this.configuration = conf; this.rootCertificateMap = new HashMap<>(); //this.rootcertList = new ArrayList(); this.rootCertificateMap.put(defaultRootCertificate.getIssuerDN().getName(), defaultRootCertificate); this.certificateUtil = new CertificateUtil(); } /** * Creates a new OcspLookup class * * @param conf The configuration to use in this lookup * @param defaultRootCertificateList The root certificate */ public OcspLookup(OcspConfig conf, ArrayList defaultRootCertificateList) { this.configuration = conf; rootCertificateMap = new HashMap<>(); for (X509Certificate rootCertificate : defaultRootCertificateList) { rootCertificateMap.put(rootCertificate.getIssuerDN().getName(), rootCertificate); } this.certificateUtil = new CertificateUtil(); } /** * Gets the configuration of the lookup client. * * @return OCSP configuration */ public OcspConfig getConfiguration() { return this.configuration; } /** * Checks the certificate status on a OCSP server. * * @param certificate The certificate to check. * @return The RevocationResponse object that contains the result. */ public RevocationResponse checkCertificate(X509Certificate certificate) throws RevocationException { OutVariable value = new OutVariable<>(); RevocationResponse revocationResponse; if (ocspCache.tryGetValue(certificate.getSubjectX500Principal(), value)) { // response already in cache. // Check if the response still is valid Date now = new Date(); revocationResponse = value.getVariable(); if (revocationResponse.getNextUpdate().before(now)) { // the cached value is to old, get new value revocationResponse = revocationResponse(certificate); // the cached value is to old // remove it from the cache /*this.ocspCache.remove(certificate.getSubjectX500Principal()); // get new value revocationResponse = this.checkCertificateOnline(certificate); if(revocationResponse != null) { // put revocationResponse in cache ocspCache.set(certificate.getSubjectX500Principal(), revocationResponse); } */ } } else { // response is not in cache revocationResponse = revocationResponse(certificate); /*if(revocationResponse != null) { // put revocationResponse in cache ocspCache.set(certificate.getSubjectX500Principal(), revocationResponse); } */ } return revocationResponse; } public RevocationResponse checkCertificateAsync(X509Certificate certificate) throws RevocationException { // Async not implemented - it is in c# return revocationResponse(certificate); } public RevocationResponse revocationResponse(X509Certificate certificate) throws RevocationException { // this method can be call recursive, so check the cache first RevocationResponse revocationResponse; OutVariable value = new OutVariable<>(); if (ocspCache.tryGetValue(certificate.getSubjectX500Principal(), value)) { // response already in cache. // Check if the response still is valid Date now = new Date(); revocationResponse = value.getVariable(); if (revocationResponse.getNextUpdate().before(now)) { // the cached value is to old, get new value from online check revocationResponse = revocationResponseOnline(certificate); } } else { // response is not in cache revocationResponse = revocationResponseOnline(certificate); } return revocationResponse; } /** * Checks the certificate status on a OCSP server. * * @param x509Certificate - The certificate to check. * @return The RevocationResponse object that contains the result. * @throws RevocationException On error... */ public RevocationResponse revocationResponseOnline(X509Certificate x509Certificate) throws RevocationException { // The response was not in the cache - we must validate the certificate online. if (x509Certificate == null) { throw new RevocationException("Certificate is null"); } X509Certificate issuerX509Certificate = findIssuerCertificate(x509Certificate); if (issuerX509Certificate == null) { throw new RevocationException("Certificate '" + x509Certificate.getSubjectX500Principal() + "' is not trusted, as issuer could not be identified"); } RevocationResponse revocationResponse; // Is the certificate that we are about to verify, the root certificate itself? if (x509Certificate.getIssuerDN().getName().equals(issuerX509Certificate.getSubjectX500Principal().getName())) { throw new RevocationException("Certificate not trusted, as the certificate is self-signed"); } else { revocationResponse = revocationResponseOnline(x509Certificate, issuerX509Certificate); if (revocationResponse != null && revocationResponse.isValid()) { // Now we know the certificate is valid. // If the issuer is a trusted root certificate, all is good if (rootCertificateMap.containsKey(issuerX509Certificate.getIssuerDN().getName())) { // the root certificate is trusted, so the RevocationResponse can be put on the cache ocspCache.add(x509Certificate.getSubjectX500Principal(), revocationResponse); } else { // we do not yet know if the certificate is valid. // the certificate might be good, but if the issuing certificate is revoked, // then the certificate should also be revoked. // Validate the issuer certificate // this is required, because certificate can have a chain that is longer then 2 // The only problem is, that we can not ocsp validate the intermediate certificate (the issuer certificate). // according to DanID - that certificate can only be validated with CRL // Note : The crl list will be/should be very short. Only containing the issuer certificate that has been revoked. // A good guess is that there at all time will be most 10 issuer certificate, so the list of revoked issuer certificate is short. List issuerUrlList = getAuthorityInformationAccessOcspUrl(issuerX509Certificate); RevocationResponse issuerRevocationResponse; if (issuerUrlList.size() > 0) { // hey, wow some url exist - lets use that // don't thing this will ever happens anyway issuerRevocationResponse = this.revocationResponse(issuerX509Certificate); } else { // we need to validate with crl instead // It does not contain the Authority Info Access, containing the rl to where the certificate must be validated // We must therefore guess, that the certificate is valid. CrlLookup crlLookupClient = new CrlLookup(); issuerRevocationResponse = crlLookupClient.checkCertificate(issuerX509Certificate); } // now to handle the issuerRevocationResponse if (issuerRevocationResponse == null) { revocationResponse.setIsValid(false); //revocationResponse.Exception = new CheckCertificateOcspUnexpectedException("The issuing certificate could not be validated."); } else { // the issuer certificate is validated, the validity of the issuer certificate // is copied to the revocationResponse revocationResponse.setIsValid(issuerRevocationResponse.isValid()); } // update the cache this.ocspCache.add(x509Certificate.getSubjectX500Principal(), revocationResponse); } } else { // the certificate is Not valid // no need to check the issuer certificate this.ocspCache.add(x509Certificate.getSubjectX500Principal(), revocationResponse); } } return revocationResponse; } /** * @param x509Certificate The certificate to verify. * @param issuerX509Certificate Issuer certificate of x509Certificate. */ private RevocationResponse revocationResponseOnline(X509Certificate x509Certificate, X509Certificate issuerX509Certificate) throws RevocationException { if (x509Certificate == null) { throw new RevocationException("Certificate to verify is null"); } RevocationResponse revocationResponse; try { // create BouncyCastle certificates //X509CertificateParser certParser = new X509CertificateParser(); //Org.BouncyCastle.X509.X509Certificate serverX509Certificate = certParser.ReadCertificate(serverX509Certificate2.RawData); // 1. Get server url List urlList = getAuthorityInformationAccessOcspUrl(x509Certificate); if (urlList.isEmpty()) { throw new RevocationException("No OCSP url found in certificate: " + x509Certificate.getSubjectDN().getName()); } // we always validate against the first defined url: String url = urlList.get(0); revocationResponse = revocationResponseOnline(x509Certificate, issuerX509Certificate, url); } catch (OCSPException e) { log.warn(e.getMessage(), e); throw new RevocationException(e); } return revocationResponse; } public RevocationResponse revocationResponseOnline(X509Certificate serverX509Certificate, X509Certificate issuerX509Certificate, String url) throws RevocationException, OCSPException { if (log.isDebugEnabled()) { log.debug("Online OCSP call for [" + serverX509Certificate.getSubjectDN() + "] to url=" + url); } if (serverX509Certificate == null) { throw new RevocationException("Server certificate is null"); } if (issuerX509Certificate == null) { throw new RevocationException("Issuer certificate for server certificate not identified"); } RevocationResponse revocationResponse; try { // 1. Generate request OCSPReq req = generateOcspRequest(issuerX509Certificate, serverX509Certificate.getSerialNumber()); // 2. make binary request online byte[] binaryResp = postData(req, url); // 3. Make result object. revocationResponse = processOcspResponse(serverX509Certificate, binaryResp); } catch (NoSuchProviderException | IOException e) { log.error(e.getMessage(), e); throw new RevocationException(e); } return revocationResponse; } private OCSPReq generateOcspRequest(X509Certificate rootX509Certificate, BigInteger serialNumber) throws OCSPException { CertificateID certificateID = new CertificateID(CertificateID.HASH_SHA1, rootX509Certificate, serialNumber); return this.generateOcspRequest(certificateID); } private OCSPReq generateOcspRequest(CertificateID id) throws OCSPException { OCSPReqGenerator ocspRequestGenerator = new OCSPReqGenerator(); ocspRequestGenerator.addRequest(id); return ocspRequestGenerator.generate(); } private RevocationResponse processOcspResponse(X509Certificate serverX509Certificate, byte[] binaryResp) throws IOException, OCSPException, RevocationException, NoSuchProviderException { OCSPResp ocspResponse = new OCSPResp(binaryResp); RevocationResponse revocationResponse = new RevocationResponse(); switch (ocspResponse.getStatus()) { case OCSPResponseStatus.SUCCESSFUL: { Object responseObject = ocspResponse.getResponseObject(); if (!(responseObject instanceof BasicOCSPResp)) { throw new IllegalStateException("OCSP response is of unexpected type"); } BasicOCSPResp basicResp = (BasicOCSPResp) responseObject; /* check condition: The certificate identified in a received response corresponds to that which was identified in the corresponding request; */ SingleResp[] responses = basicResp.getResponses(); if (responses.length != 1) { throw new IllegalStateException("unexpected number of responses received"); } SingleResp singleResp = responses[0]; if (!serverX509Certificate.getSerialNumber().equals(singleResp.getCertID().getSerialNumber())) { throw new RevocationException("Serial number mismatch problem"); } X509Certificate ocspCertificate = this.findOcspClientCertificate(basicResp.getCerts("BC")); /* check condition The signature on the response is valid; The chain is validated other check */ //if(!this.verifyOcspCertificateChain(certificate, ocspCertificate)) { // throw new IllegalStateException("Certificate used to sign OCSP Response could not be verified"); //} /* check the signature on the ocsp response */ if (!basicResp.verify(ocspCertificate.getPublicKey(), "BC")) { throw new RevocationException("signature validation failed for ocsp response"); } if (!canSignOcspResponses(ocspCertificate)) { throw new RevocationException("ocsp signing certificate has not been cleared for ocsp response signing"); } /* check expiry of the signing certificate */ if (!certificateValid(ocspCertificate)) { throw new IllegalStateException("ocsp signing certificate is not valid"); } Object certStatus = singleResp.getCertStatus(); // Now to validate the actual revocation status if (certStatus == null) { // according to org.openoces.ooapi.utils.ocsp.ResponseParser.cs (DanID test code) // in the TU11, method SerialNumberInResponseIsNotRevoked(..), // when the certificateStatus is empty, all is okay - not revoked // no revocation data exist - the certificate must be valid revocationResponse.setIsValid(true); revocationResponse.setNextUpdate(singleResp.getNextUpdate()); } else if (certStatus.equals(CertificateStatus.GOOD)) { // this is the expected certificateStatus for valid certificates // however if the status is good the certStatus is null revocationResponse.setIsValid(true); revocationResponse.setNextUpdate(singleResp.getNextUpdate()); } else if (certStatus instanceof org.bouncycastle.ocsp.RevokedStatus) { revocationResponse.setIsValid(false); revocationResponse.setNextUpdate(singleResp.getNextUpdate()); } else if (certStatus instanceof org.bouncycastle.ocsp.UnknownStatus) { throw new RevocationException("ocsp response indicates unknown certificate status"); } else { throw new RevocationException("unknown status"); } break; } case OCSPResponseStatus.INTERNAL_ERROR: case OCSPResponseStatus.MALFORMED_REQUEST: case OCSPResponseStatus.SIG_REQUIRED: case OCSPResponseStatus.TRY_LATER: case OCSPResponseStatus.UNAUTHORIZED: default: { log.warn("OCSPResponse has Status error=" + ocspResponse.getStatus()); // all other then success is. // new RevocationException("OCSPResponseStatus not valid!"); } } return revocationResponse; } private byte[] postData(OCSPReq req, String url) throws RevocationException { try { HttpURLConnection conn = postRequest(req.getEncoded(), url); if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { log.error("HTTP response code from OCSP request to [" + url + "] was: " + conn.getResponseCode()); throw new RevocationException("HTTP response code from OCSP request to [" + url + "] was: " + conn.getResponseCode()); } int len = conn.getContentLength(); return readResponse(conn, len); } catch (IOException e) { log.error("Error doing OCSP lookup by url=" + url + ": " + e.getMessage(), e); throw new RevocationException(e); } } private HttpURLConnection postRequest(byte[] bs, String responderURL) throws IOException { HttpURLConnection conn = this.setupHttpConnectionForPost(bs.length, responderURL); OutputStream os = conn.getOutputStream(); os.write(bs); conn.connect(); os.close(); return conn; } private HttpURLConnection setupHttpConnectionForPost(int contentLength, String responderURL) throws IOException { HttpURLConnection conn; conn = (HttpURLConnection) new URL(responderURL).openConnection(); conn.setAllowUserInteraction(false); conn.setDoInput(true); conn.setDoOutput(true); conn.setUseCaches(false); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Length", "" + contentLength); conn.setRequestProperty("Content-Type", "application/ocsp-request"); return conn; } private byte[] readResponse(HttpURLConnection conn, int len) throws IOException { InputStream is = conn.getInputStream(); byte[] respData = new byte[len]; int bRead = 0; do { bRead += is.read(respData, bRead, Math.min(1024, len - bRead)); } while (bRead != len); is.close(); conn.disconnect(); return respData; } public X509Certificate findIssuerCertificate(X509Certificate certificate) { // tree possibilities // 1: A root certificate - exist in root list. // 2: A root certificate not trusted/not in list. // 3: Issuer certificate, can be downloaded. X509Certificate issuerCertificate; String key = certificate.getIssuerDN().getName(); if (rootCertificateMap.containsKey(key)) { // root certificate is in the trusted key store (possibility 1) issuerCertificate = rootCertificateMap.get(key); } else { // Down to possibility 2 or 3: // 2: A root certificate not trusted/not in list. // 3: Issuer certificate, can can be downloaded // So we will try to download the certificate issuerCertificate = certificateUtil.downloadIssuerCert(certificate); // if ( issuerCertificate== null){ // The issuer certificate is a root certificate , but not trusted (oces1) // Possible 2 //} else { // The issuer certificate is a issuerCertificate (oces2) // Possible 3 // } } return issuerCertificate; } public String GetServerUriFromCertificate(X509Certificate certificate) throws RevocationException { // not optimal implementation - could be improved - look at danIds OOAPI TU11 ASN1InputStream aIn = null; try { byte[] ext = certificate.getExtensionValue(X509Extensions.AuthorityInfoAccess.getId()); aIn = new ASN1InputStream(ext); aIn = new ASN1InputStream(((ASN1OctetString) aIn.readObject()).getOctets()); ASN1Encodable object = aIn.readObject(); AuthorityInformationAccess auth = AuthorityInformationAccess.getInstance(object); AccessDescription[] acc = auth.getAccessDescriptions(); return acc[0].getAccessLocation().toString().substring(3); } catch (IOException e) { throw new RevocationException(e); } finally { if (aIn != null) { try { aIn.close(); } catch (IOException e) { log.warn("Error closing stream: " + e.getMessage(), e); } } } } public List getAuthorityInformationAccessOcspUrl(X509Certificate x509Certificate) { List ocspUrls = new ArrayList<>(); ASN1InputStream extensionAns1InputStream = null; ASN1InputStream octetAns1InputStream = null; try { byte[] extBytes = x509Certificate.getExtensionValue(X509Extensions.AuthorityInfoAccess.getId()); extensionAns1InputStream = new ASN1InputStream(extBytes); ASN1OctetString ans1OctetString = (ASN1OctetString) extensionAns1InputStream.readObject(); byte[] octetsBytes = ans1OctetString.getOctets(); octetAns1InputStream = new ASN1InputStream(octetsBytes); ASN1Encodable ans1Encodable = octetAns1InputStream.readObject(); AuthorityInformationAccess auth = AuthorityInformationAccess.getInstance(ans1Encodable); AccessDescription[] accessDescriptionArray = auth.getAccessDescriptions(); GeneralName generalName; String url; for (AccessDescription anAccessDescriptionArray : accessDescriptionArray) { generalName = anAccessDescriptionArray.getAccessLocation(); url = generalName.toString(); if (url.length() > 3) { ocspUrls.add(url.substring(3)); } } } catch (IOException e) { log.warn(e.getMessage(), e); } finally { if (extensionAns1InputStream != null) { try { extensionAns1InputStream.close(); } catch (IOException e) { log.warn("Error closing stream: " + e.getMessage(), e); } } if (octetAns1InputStream != null) { try { octetAns1InputStream.close(); } catch (IOException e) { log.warn("Error closing stream: " + e.getMessage(), e); } } } return ocspUrls; } private boolean certificateValid(X509Certificate ocspCertificate) { try { ocspCertificate.checkValidity(); return true; } catch (CertificateExpiredException | CertificateNotYetValidException e) { return false; } } private boolean canSignOcspResponses(X509Certificate ocspCertificate) { try { return ocspCertificate.getExtendedKeyUsage().contains(KeyPurposeId.id_kp_OCSPSigning.getId()); } catch (CertificateParsingException e) { throw new RuntimeException("ocsp signing certificate has not been cleared for ocsp response signing"); } } private X509Certificate findOcspClientCertificate(X509Certificate[] certs) { int KeyUsageDigitalSignature = 0; X509Certificate ocspCert = null; for (X509Certificate certificate : certs) { boolean[] keyUsage = certificate.getKeyUsage(); if (keyUsage != null && keyUsage[KeyUsageDigitalSignature] && null != certificate.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId())) { ocspCert = certificate; } } if (ocspCert == null) { throw new RuntimeException("Could not find valid OCSP certificate"); } return ocspCert; } /** * @return Always returns OCSP * @since OIORASP 1.3.0 */ @Override public RevocationSourceType getRevocationSourceType() { return RevocationSourceType.OCSP; } }