/* * Copyright 2016 Analytical Graphics, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.keycloak.common.util; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.CRLReason; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.asn1.x509.AuthorityInformationAccess; import org.bouncycastle.asn1.x509.AccessDescription; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; import org.bouncycastle.cert.ocsp.*; import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID; 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.JcaContentVerifierProviderBuilder; import java.io.*; import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.cert.*; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; /** * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a> * @version $Revision: 1 $ * @since 10/29/2016 */ public final class OCSPUtils { static { BouncyIntegration.init(); } private final static Logger logger = Logger.getLogger(""+OCSPUtils.class); private static int OCSP_CONNECT_TIMEOUT = 10000; // 10 sec private static final int TIME_SKEW = 900000; public enum RevocationStatus { GOOD, REVOKED, UNKNOWN } public interface OCSPRevocationStatus { RevocationStatus getRevocationStatus(); Date getRevocationTime(); CRLReason getRevocationReason(); } /** * Requests certificate revocation status using OCSP. * @param cert the certificate to be checked * @param issuerCertificate The issuer certificate * @param responderURI an address of OCSP responder. Overrides any OCSP responder URIs stored in certificate's AIA extension * @param date * @param responderCert a certificate that OCSP responder uses to sign OCSP responses * @return revocation status */ public static OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate, URI responderURI, X509Certificate responderCert, Date date) throws CertPathValidatorException { if (cert == null) throw new IllegalArgumentException("cert cannot be null"); if (issuerCertificate == null) throw new IllegalArgumentException("issuerCertificate cannot be null"); if (responderURI == null) throw new IllegalArgumentException("responderURI cannot be null"); return check(cert, issuerCertificate, Collections.singletonList(responderURI), responderCert, date); } /** * Requests certificate revocation status using OCSP. The OCSP responder URI * is obtained from the certificate's AIA extension. * @param cert the certificate to be checked * @param issuerCertificate The issuer certificate * @param date * @return revocation status */ public static OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate, Date date, X509Certificate responderCert) throws CertPathValidatorException { List<String> responderURIs = null; try { responderURIs = getResponderURIs(cert); } catch (CertificateEncodingException e) { logger.log(Level.FINE, "CertificateEncodingException: {0}", e.getMessage()); throw new CertPathValidatorException(e.getMessage(), e); } if (responderURIs.size() == 0) { logger.log(Level.INFO, "No OCSP responders in the specified certificate"); throw new CertPathValidatorException("No OCSP Responder URI in certificate"); } List<URI> uris = new LinkedList<>(); for (String value : responderURIs) { try { URI responderURI = URI.create(value); uris.add(responderURI); } catch (IllegalArgumentException ex) { logger.log(Level.FINE, "Malformed responder URI {0}", value); } } return check(cert, issuerCertificate, Collections.unmodifiableList(uris), responderCert, date); } /** * Requests certificate revocation status using OCSP. The OCSP responder URI * is obtained from the certificate's AIA extension. * @param cert the certificate to be checked * @param issuerCertificate The issuer certificate * @return revocation status */ public static OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException { return check(cert, issuerCertificate, null, null); } private static OCSPResp getResponse(OCSPReq ocspReq, URI responderUri) throws IOException { DataOutputStream dataOut = null; InputStream in = null; try { byte[] array = ocspReq.getEncoded(); URL urlt = responderUri.toURL(); HttpURLConnection con = (HttpURLConnection) urlt.openConnection(); con.setRequestMethod("POST"); con.setConnectTimeout(OCSP_CONNECT_TIMEOUT); con.setReadTimeout(OCSP_CONNECT_TIMEOUT); con.setRequestProperty("Content-type", "application/ocsp-request"); con.setRequestProperty("Content-length", String.valueOf(array.length)); // con.setRequestProperty("Accept", "application/ocsp-response"); con.setDoOutput(true); con.setDoInput(true); OutputStream out = con.getOutputStream(); dataOut = new DataOutputStream(new BufferedOutputStream(out)); dataOut.write(array); dataOut.flush(); if (con.getResponseCode() / 100 != 2) { String errorMessage = String.format("Connection error, unable to obtain certificate revocation status using OCSP responder \"%s\", code \"%d\"", responderUri.toString(), con.getResponseCode()); throw new IOException(errorMessage); } //Get Response in = (InputStream) con.getInputStream(); int contentLen = con.getContentLength(); if (contentLen == -1) { contentLen = Integer.MAX_VALUE; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bytesRead = 0; byte[] buffer = new byte[2048]; while ((bytesRead = in.read(buffer, 0, buffer.length)) >= 0) { baos.write(buffer, 0, bytesRead); } baos.flush(); byte[] data = baos.toByteArray(); return new OCSPResp(data); } finally { if (dataOut != null) { dataOut.close(); } if (in != null) { in.close(); } } } /** * Requests certificate revocation status using OCSP. * @param cert the certificate to be checked * @param issuerCertificate the issuer certificate * @param responderURIs the OCSP responder URIs * @param responderCert the OCSP responder certificate * @param date if null, the current time is used. * @return a revocation status * @throws CertPathValidatorException */ private static OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate, List<URI> responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException { if (responderURIs == null || responderURIs.size() == 0) throw new IllegalArgumentException("Need at least one responder"); try { DigestCalculator digCalc = new BcDigestCalculatorProvider() .get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1)); JcaCertificateID certificateID = new JcaCertificateID(digCalc, issuerCertificate, cert.getSerialNumber()); // Create a nounce extension to protect against replay attacks SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); BigInteger nounce = BigInteger.valueOf(Math.abs(random.nextInt())); DEROctetString derString = new DEROctetString(nounce.toByteArray()); Extension nounceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, derString); Extensions extensions = new Extensions(nounceExtension); OCSPReq ocspReq = new OCSPReqBuilder().addRequest(certificateID, extensions).build(); URI responderURI = responderURIs.get(0); logger.log(Level.INFO, "OCSP Responder {0}", responderURI); try { OCSPResp resp = getResponse(ocspReq, responderURI); logger.log(Level.FINE, "Received a response from OCSP responder {0}, the response status is {1}", new Object[]{responderURI, resp.getStatus()}); switch (resp.getStatus()) { case OCSPResp.SUCCESSFUL: if (resp.getResponseObject() instanceof BasicOCSPResp) { return processBasicOCSPResponse(issuerCertificate, responderCert, date, certificateID, nounce, (BasicOCSPResp)resp.getResponseObject()); } else { throw new CertPathValidatorException("OCSP responder returned an invalid or unknown OCSP response."); } case OCSPResp.INTERNAL_ERROR: case OCSPResp.TRY_LATER: throw new CertPathValidatorException("Internal error/try later. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS); case OCSPResp.SIG_REQUIRED: throw new CertPathValidatorException("Invalid or missing signature. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.INVALID_SIGNATURE); case OCSPResp.UNAUTHORIZED: throw new CertPathValidatorException("Unauthorized request. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED); case OCSPResp.MALFORMED_REQUEST: default: throw new CertPathValidatorException("OCSP request is malformed. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED); } } catch(IOException e) { logger.log(Level.FINE, "OCSP Responder \"{0}\" failed to return a valid OCSP response\n{1}", new Object[] {responderURI, e.getMessage()}); throw new CertPathValidatorException("OCSP check failed", e); } } catch(CertificateNotYetValidException | CertificateExpiredException | OperatorCreationException | OCSPException | CertificateEncodingException | NoSuchAlgorithmException | NoSuchProviderException e) { logger.log(Level.FINE, e.getMessage()); throw new CertPathValidatorException(e.getMessage(), e); } } private static OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, BigInteger nounce, BasicOCSPResp basicOcspResponse) throws OCSPException, NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException { SingleResp expectedResponse = null; for (SingleResp singleResponse : basicOcspResponse.getResponses()) { if (compareCertIDs(certificateID, singleResponse.getCertID())) { expectedResponse = singleResponse; break; } } if (expectedResponse != null) { verifyResponse(basicOcspResponse, issuerCertificate, responderCertificate, nounce.toByteArray(), date); return singleResponseToRevocationStatus(expectedResponse); } else { throw new CertPathValidatorException("OCSP response does not include a response for a certificate supplied in the OCSP request"); } } private static boolean compareCertIDs(JcaCertificateID idLeft, CertificateID idRight) { if (idLeft == idRight) return true; if (idLeft == null || idRight == null) return false; return Arrays.equals(idLeft.getIssuerKeyHash(), idRight.getIssuerKeyHash()) && Arrays.equals(idLeft.getIssuerNameHash(), idRight.getIssuerNameHash()) && idLeft.getSerialNumber().equals(idRight.getSerialNumber()); } private static void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, byte[] requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException { List<X509CertificateHolder> certs = new ArrayList<>(Arrays.asList(basicOcspResponse.getCerts())); X509Certificate signingCert = null; try { certs.add(new JcaX509CertificateHolder(issuerCertificate)); if (responderCertificate != null) { certs.add(new JcaX509CertificateHolder(responderCertificate)); } } catch (CertificateEncodingException e) { e.printStackTrace(); } if (certs.size() > 0) { X500Name responderName = basicOcspResponse.getResponderId().toASN1Primitive().getName(); byte[] responderKey = basicOcspResponse.getResponderId().toASN1Primitive().getKeyHash(); if (responderName != null) { logger.log(Level.INFO, "Responder Name: {0}", responderName.toString()); for (X509CertificateHolder certHolder : certs) { try { X509Certificate tempCert = new JcaX509CertificateConverter() .setProvider("BC").getCertificate(certHolder); X500Name respName = new X500Name(tempCert.getSubjectX500Principal().getName()); if (responderName.equals(respName)) { signingCert = tempCert; logger.log(Level.INFO, "Found a certificate whose principal \"{0}\" matches the responder name \"{1}\"", new Object[] {tempCert.getSubjectDN().getName(), responderName.toString()}); break; } } catch (CertificateException e) { logger.log(Level.FINE, e.getMessage()); } } } else if (responderKey != null) { SubjectKeyIdentifier responderSubjectKey = new SubjectKeyIdentifier(responderKey); logger.log(Level.INFO, "Responder Key: {0}", Arrays.toString(responderKey)); for (X509CertificateHolder certHolder : certs) { try { X509Certificate tempCert = new JcaX509CertificateConverter() .setProvider("BC").getCertificate(certHolder); SubjectKeyIdentifier subjectKeyIdentifier = null; if (certHolder.getExtensions() != null) { subjectKeyIdentifier = SubjectKeyIdentifier.fromExtensions(certHolder.getExtensions()); } if (subjectKeyIdentifier != null) { logger.log(Level.INFO, "Certificate: {0}\nSubject Key Id: {1}", new Object[] {tempCert.getSubjectDN().getName(), Arrays.toString(subjectKeyIdentifier.getKeyIdentifier())}); } if (subjectKeyIdentifier != null && responderSubjectKey.equals(subjectKeyIdentifier)) { signingCert = tempCert; logger.log(Level.INFO, "Found a signer certificate \"{0}\" with the subject key extension value matching the responder key", signingCert.getSubjectDN().getName()); break; } subjectKeyIdentifier = new JcaX509ExtensionUtils().createSubjectKeyIdentifier(tempCert.getPublicKey()); if (responderSubjectKey.equals(subjectKeyIdentifier)) { signingCert = tempCert; logger.log(Level.INFO, "Found a certificate \"{0}\" with the subject key matching the OCSP responder key", signingCert.getSubjectDN().getName()); break; } } catch (CertificateException e) { logger.log(Level.FINE, e.getMessage()); } } } } if (signingCert != null) { if (signingCert.equals(issuerCertificate)) { logger.log(Level.INFO, "OCSP response is signed by the target\'s Issuing CA"); } else if (responderCertificate != null && signingCert.equals(responderCertificate)) { // https://www.ietf.org/rfc/rfc2560.txt // 2.6 OCSP Signature Authority Delegation // - The responder certificate is issued to the responder by CA logger.log(Level.INFO, "OCSP response is signed by an authorized responder certificate"); } else { // 4.2.2.2 Authorized Responders // 3. Includes a value of id-ad-ocspSigning in an ExtendedKeyUsage // extension and is issued by the CA that issued the certificate in // question." if (!signingCert.getIssuerX500Principal().equals(issuerCertificate.getSubjectX500Principal())) { logger.log(Level.INFO, "Signer certificate's Issuer: {0}\nIssuer certificate's Subject: {1}", new Object[] {signingCert.getIssuerX500Principal().getName(), issuerCertificate.getSubjectX500Principal().getName()}); throw new CertPathValidatorException("Responder\'s certificate is not authorized to sign OCSP responses"); } try { List<String> purposes = signingCert.getExtendedKeyUsage(); if (purposes != null && !purposes.contains(KeyPurposeId.id_kp_OCSPSigning.getId())) { logger.log(Level.INFO, "OCSPSigning extended usage is not set"); throw new CertPathValidatorException("Responder\'s certificate not valid for signing OCSP responses"); } } catch (CertificateParsingException e) { logger.log(Level.FINE, "Failed to get certificate's extended key usage extension\n{0}", e.getMessage()); } if (date == null) { signingCert.checkValidity(); } else { signingCert.checkValidity(date); } try { Extension noOCSPCheck = new JcaX509CertificateHolder(signingCert).getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck); // TODO If the extension is present, the OCSP client can trust the // responder's certificate for the lifetime of the certificate. logger.log(Level.INFO, "OCSP no-check extension is {0} present", noOCSPCheck == null ? "not" : ""); } catch (CertificateEncodingException e) { logger.log(Level.FINE, "Certificate encoding exception: {0}", e.getMessage()); } try { signingCert.verify(issuerCertificate.getPublicKey()); logger.log(Level.INFO, "OCSP response is signed by an Authorized Responder"); } catch (GeneralSecurityException ex) { signingCert = null; } } } if (signingCert == null) { throw new CertPathValidatorException("Unable to verify OCSP Response\'s signature"); } else { if (!verifySignature(basicOcspResponse, signingCert)) { throw new CertPathValidatorException("Error verifying OCSP Response\'s signature"); } else { Extension responseNonce = basicOcspResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce); if (responseNonce != null && requestNonce != null && !Arrays.equals(requestNonce, responseNonce.getExtnValue().getOctets())) { throw new CertPathValidatorException("Nonces do not match."); } else { // See Sun's OCSP implementation. // https://www.ietf.org/rfc/rfc2560.txt, if nextUpdate is not set, // the responder is indicating that newer update is avilable all the time long current = date == null ? System.currentTimeMillis() : date.getTime(); Date stop = new Date(current + (long) TIME_SKEW); Date start = new Date(current - (long) TIME_SKEW); Iterator<SingleResp> iter = Arrays.asList(basicOcspResponse.getResponses()).iterator(); SingleResp singleRes = null; do { if (!iter.hasNext()) { return; } singleRes = iter.next(); } while (!stop.before(singleRes.getThisUpdate()) && !start.after(singleRes.getNextUpdate() != null ? singleRes.getNextUpdate() : singleRes.getThisUpdate())); throw new CertPathValidatorException("Response is unreliable: its validity interval is out-of-date"); } } } } private static boolean verifySignature(BasicOCSPResp basicOcspResponse, X509Certificate cert) { try { ContentVerifierProvider contentVerifier = new JcaContentVerifierProviderBuilder() .setProvider("BC").build(cert.getPublicKey()); return basicOcspResponse.isSignatureValid(contentVerifier); } catch (OperatorCreationException e) { logger.log(Level.FINE, "Unable to construct OCSP content signature verifier\n{0}", e.getMessage()); } catch (OCSPException e) { logger.log(Level.FINE, "Unable to validate OCSP response signature\n{0}", e.getMessage()); } return false; } private static OCSPRevocationStatus unknownStatus() { return new OCSPRevocationStatus() { @Override public RevocationStatus getRevocationStatus() { return RevocationStatus.UNKNOWN; } @Override public Date getRevocationTime() { return new Date(System.currentTimeMillis()); } @Override public CRLReason getRevocationReason() { return CRLReason.lookup(CRLReason.unspecified); } }; } private static OCSPRevocationStatus singleResponseToRevocationStatus(final SingleResp singleResponse) throws CertPathValidatorException { final CertificateStatus certStatus = singleResponse.getCertStatus(); int revocationReason = CRLReason.unspecified; Date revocationTime = null; RevocationStatus status = RevocationStatus.UNKNOWN; if (certStatus == CertificateStatus.GOOD) { status = RevocationStatus.GOOD; } else if (certStatus instanceof RevokedStatus) { RevokedStatus revoked = (RevokedStatus)certStatus; revocationTime = revoked.getRevocationTime(); status = RevocationStatus.REVOKED; if (revoked.hasRevocationReason()) { revocationReason = revoked.getRevocationReason(); } } else if (certStatus instanceof UnknownStatus) { status = RevocationStatus.UNKNOWN; } else { throw new CertPathValidatorException("Unrecognized revocation status received from OCSP."); } final RevocationStatus finalStatus = status; final Date finalRevocationTime = revocationTime; final int finalRevocationReason = revocationReason; return new OCSPRevocationStatus() { @Override public RevocationStatus getRevocationStatus() { return finalStatus; } @Override public Date getRevocationTime() { return finalRevocationTime; } @Override public CRLReason getRevocationReason() { return CRLReason.lookup(finalRevocationReason); } }; } /** * Extracts OCSP responder URI from X509 AIA v3 extension, if available. There can be * multiple responder URIs encoded in the certificate. * @param cert * @return a list of available responder URIs. * @throws CertificateEncodingException */ private static List<String> getResponderURIs(X509Certificate cert) throws CertificateEncodingException { LinkedList<String> responderURIs = new LinkedList<>(); JcaX509CertificateHolder holder = new JcaX509CertificateHolder(cert); Extension aia = holder.getExtension(Extension.authorityInfoAccess); if (aia != null) { try { ASN1InputStream in = new ASN1InputStream(aia.getExtnValue().getOctetStream()); ASN1Sequence seq = (ASN1Sequence)in.readObject(); AuthorityInformationAccess authorityInfoAccess = AuthorityInformationAccess.getInstance(seq); for (AccessDescription ad : authorityInfoAccess.getAccessDescriptions()) { if (ad.getAccessMethod().equals(AccessDescription.id_ad_ocsp)) { // See https://www.ietf.org/rfc/rfc2560.txt, 3.1 Certificate Content if (ad.getAccessLocation().getTagNo() == GeneralName.uniformResourceIdentifier) { DERIA5String value = DERIA5String.getInstance(ad.getAccessLocation().getName()); responderURIs.add(value.getString()); } } } } catch (IOException e) { e.printStackTrace(); } } return responderURIs; } }