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