/*
* Copyright (c) 2012 ICM Uniwersytet Warszawski All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package eu.emi.security.authn.x509.helpers.ocsp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Random;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentVerifierProvider;
import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.bouncycastle.util.encoders.Base64;
import eu.emi.security.authn.x509.X509Credential;
import eu.emi.security.authn.x509.helpers.BinaryCertChainValidator;
import eu.emi.security.authn.x509.impl.CertificateUtils;
import eu.emi.security.authn.x509.impl.FormatMode;
import eu.emi.security.authn.x509.impl.SocketFactoryCreator;
/**
* OCSP client is responsible for the network related activity of the OCSP invocation pipeline.
* This class is state less and thread safe.
* <p>
* It is implementing the RFC 2560 also taking care to support the lightweight profile recommendations
* defined in the RFC 5019.
*
* @author K. Benedyczak
*/
public class OCSPClientImpl
{
private static final Charset ASCII = Charset.forName("US-ASCII");
private static final int MAX_RESPONSE_SIZE = 20480;
/**
* Returns a verified single response, related to the checked certificate. This is single-shot version,
* which can be used instead of manual invocation of low-level methods.
* @param responder mandatory - URL of the responder. HTTP or HTTPs, however in https mode the
* @param toCheckCert mandatory certificate to be checked
* @param issuerCert mandatory certificate of the toCheckCert issuer
* @param requester if not null, then it is assumed that request must be signed by the requester.
* @param addNonce if true nonce will be added to the request and required in response
* @param timeout timeout
* @return Final OCSP checking result
* @throws IOException IO exception
* @throws OCSPException OCSP exception
*/
public OCSPResult queryForCertificate(URL responder, X509Certificate toCheckCert,
X509Certificate issuerCert, X509Credential requester, boolean addNonce, int timeout)
throws IOException, OCSPException
{
OCSPReq request = createRequest(toCheckCert, issuerCert, requester, addNonce);
OCSPResp response = send(responder, request, timeout).getResponse();
byte[] nonce = null;
if (addNonce)
nonce = extractNonce(request);
SingleResp resp = verifyResponse(response, toCheckCert, issuerCert, nonce);
return new OCSPResult(resp);
}
public OCSPReq createRequest(X509Certificate toCheckCert,
X509Certificate issuerCert, X509Credential requester, boolean addNonce)
throws OCSPException
{
OCSPReqBuilder generator = new OCSPReqBuilder();
CertificateID certId;
try
{
DigestCalculator digestCalc = new BcDigestCalculatorProvider().get(CertificateID.HASH_SHA1);
X509CertificateHolder issuerCertHolder = new JcaX509CertificateHolder(issuerCert);
certId = new CertificateID(digestCalc, issuerCertHolder, toCheckCert.getSerialNumber());
} catch (OperatorCreationException e1)
{
throw new OCSPException("Problem creating digester", e1);
} catch (CertificateEncodingException e)
{
throw new OCSPException("Issuer certificate is unsupported ", e);
}
generator.addRequest(certId);
if (addNonce)
{
byte[] nonce = new byte[16];
Random rand = new Random();
rand.nextBytes(nonce);
Extensions extensions = new Extensions(new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce,
false, new DEROctetString(nonce)));
generator.setRequestExtensions(extensions);
}
if (requester != null)
{
X500Name subjectName = new X500Name(requester.getCertificate().getSubjectX500Principal().getName());
generator.setRequestorName(subjectName);
try
{
JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(
requester.getCertificate().getSigAlgOID());
return generator.build(csBuilder.build(requester.getKey()), null);
} catch (OperatorCreationException e)
{
throw new OCSPException("Unsupported signing algorithm when creating a OCSP request?", e);
}
} else
{
return generator.build();
}
}
public OCSPResponseStructure send(URL responder, OCSPReq requestO, int timeout) throws IOException {
InputStream in = null;
byte[] request = requestO.getEncoded();
byte[] response = null;
Date maxCache = null;
HttpURLConnection con = null;
try {
String getUrl = getHttpGetUrl(responder, request);
if (getUrl == null)
con = doPost(responder, request, timeout);
else
{
URL u = new URL(getUrl);
con = (HttpURLConnection) u.openConnection();
configureHttpConnection(con, timeout);
}
in = con.getInputStream();
int contentLength = con.getContentLength();
if (contentLength == -1 || contentLength > MAX_RESPONSE_SIZE)
contentLength = MAX_RESPONSE_SIZE;
maxCache = getNextUpdateFromCacheHeader(con.getHeaderField("cache-control"));
response = new byte[contentLength];
int total = 0;
int count = 0;
while (total < contentLength) {
count = in.read(response, total, response.length - total);
if (count < 0)
break;
total += count;
}
if (count >= 0 && in.read() >= 0)
throw new IOException("OCSP response size exceeded the upper limit of " +
MAX_RESPONSE_SIZE);
if (total != contentLength)
response = Arrays.copyOf(response, total);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ioe) {
throw ioe;
}
}
}
OCSPResp resp = new OCSPResp(response);
return new OCSPResponseStructure(resp, maxCache);
}
private void configureHttpConnection(HttpURLConnection con, int timeout)
{
if (con instanceof HttpsURLConnection)
{
HttpsURLConnection httpsCon = (HttpsURLConnection) con;
BinaryCertChainValidator trustAll = new BinaryCertChainValidator(true);
SSLSocketFactory sf = SocketFactoryCreator.getSocketFactory(null, trustAll);
httpsCon.setSSLSocketFactory(sf);
}
con.setConnectTimeout(timeout);
con.setReadTimeout(timeout);
}
/**
*
* @return null if the encoded request is > 255, or the string which can be used as GET
* request URL with request encoded.
*/
private String getHttpGetUrl(URL responder, byte[] request)
{
if (responder.toExternalForm().length() + request.length > 255)
return null; //as Base64 is making the request even bigger this is a VERY safe bet.
byte[] base64 = Base64.encode(request);
String ret = new String(base64, ASCII);
try
{
ret = URLEncoder.encode(ret, ASCII.name());
} catch (UnsupportedEncodingException e)
{
throw new RuntimeException("US-ASCII encoding is not known?", e);
}
String url = responder.toExternalForm();
if (url.endsWith("/"))
ret = url + ret;
else
ret = url + "/" + ret;
if (ret.length() > 255)
return null;
return ret;
}
private HttpURLConnection doPost(URL responder, byte[] request, int timeout) throws IOException
{
HttpURLConnection con = (HttpURLConnection) responder.openConnection();
configureHttpConnection(con, timeout);
OutputStream out = null;
try
{
con.setDoOutput(true);
con.setRequestMethod("POST");
con.setRequestProperty("Content-type", "application/ocsp-request");
con.setRequestProperty("Content-length", String.valueOf(request.length));
out = con.getOutputStream();
out.write(request);
out.flush();
return con;
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ioe) {
throw ioe;
}
}
}
}
public static Date getNextUpdateFromCacheHeader(String cc)
{
if (cc == null)
return null;
int i = cc.indexOf("max-age=");
if (i == -1)
return null;
i+=8;
int j = cc.indexOf(",", i);
if (j == -1)
j=cc.length();
String deltaS = cc.substring(i, j).trim();
int delta;
try
{
delta = Integer.parseInt(deltaS);
}catch (NumberFormatException e)
{
return null;
}
return new Date(System.currentTimeMillis() + (delta*1000L));
}
private static String getResponderErrorDesc(int errorNo)
{
switch (errorNo)
{
case OCSPResponseStatus.INTERNAL_ERROR:
return "internal server error";
case OCSPResponseStatus.MALFORMED_REQUEST:
return "malformed request";
case OCSPResponseStatus.SIG_REQUIRED:
return "request is required to be signed";
case OCSPResponseStatus.TRY_LATER:
return "try again later";
case OCSPResponseStatus.UNAUTHORIZED:
return "request was not authorized";
default:
return "unknown error";
}
}
/**
* Verifies the provided response
* @param response OCSP response
* @param toCheckCert mandatory certificate to be checked
* @param issuerCert mandatory certificate of the toCheckCert issuer
* @param checkNonce expected OCSP nonce
* @return verified response corresponding to the certificate being checked
* @throws OCSPException OCSP exception
*/
public SingleResp verifyResponse(OCSPResp response, X509Certificate toCheckCert,
X509Certificate issuerCert, byte[] checkNonce) throws OCSPException
{
if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL)
throw new OCSPException("Responder returned an error: " +
getResponderErrorDesc(response.getStatus()));
Object respO = response.getResponseObject();
if (!(respO instanceof BasicOCSPResp))
throw new OCSPException("Only Basic OCSP response type is supported");
BasicOCSPResp bresp = (BasicOCSPResp) respO;
//version, producedAt and responderID are ignored.
if (checkNonce != null)
{
byte[] nonceAsn;
try
{
nonceAsn = bresp.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce).
getExtnValue().getEncoded();
} catch (IOException e1)
{
throw new OCSPException("Can't parse OCSP nonce extension", e1);
}
if (nonceAsn == null)
throw new OCSPException("Nonce was sent and is required but did not get it in reply");
ASN1OctetString octs;
try
{
octs = (ASN1OctetString)ASN1Primitive.fromByteArray(nonceAsn);
} catch (Exception e)
{
throw new OCSPException("Nonce received with the reply is invalid, " +
"unable to parse it", e);
}
byte[] nonce = octs.getOctets();
if (!Arrays.equals(nonce, checkNonce))
throw new OCSPException("Received nonce doesn't match the one sent to the server. " +
"Sent: " + Arrays.toString(checkNonce) + " received: " +
Arrays.toString(nonce));
}
PublicKey key = establishResponsePubKey(bresp, issuerCert);
try
{
ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder().build(key);
if (!bresp.isSignatureValid(verifierProvider))
throw new OCSPException("Failed to verify the OCSP response signature. " +
"It is corrupted or faked");
} catch (OperatorCreationException e)
{
throw new OCSPException("The OCSP is signed with unsupported key: " +
"can not verify its signature", e);
}
if (bresp.getCriticalExtensionOIDs().size() > 0)
throw new OCSPException("OCSP contains unsupported critical extensions: " +
bresp.getCriticalExtensionOIDs());
SingleResp[] resps = bresp.getResponses();
for (int i=0; i<resps.length; i++)
{
SingleResp sResp = resps[i];
if (sResp.getCriticalExtensionOIDs().size() > 0)
throw new OCSPException("OCSP SingleResponse contains unsupported critical extensions: " +
sResp.getCriticalExtensionOIDs());
if (!checkCertIDMatching(toCheckCert, issuerCert, sResp.getCertID()))
continue;
verifyTimeRange(sResp.getThisUpdate(), sResp.getNextUpdate());
return sResp;
}
throw new OCSPException("Received a correct answer from OCSP responder, but it didn't contain " +
"any information on the certificate being checked");
}
private void verifyTimeRange(Date thisUpdate, Date nextUpdate) throws OCSPException
{
Date now = new Date();
if (thisUpdate == null)
throw new OCSPException("Malformed OCSP response, no thisUpdate time");
if (nextUpdate == null)
throw new OCSPException("Unsupported OCSP response, no nextUpdate time (required by RFC 5019)");
int tolerance = 120000; //two minutes
Date futureNow = new Date(now.getTime() + tolerance);
Date pastNow = new Date(now.getTime() - tolerance);
if (futureNow.before(thisUpdate))
throw new OCSPException("Response is not yet valid, will be from: " + thisUpdate);
if (pastNow.after(nextUpdate))
throw new OCSPException("Response has expired on: " + nextUpdate);
}
private boolean checkCertIDMatching(X509Certificate toFind, X509Certificate issuerCert,
CertificateID checkedCertId) throws OCSPException
{
try
{
JcaX509CertificateHolder issuerCertHolder = new JcaX509CertificateHolder(issuerCert);
DigestCalculator digCalc = new BcDigestCalculatorProvider().get(new AlgorithmIdentifier(
checkedCertId.getHashAlgOID()));
CertificateID certId = new CertificateID(digCalc, issuerCertHolder,
toFind.getSerialNumber());
return certId.getHashAlgOID().equals(checkedCertId.getHashAlgOID()) &&
Arrays.equals(certId.getIssuerKeyHash(), checkedCertId.getIssuerKeyHash()) &&
Arrays.equals(certId.getIssuerNameHash(), checkedCertId.getIssuerNameHash());
} catch (OperatorCreationException e)
{
throw new OCSPException("Cant get digester for the checked certificate, the algorithm " +
"is: " + checkedCertId.getHashAlgOID(), e);
} catch (CertificateEncodingException e)
{
throw new OCSPException("Issuer certificate is unsupported", e);
}
}
private PublicKey establishResponsePubKey(BasicOCSPResp bresp, X509Certificate issuerCert) throws OCSPException
{
X509CertificateHolder[] signerCerts = bresp.getCerts();
if (signerCerts == null || signerCerts.length == 0)
return issuerCert.getPublicKey();
X509Certificate signerCert;
try
{
signerCert = new JcaX509CertificateConverter().getCertificate(signerCerts[0]);
} catch (CertificateException e1)
{
throw new OCSPException("Can't unwrap signer's certificate from the BasicOCSPResp", e1);
}
if (signerCert.equals(issuerCert))
return issuerCert.getPublicKey();
//ok - now we have the last possibility - delegated OCSP responder
if (!issuerCert.getSubjectX500Principal().equals(signerCert.getIssuerX500Principal()))
throw new OCSPException("Response is signed by an untrusted/invalid entity: " +
CertificateUtils.format(signerCert, FormatMode.COMPACT_ONE_LINE));
try
{
List<String> keyUsage = signerCert.getExtendedKeyUsage();
if (keyUsage == null || !keyUsage.contains(KeyPurposeId.id_kp_OCSPSigning.getId()))
throw new OCSPException("Response is signed by an entity which does not have the " +
"OCSP delegation from the CA (no flag in ExtendedKeyUsage)");
} catch (CertificateParsingException e)
{
throw new OCSPException("Response contains an unparsable certificate (ExtendedKeyUsage)", e);
}
try
{
signerCert.verify(issuerCert.getPublicKey(), BouncyCastleProvider.PROVIDER_NAME);
} catch (Exception e)
{
throw new OCSPException("Response contains a certificate which is improperly signed, " +
"it is faked or corrupted: " + e.getMessage(), e);
}
return signerCert.getPublicKey();
}
public static byte[] extractNonce(OCSPReq request) throws IOException
{
Extension nonceExt = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
if (nonceExt == null)
return null;
byte[] nonceAsn = nonceExt.getExtnValue().getEncoded();
if (nonceAsn == null)
return null;
ASN1OctetString octs;
try
{
octs = (ASN1OctetString)ASN1Primitive.fromByteArray(nonceAsn);
} catch (Exception e)
{
throw new IllegalStateException("Can't decode nonce encoded in request", e);
}
return octs.getOctets();
}
}