/*
* 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;
}
}