Hi everybody, I have been struggling for some months with a weird issue about how Java validates OCSP responses. Following the RFC2560 standard the responses sent by the responder should be signed following one of these three possibilities:
> All definitive response messages SHALL be digitally signed. The key > used to sign the response MUST belong to one of the following: > > -- the CA who issued the certificate in question > -- a Trusted Responder whose public key is trusted by the requester > -- a CA Designated Responder (Authorized Responder) who holds a > specially marked certificate issued directly by the CA, indicating > that the responder may issue OCSP responses for that CA In current java implementation (openjdk 6, 7 and 8) the case (1) and (3) are considered by default and case (2) can be configured using some properties ("ocsp.responderCertSubjectName" for example). But the problem is that both configurations are exclusive, if your application accepts responses for the cases (1) and (3) it fails with the case (2) and vice-versa. I faced an OCSP responder that in some cases it answered using the case (1) and in others using the case (2). The case (1) was used to sign responses for their own certificates and the case (2) was used to sign responses for foreign certificates (spanish national id certificates specifically). I'm not completely sure if the standard admits this situation but I haven't read anything against that. Besides why not to take the configured certificate ("ocsp.responderCertSubjectName" or any of the other properties) as a failback and not as the unique valid signer. Looking at the code the problem is that only one certificate is passed as the valid signer for responses (the one configured via properties or the issuer cert). Following Andrew advise I have made a little patch against current openjdk-8 that just considers both of them (OCSPResponse class receives both certs and this way can check the three cases). Thanks in advance!
diff --git a/OCSP.java b/OCSP.java index ca82ef5..d07f01f 100644 --- a/OCSP.java +++ b/OCSP.java @@ -129,7 +129,7 @@ public final class OCSP { ("Exception while encoding OCSPRequest", e); } OCSPResponse ocspResponse = check(Collections.singletonList(certId), - responderURI, issuerCert, null, Collections.<Extension>emptyList()); + responderURI, issuerCert, null, null, Collections.<Extension>emptyList()); return (RevocationStatus)ocspResponse.getSingleResponse(certId); } @@ -176,7 +176,7 @@ public final class OCSP { ("Exception while encoding OCSPRequest", e); } OCSPResponse ocspResponse = check(Collections.singletonList(certId), - responderURI, responderCert, date, extensions); + responderURI, issuerCert, responderCert, date, extensions); return (RevocationStatus) ocspResponse.getSingleResponse(certId); } @@ -185,6 +185,7 @@ public final class OCSP { * * @param certs the CertIds to be checked * @param responderURI the URI of the OCSP responder + * @param issuerCert the issuer certificate * @param responderCert the OCSP responder's certificate * @param date the time the validity of the OCSP responder's certificate * should be checked against. If null, the current time is used. @@ -195,7 +196,7 @@ public final class OCSP { * encoding the OCSP Request or validating the OCSP Response */ static OCSPResponse check(List<CertId> certIds, URI responderURI, - X509Certificate responderCert, Date date, + X509Certificate issuerCert, X509Certificate responderCert, Date date, List<Extension> extensions) throws IOException, CertPathValidatorException { @@ -282,7 +283,7 @@ public final class OCSP { } // verify the response - ocspResponse.verify(certIds, responderCert, date, request.getNonce()); + ocspResponse.verify(certIds, issuerCert, responderCert, date, request.getNonce()); return ocspResponse; } diff --git a/OCSPResponse.java b/OCSPResponse.java index 2cd437d..193cfc0 100644 --- a/OCSPResponse.java +++ b/OCSPResponse.java @@ -377,7 +377,7 @@ public final class OCSPResponse { } } - void verify(List<CertId> certIds, X509Certificate responderCert, + void verify(List<CertId> certIds, X509Certificate issuerCert, X509Certificate responderCert, Date date, byte[] nonce) throws CertPathValidatorException { @@ -411,11 +411,30 @@ public final class OCSPResponse { // First check if the cert matches the expected responder cert if (cert.equals(responderCert)) { // cert is trusted, now verify the signed response + if (debug != null) { + debug.println("Case (2): Trusted Responder whose public key is trusted by the requester"); + } + + // Next check if it is the issuerCert (common situation) + } else if (cert.equals(issuerCert)) { + if (debug != null) { + debug.println("Case (1): CA who issued the certificate in question"); + } + responderCert = issuerCert; // Next check if the cert was issued by the responder cert // which was set locally. - } else if (cert.getIssuerX500Principal().equals( - responderCert.getSubjectX500Principal())) { + } else if (cert.getIssuerX500Principal().equals(issuerCert.getSubjectX500Principal()) || + (responderCert != null && + cert.getIssuerX500Principal().equals(responderCert.getSubjectX500Principal()))) { + // the case of responderCert is not covered by the RFC2560 + // but it was covered before, so I won't change the behavior + if (cert.getIssuerX500Principal().equals(issuerCert.getSubjectX500Principal())) { + responderCert = issuerCert; + } + if (debug != null) { + debug.println("Case (3): CA Designated Responder (Authorized Responder) who holds a specially marked certificate issued directly by the CA, indicating that the responder may issue OCSP responses for that CA"); + } // Check for the OCSPSigning key purpose try { @@ -537,6 +556,18 @@ public final class OCSPResponse { } /** + * Returns the signer certificate if it exists. + * @return The first certificate used to sign the response or null + */ + X509CertImpl getSignerCertificate() { + if (!certs.isEmpty()) { + return certs.get(0); + } else { + return null; + } + } + + /** * Returns the OCSP ResponseStatus. */ ResponseStatus getResponseStatus() { diff --git a/RevocationChecker.java b/RevocationChecker.java index 98d8a9d..d1e50bd 100644 --- a/RevocationChecker.java +++ b/RevocationChecker.java @@ -633,9 +633,6 @@ class RevocationChecker extends PKIXRevocationChecker { URI responderURI = (this.responderURI != null) ? this.responderURI : getOCSPServerURI(currCert); - X509Certificate respCert = (responderCert == null) ? issuerCert - : responderCert; - // The algorithm constraints of the OCSP trusted responder certificate // does not need to be checked in this code. The constraints will be // checked when the responder's certificate is validated. @@ -667,12 +664,12 @@ class RevocationChecker extends PKIXRevocationChecker { nonce = ext.getValue(); } } - response.verify(Collections.singletonList(certId), respCert, + response.verify(Collections.singletonList(certId), issuerCert, responderCert, params.date(), nonce); } else { response = OCSP.check(Collections.singletonList(certId), - responderURI, respCert, params.date(), + responderURI, issuerCert, responderCert, params.date(), ocspExtensions); } } catch (IOException e) { @@ -685,7 +682,7 @@ class RevocationChecker extends PKIXRevocationChecker { if (certStatus == RevocationStatus.CertStatus.REVOKED) { Throwable t = new CertificateRevokedException( rs.getRevocationTime(), rs.getRevocationReason(), - respCert.getSubjectX500Principal(), rs.getSingleExtensions()); + response.getSignerCertificate().getSubjectX500Principal(), rs.getSingleExtensions()); throw new CertPathValidatorException(t.getMessage(), t, null, -1, BasicReason.REVOKED); } else if (certStatus == RevocationStatus.CertStatus.UNKNOWN) {