diff --git a/ant/jdk13.xml b/ant/jdk13.xml index 74ad1e18b5..68fcd4af4c 100644 --- a/ant/jdk13.xml +++ b/ant/jdk13.xml @@ -111,8 +111,8 @@ - + @@ -246,6 +246,7 @@ + diff --git a/ant/jdk14.xml b/ant/jdk14.xml index 32916cc908..f0037ed388 100644 --- a/ant/jdk14.xml +++ b/ant/jdk14.xml @@ -70,7 +70,7 @@ - + @@ -175,6 +175,7 @@ + diff --git a/build1-1 b/build1-1 index 1106236e2e..258b7d70cf 100644 --- a/build1-1 +++ b/build1-1 @@ -102,8 +102,9 @@ find $jdk11src -name "*.java" -exec scripts/useseccert.sh \{\} \; rm -f org/bouncycastle/jce/provider/PKIXCertPathBuilderSpi_8.java rm -f org/bouncycastle/jce/provider/WrappedRevocationChecker.java rm -f org/bouncycastle/jce/provider/ProvRevocationChecker.java - rm -f org/bouncycastle/jce/provider/OcspCache.java rm -f org/bouncycastle/jce/provider/ProvOcspRevocationChecker.java + rm -f org/bouncycastle/jce/provider/OcspResponseManager.java + rm -f org/bouncycastle/jce/provider/test/OcspResponseManagerTest.java rm -rf org/bouncycastle/i18n/filter/test rm -rf org/bouncycastle/math/ec/test rm -rf org/bouncycastle/jce/provider/test/ECEncodingTest.java diff --git a/build1-2 b/build1-2 index 144b0d0a60..23b4705c9a 100644 --- a/build1-2 +++ b/build1-2 @@ -182,8 +182,9 @@ find $jdk12src -name "*.java" -exec scripts/usejcecert.sh \{\} \; rm -f org/bouncycastle/jce/provider/PKIXCertPathBuilderSpi_8.java rm -f org/bouncycastle/jce/provider/WrappedRevocationChecker.java rm -f org/bouncycastle/jce/provider/ProvRevocationChecker.java - rm -f org/bouncycastle/jce/provider/OcspCache.java rm -f org/bouncycastle/jce/provider/ProvOcspRevocationChecker.java + rm -f org/bouncycastle/jce/provider/OcspResponseManager.java + rm -f org/bouncycastle/jce/provider/test/OcspResponseManagerTest.java rm -rf org/bouncycastle/x509/PKIXAttrCert*.java rm -rf org/bouncycastle/jce/provider/RFC3281*.java rm -rf org/bouncycastle/jcajce/PKCS12StoreParameter.java diff --git a/docs/releasenotes.html b/docs/releasenotes.html index d3f60c7e14..aa6f4f018d 100644 --- a/docs/releasenotes.html +++ b/docs/releasenotes.html @@ -24,10 +24,13 @@

2.0 Release History

2.1.2 Defects Fixed

  • Leading zeroes were sometimes dropped from Ed25519 signatures leading to verification errors in the PGP API. This has been fixed.
  • +
  • Issue when getting BC ProvRevocationChecker (engineGetRevocationChecker), then adding it to the PKIXBuilderParams from the client, causing the ProvOcspRevocationChecker to have an old "parent" reference without ocspResponses added.

2.1.3 Additional Features and Functionality

  • BCJSSE: Added support for security property "jdk.tls.server.defaultDHEParameters" (disabled in FIPS mode).
  • +
  • BCJSSE: Support has been added for server-side OCSP stapling (status_request and status_request_v2) for TLSv1.2 and TLSv1.3 during TLS handshake.
  • +
  • New property has been added (org.bouncycastle.prov.revocation.checker.no-fallback) to complement RevocationChecker's NO_FALLBACK option.

2.2.1 Version

@@ -100,6 +103,9 @@

2.3.5 Security Advisories.

  • CVE-2024-30171 - Possible timing based leakage in RSA based handshakes due to exception processing eliminated.
  • CVE-2024-30172 - Crafted signature and public key can be used to trigger an infinite loop in the Ed25519 verification code.
  • CVE-2024-34447 - When endpoint identification is enabled in the BCJSSE and an SSL socket is not created with an explicit hostname (as happens with HttpsURLConnection), hostname verification could be performed against a DNS-resolved IP address. This has been fixed.
  • +
  • BCJSSE: For OCSP server stapling to be enabled, the property jdk.tls.server.enableStatusRequestExtension must be +set to true. Other properties that control stapling are also available: jdk.tls.server.[cacheSize | cacheLifetime +| responseTimeout | responderURI | responderOverride | ignoreExtensions]
  • 2.4.1 Version

    diff --git a/prov/src/main/java/org/bouncycastle/jce/provider/OcspCache.java b/prov/src/main/java/org/bouncycastle/jce/provider/OcspCache.java deleted file mode 100644 index 06817817d9..0000000000 --- a/prov/src/main/java/org/bouncycastle/jce/provider/OcspCache.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.bouncycastle.jce.provider; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.ref.WeakReference; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.security.cert.CertPathValidatorException; -import java.security.cert.Extension; -import java.security.cert.X509Certificate; -import java.text.ParseException; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; - -import org.bouncycastle.asn1.ASN1EncodableVector; -import org.bouncycastle.asn1.ASN1GeneralizedTime; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.ASN1OctetString; -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.ocsp.BasicOCSPResponse; -import org.bouncycastle.asn1.ocsp.CertID; -import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; -import org.bouncycastle.asn1.ocsp.OCSPRequest; -import org.bouncycastle.asn1.ocsp.OCSPResponse; -import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; -import org.bouncycastle.asn1.ocsp.Request; -import org.bouncycastle.asn1.ocsp.ResponseBytes; -import org.bouncycastle.asn1.ocsp.ResponseData; -import org.bouncycastle.asn1.ocsp.SingleResponse; -import org.bouncycastle.asn1.ocsp.TBSRequest; -import org.bouncycastle.asn1.x509.Extensions; -import org.bouncycastle.jcajce.PKIXCertRevocationCheckerParameters; -import org.bouncycastle.jcajce.util.JcaJceHelper; -import org.bouncycastle.util.io.Streams; - -class OcspCache -{ - private static final int DEFAULT_TIMEOUT = 15000; - private static final int DEFAULT_MAX_RESPONSE_SIZE = 32 * 1024; - - private static Map>> cache - = Collections.synchronizedMap(new WeakHashMap>>()); - - static OCSPResponse getOcspResponse( - CertID certID, PKIXCertRevocationCheckerParameters parameters, - URI ocspResponder, X509Certificate responderCert, List ocspExtensions, - JcaJceHelper helper) - throws CertPathValidatorException - { - Map responseMap = null; - - WeakReference> markerRef = cache.get(ocspResponder); - if (markerRef != null) - { - responseMap = markerRef.get(); - } - - if (responseMap != null) - { - OCSPResponse response = responseMap.get(certID); - if (response != null) - { - BasicOCSPResponse basicResp = BasicOCSPResponse.getInstance( - ASN1OctetString.getInstance(response.getResponseBytes().getResponse()).getOctets()); - - ResponseData responseData = ResponseData.getInstance(basicResp.getTbsResponseData()); - - ASN1Sequence s = responseData.getResponses(); - - for (int i = 0; i != s.size(); i++) - { - SingleResponse resp = SingleResponse.getInstance(s.getObjectAt(i)); - - if (certID.equals(resp.getCertID())) - { - ASN1GeneralizedTime nextUp = resp.getNextUpdate(); - try - { - if (nextUp != null && parameters.getValidDate().after(nextUp.getDate())) - { - responseMap.remove(certID); - response = null; - } - } - catch (ParseException e) - { - // this should never happen, but... - responseMap.remove(certID); - response = null; - } - } - } - if (response != null) - { - return response; - } - } - } - - URL ocspUrl; - try - { - ocspUrl = ocspResponder.toURL(); - } - catch (MalformedURLException e) - { - throw new CertPathValidatorException("configuration error: " + e.getMessage(), - e, parameters.getCertPath(), parameters.getIndex()); - } - - // - // basic request generation - // - ASN1EncodableVector requests = new ASN1EncodableVector(); - - requests.add(new Request(certID, null)); - - List exts = ocspExtensions; - ASN1EncodableVector requestExtensions = new ASN1EncodableVector(); - - byte[] nonce = null; - for (int i = 0; i != exts.size(); i++) - { - Extension ext = (Extension)exts.get(i); - byte[] value = ext.getValue(); - - if (OCSPObjectIdentifiers.id_pkix_ocsp_nonce.getId().equals(ext.getId())) - { - nonce = value; - } - - requestExtensions.add(new org.bouncycastle.asn1.x509.Extension( - new ASN1ObjectIdentifier(ext.getId()), ext.isCritical(), value)); - } - - // TODO: configure originator - TBSRequest tbsReq; - if (requestExtensions.size() != 0) - { - tbsReq = new TBSRequest(null, new DERSequence(requests), - Extensions.getInstance(new DERSequence(requestExtensions))); - } - else - { - tbsReq = new TBSRequest(null, new DERSequence(requests), (Extensions)null); - } - - org.bouncycastle.asn1.ocsp.Signature signature = null; - - try - { - - byte[] request = new OCSPRequest(tbsReq, signature).getEncoded(); - - HttpURLConnection ocspCon = (HttpURLConnection)ocspUrl.openConnection(); - ocspCon.setConnectTimeout(DEFAULT_TIMEOUT); - ocspCon.setReadTimeout(DEFAULT_TIMEOUT); - ocspCon.setDoOutput(true); - ocspCon.setDoInput(true); - ocspCon.setRequestMethod("POST"); - ocspCon.setRequestProperty("Content-type", "application/ocsp-request"); - ocspCon.setRequestProperty("Content-length", String.valueOf(request.length)); - - OutputStream reqOut = ocspCon.getOutputStream(); - reqOut.write(request); - reqOut.flush(); - - InputStream reqIn = ocspCon.getInputStream(); - int contentLength = ocspCon.getContentLength(); - if (contentLength < 0) - { - // TODO: make configurable - contentLength = DEFAULT_MAX_RESPONSE_SIZE; - } - OCSPResponse response = OCSPResponse.getInstance(Streams.readAllLimited(reqIn, contentLength)); - - if (OCSPResponseStatus.SUCCESSFUL == response.getResponseStatus().getIntValue()) - { - boolean validated = false; - ResponseBytes respBytes = ResponseBytes.getInstance(response.getResponseBytes()); - - if (respBytes.getResponseType().equals(OCSPObjectIdentifiers.id_pkix_ocsp_basic)) - { - BasicOCSPResponse basicResp = BasicOCSPResponse.getInstance(respBytes.getResponse().getOctets()); - - validated = ProvOcspRevocationChecker.validatedOcspResponse(basicResp, parameters, nonce, responderCert, helper); - } - - if (!validated) - { - throw new CertPathValidatorException( - "OCSP response failed to validate", null, parameters.getCertPath(), parameters.getIndex()); - } - - markerRef = cache.get(ocspResponder); - if (markerRef != null) - { - responseMap = markerRef.get(); - } - - if (responseMap != null) - { - responseMap.put(certID, response); - } - else - { - responseMap = new HashMap(); - responseMap.put(certID, response); - cache.put(ocspResponder, new WeakReference>(responseMap)); - } - - return response; - } - else - { - throw new CertPathValidatorException( - "OCSP responder failed: " + response.getResponseStatus().getValue(), - null, parameters.getCertPath(), parameters.getIndex()); - } - } - catch (IOException e) - { - throw new CertPathValidatorException("configuration error: " + e.getMessage(), - e, parameters.getCertPath(), parameters.getIndex()); - } - } -} diff --git a/prov/src/main/java/org/bouncycastle/jce/provider/OcspResponseManager.java b/prov/src/main/java/org/bouncycastle/jce/provider/OcspResponseManager.java new file mode 100644 index 0000000000..9065e24f96 --- /dev/null +++ b/prov/src/main/java/org/bouncycastle/jce/provider/OcspResponseManager.java @@ -0,0 +1,524 @@ +package org.bouncycastle.jce.provider; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.security.MessageDigest; +import java.security.cert.CertPathValidatorException; +import java.security.cert.Extension; +import java.security.cert.X509Certificate; +import java.text.ParseException; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.logging.Logger; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1GeneralizedTime; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1String; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.ocsp.BasicOCSPResponse; +import org.bouncycastle.asn1.ocsp.CertID; +import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; +import org.bouncycastle.asn1.ocsp.OCSPRequest; +import org.bouncycastle.asn1.ocsp.OCSPResponse; +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; +import org.bouncycastle.asn1.ocsp.Request; +import org.bouncycastle.asn1.ocsp.ResponseData; +import org.bouncycastle.asn1.ocsp.SingleResponse; +import org.bouncycastle.asn1.ocsp.TBSRequest; +import org.bouncycastle.asn1.x509.AccessDescription; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.AuthorityInformationAccess; +import org.bouncycastle.asn1.x509.Certificate; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.internal.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.jcajce.util.JcaJceHelper; +import org.bouncycastle.jcajce.util.MessageDigestUtils; +import org.bouncycastle.util.Properties; +import org.bouncycastle.util.io.Streams; + +public class OcspResponseManager +{ + private static final Logger LOG = Logger.getLogger(OcspResponseManager.class.getName()); + + private static final int DEFAULT_CONNECT_TIMEOUT = 15000; // milliseconds + private static final int DEFAULT_READ_TIMEOUT = 15000; // milliseconds + private static final int DEFAULT_MAX_RESPONSE_SIZE = 32 * 1024; + + private static final int DEFAULT_CACHE_SIZE = 256; + private static final int DEFAULT_CACHE_LIFETIME = 3600; // seconds + + // Revocation check OCSP parameters. + private static boolean ocspEnable = !Properties.isOverrideSetTo("ocsp.enable", false); // OCSP validation enabled by default unless explicit override + private static String ocspURL = Properties.getPropertyValue("ocsp.responderURL"); + private static int ocspTimeout = Properties.asInteger("com.sun.security.ocsp.timeout", DEFAULT_CONNECT_TIMEOUT); + private static int ocspReadTimeout = Properties.asInteger("com.sun.security.ocsp.readtimeout", DEFAULT_READ_TIMEOUT); + + // Server OCSP stapling parameters. + private static boolean responderOverride = Properties.isOverrideSet("jdk.tls.stapling.responderOverride"); + private static String responderUri = Properties.getPropertyValue("jdk.tls.stapling.responderURI"); + private static int responseTimeout = Properties.asInteger("jdk.tls.stapling.responseTimeout", DEFAULT_READ_TIMEOUT); + private static boolean ignoreExtensions = Properties.isOverrideSet("jdk.tls.stapling.ignoreExtensions"); + + // Caching parameters. + private static int cacheSize = Properties.asInteger("jdk.tls.stapling.cacheSize", DEFAULT_CACHE_SIZE); + private static int cacheLifetime = Properties.asInteger("jdk.tls.stapling.cacheLifetime", DEFAULT_CACHE_LIFETIME); + + // OCSP response cache. + private static final Map cache = Collections.synchronizedMap(new WeakHashMap()); + + /** + * Convenience method to reset all the parameters. + */ + public static void reset() + { + ocspEnable = !Properties.isOverrideSetTo("ocsp.enable", false); + ocspURL = Properties.getPropertyValue("ocsp.responderURL"); + ocspTimeout = Properties.asInteger("com.sun.security.ocsp.timeout", DEFAULT_CONNECT_TIMEOUT); + ocspReadTimeout = Properties.asInteger("com.sun.security.ocsp.readtimeout", DEFAULT_READ_TIMEOUT); + + responderOverride = Properties.isOverrideSet("jdk.tls.stapling.responderOverride"); + responderUri = Properties.getPropertyValue("jdk.tls.stapling.responderURI"); + responseTimeout = Properties.asInteger("jdk.tls.stapling.responseTimeout", DEFAULT_READ_TIMEOUT); + ignoreExtensions = Properties.isOverrideSet("jdk.tls.stapling.ignoreExtensions"); + + cacheSize = Properties.asInteger("jdk.tls.stapling.cacheSize", DEFAULT_CACHE_SIZE); + cacheLifetime = Properties.asInteger("jdk.tls.stapling.cacheLifetime", DEFAULT_CACHE_LIFETIME); + + cache.clear(); + } + + public static OCSPResponse getOCSPResponseForRevocationCheck(X509Certificate cert, X509Certificate issuer, List extensionList, URI parentOcspURI, JcaJceHelper helper) throws CertPathValidatorException + { + // check OCSP revocation checking is disabled + if (!ocspEnable) + { + LOG.warning("[revocation check] OCSP disabled by \"ocsp.enable\" setting"); + throw new RecoverableCertPathValidatorException("[revocation check] OCSP disabled by \"ocsp.enable\" setting", null, null, -1); + } + + LOG.info("[revocation check] Getting OCSP response for cert: " + cert.getSubjectX500Principal()); + + // create valid CertID + CertID certID = createCertID(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1), issuer, new ASN1Integer(cert.getSerialNumber()), helper); + if (certID == null) + { + throw new CertPathValidatorException("[revocation check] Error creating CertID for certificate: " + cert.getSubjectX500Principal(), null, null, -1); + } + + // handle ocsp_no_check extension of the cert + if (cert.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId()) != null) + { + LOG.info("[revocation check] Found ocsp_no_check extension. Skipping OCSP retrieval for cert: " + cert.getSubjectX500Principal()); + return null; + } + + // validate responder URL + URL responderURL = getResponderURLForRevocationCheck(cert, parentOcspURI); + + // build extensions + Extensions extensions = buildExtensions(extensionList); + + // at this point we have a valid responder, so we need to make an HTTP request + LOG.info("[revocation check] Sending POST request to: " + responderURL); + try + { + byte[] request = buildRequest(certID, extensions); + + // make HTTP call + OCSPResponse response = sendPostRequest(responderURL, request, ocspTimeout, ocspReadTimeout); + if (OCSPResponseStatus.SUCCESSFUL == response.getResponseStatus().getIntValue()) + { + LOG.info("[revocation check] Successfully retrieved OCSP response for cert: " + cert.getSubjectX500Principal()); + return response; + } + } + catch (IOException e) + { + LOG.warning("[revocation check] Network error while trying to retrieve OCSP response for cert: " + cert.getSubjectX500Principal() + " from responder URL: " + responderURL); + throw new RecoverableCertPathValidatorException("[revocation check] Network error while trying to retrieve OCSP response for cert: " + cert.getSubjectX500Principal() + " from responder URL: " + responderURL, e, null, -1); + } + return null; + } + + public static OCSPResponse getOCSPResponseForStapling(X509Certificate cert, X509Certificate issuerX509, Extensions extensions, JcaJceHelper helper) + { + LOG.info("[stapling] Getting OCSP response for cert: " + cert.getSubjectX500Principal()); + + // create CertID + CertID certID = createCertID(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1), issuerX509, new ASN1Integer(cert.getSerialNumber()), helper); + if (certID == null) + { + // if we were unable to build a CertID then there is no point on going forward + LOG.warning("[stapling] Unable to build a CertID for cert: " + cert.getSubjectX500Principal()); + return null; + } + + CachedOCSPResponse cachedResponse = getCachedOCSPResponse(certID, extensions); + if (cachedResponse != null) + { + LOG.info("[stapling] Found cached OCSP response for cert: " + cert.getSubjectX500Principal()); + return cachedResponse.response; + } + + // handle ocsp_no_check extension + if (cert.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId()) != null) + { + LOG.info("[stapling] Found ocsp_no_check extension. Skipping OCSP retrieval for cert: " + cert.getSubjectX500Principal()); + return null; + } + + // if not found retrieve it via http + URL responderUrl = getResponderURLForServer(cert); + if (responderUrl == null) + { + // if we could not retrieve a valid responder URL then return a null response + LOG.warning("[stapling] Unable to find a valid responder URL for cert: " + cert.getSubjectX500Principal()); + return null; + } + + LOG.info("[stapling] Sending POST request to: " + responderUrl); + try + { + // build the OCSP request + byte[] request = buildRequest(certID, ignoreExtensions ? null : extensions); + + // make HTTP call + OCSPResponse response = sendPostRequest(responderUrl, request, responseTimeout, responseTimeout); + if (OCSPResponseStatus.SUCCESSFUL == response.getResponseStatus().getIntValue()) + { + LOG.info("[stapling] Successfully retrieved OCSP response for cert: " + cert.getSubjectX500Principal()); + cacheOCSPResponse(certID, response); + return response; + } + } + catch (IOException e) + { + LOG.warning("[stapling] Network error while trying to retrieve OCSP response for cert: " + cert.getSubjectX500Principal() + " from responder URL: " + responderUrl); + } + return null; + } + + private static URL getResponderURLForRevocationCheck(X509Certificate cert, URI parentOcspURI) throws CertPathValidatorException + { + LOG.info("[revocation check] Getting OCSP responder URL for cert: " + cert.getSubjectX500Principal()); + // try to get parent responder URL + URL responderURL = null; + if (parentOcspURI != null) + { + try + { + responderURL = parentOcspURI.toURL(); + LOG.info("[revocation check] Found OCSP responder URL from parent PKIXRevocationChecker: " + responderURL); + } + catch (Exception e) + { + // invalid responder URL - fallback to configured responder + } + } + // if not found try to get configured responder URL + if (responderURL == null && ocspURL != null && !ocspURL.isEmpty()) + { + try + { + responderURL = URI.create(ocspURL).toURL(); + LOG.info("[revocation check] Found OCSP responder URL from property ocsp.responderURL: " + responderURL); + } + catch (Exception e) + { + // misconfigured ocsp.responderURL + throw new CertPathValidatorException("[revocation check] Misconfigured property ocsp.responderURL: " + ocspURL, e); + } + } + // if still not found try to get the responder URL from the AIA extension of the cert + if (responderURL == null) + { + responderURL = getResponderURLFromCert(cert); + if (responderURL != null) + { + LOG.info("[revocation check] Found OCSP responder URL from cert AIA extension: " + responderURL); + } + } + // if still not found at this point then block the request + if (responderURL == null) + { + LOG.warning("[revocation check] Unable to find a valid OCSP responder URL for cert: " + cert.getSubjectX500Principal()); + throw new RecoverableCertPathValidatorException("[revocation check] Unable to find a valid OCSP responder URL for cert: " + cert.getSubjectX500Principal(), null, null, -1); + } + return responderURL; + } + + private static URL getResponderURLForServer(X509Certificate cert) + { + LOG.info("[stapling] Getting OCSP responder URL for cert: " + cert.getSubjectX500Principal()); + + URL responderUrl = null; + if (responderOverride && responderUri != null && !responderUri.isEmpty()) + { + try + { + responderUrl = URI.create(responderUri).toURL(); + LOG.info("[stapling] Found OCSP responder URL from property jdk.tls.stapling.responderURI: " + responderUrl); + } + catch (Exception e) + { + // invalid responder URL - fallback to cert responder + } + } + // if not found try to get the responder URL from the AIA extension of the cert + if (responderUrl == null) + { + responderUrl = getResponderURLFromCert(cert); + if (responderUrl != null) + { + LOG.info("[stapling] Found OCSP responder URL from cert AIA extension: " + responderUrl); + } + } + return responderUrl; + } + + private static URL getResponderURLFromCert(X509Certificate cert) + { + byte[] extValue = cert.getExtensionValue(org.bouncycastle.asn1.x509.Extension.authorityInfoAccess.getId()); + if (extValue != null) + { + AuthorityInformationAccess aiAccess = AuthorityInformationAccess.getInstance(ASN1OctetString.getInstance(extValue).getOctets()); + AccessDescription[] descriptions = aiAccess.getAccessDescriptions(); + for (int i = 0; i != descriptions.length; i++) + { + AccessDescription aDesc = descriptions[i]; + if (AccessDescription.id_ad_ocsp.equals(aDesc.getAccessMethod())) + { + GeneralName name = aDesc.getAccessLocation(); + if (name.getTagNo() == GeneralName.uniformResourceIdentifier) + { + try + { + return URI.create(((ASN1String) name.getName()).getString()).toURL(); + } + catch (Exception e) + { + // ignore... + } + } + } + } + } + return null; + } + + private static CertID createCertID(AlgorithmIdentifier digestAlg, X509Certificate issuerX509, ASN1Integer serialNumber, JcaJceHelper helper) + { + try + { + Certificate issuerCert = Certificate.getInstance(issuerX509.getEncoded()); + MessageDigest digest = helper.createMessageDigest(MessageDigestUtils.getDigestName(digestAlg.getAlgorithm())); + ASN1OctetString issuerNameHash = new DEROctetString(digest.digest(issuerCert.getSubject().getEncoded(ASN1Encoding.DER))); + ASN1OctetString issuerKeyHash = new DEROctetString(digest.digest(issuerCert.getSubjectPublicKeyInfo().getPublicKeyData().getBytes())); + return new CertID(digestAlg, issuerNameHash, issuerKeyHash, serialNumber); + } + catch (Exception e) + { + return null; + } + } + + private static byte[] buildRequest(CertID certID, Extensions extensions) throws IOException + { + ASN1EncodableVector requests = new ASN1EncodableVector(); + requests.add(new Request(certID, null)); + + // build request + TBSRequest tbsReq = new TBSRequest(null, new DERSequence(requests), extensions); + + // JSSE doesn't provide a signature when sending the request + return new OCSPRequest(tbsReq, null).getEncoded(); + } + + private static Extensions buildExtensions(List extensionList) + { + // build extensions + ASN1EncodableVector extVector = new ASN1EncodableVector(); + for (int i = 0; i < extensionList.size(); i++) + { + Extension extension = extensionList.get(i); + extVector.add(new org.bouncycastle.asn1.x509.Extension(new ASN1ObjectIdentifier(extension.getId()), extension.isCritical(), extension.getValue())); + } + return extVector.size() > 0 ? Extensions.getInstance(new DERSequence(extVector)) : null; + } + + private static OCSPResponse sendPostRequest(URL ocspUrl, byte[] ocspRequest, int connectTimeout, int readTimeout) throws IOException + { + HttpURLConnection ocspCon = null; + try + { + ocspCon = (HttpURLConnection) ocspUrl.openConnection(); + ocspCon.setConnectTimeout(connectTimeout); + ocspCon.setReadTimeout(readTimeout); + ocspCon.setDoOutput(true); + ocspCon.setDoInput(true); + ocspCon.setRequestMethod("POST"); + ocspCon.setRequestProperty("Content-type", "application/ocsp-request"); + ocspCon.setRequestProperty("Content-length", String.valueOf(ocspRequest.length)); + + OutputStream reqOut = ocspCon.getOutputStream(); + reqOut.write(ocspRequest); + reqOut.flush(); + + InputStream reqIn = ocspCon.getInputStream(); + int contentLength = ocspCon.getContentLength(); + if (contentLength < 0) + { + contentLength = DEFAULT_MAX_RESPONSE_SIZE; + } + return OCSPResponse.getInstance(Streams.readAllLimited(reqIn, contentLength)); + } + finally + { + if (ocspCon != null) + { + ocspCon.disconnect(); + } + } + } + + private static CachedOCSPResponse getCachedOCSPResponse(CertID certID, Extensions extensions) + { + // if nonce extension is present in the request extensions don't retrieve from cache + if (extensions != null && extensions.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce) != null) + { + LOG.fine("Nonce found in the request, skipping OCSP response cache check"); + return null; + } + CachedOCSPResponse cachedResponse = cache.get(certID); + if (cachedResponse != null) + { + if (!isResponseExpired(cachedResponse)) + { + return cachedResponse; + } + else + { + LOG.fine("Expired cached OCSP response"); + } + } + return null; + } + + private static void cacheOCSPResponse(CertID certID, OCSPResponse response) + { + // check cache is enabled + if (cacheLifetime > 0 && cacheSize > 0) + { + // first clean expired entries from the cache + cleanCache(); + + // cache only if the cache is not full + if (cache.size() < cacheSize) + { + Date nextUpdate = getNextUpdateFromResponse(response, certID); + + // add the OCSP response to the cache with the current timestamp + cache.put(certID, new CachedOCSPResponse(response, System.currentTimeMillis(), nextUpdate)); + LOG.fine("Cached OCSP response for CertID: " + certID.getSerialNumber()); + } + } + } + + private static void cleanCache() + { + int cleanedEntries = 0; + Iterator> iterator = cache.entrySet().iterator(); + while (iterator.hasNext()) + { + Map.Entry entry = iterator.next(); + if (isResponseExpired(entry.getValue())) + { + iterator.remove(); + cleanedEntries++; + } + } + if (cleanedEntries > 0) + { + LOG.fine("Cleaned expired cache entries: " + cleanedEntries); + } + } + + private static boolean isResponseExpired(CachedOCSPResponse cachedResponse) + { + boolean exceededLifetime = (System.currentTimeMillis() - cachedResponse.timestamp) > cacheLifetime * 1000L; // cache lifetime is in seconds + boolean exceededNextUpdate = cachedResponse.nextUpdate != null && cachedResponse.nextUpdate.before(new Date()); + return exceededLifetime || exceededNextUpdate; + } + + private static Date getNextUpdateFromResponse(OCSPResponse response, CertID certID) + { + // try to find the nextUpdate extension on the response + Date nextUpdate = null; + + BasicOCSPResponse basicResp = BasicOCSPResponse.getInstance(ASN1OctetString.getInstance(response.getResponseBytes().getResponse()).getOctets()); + if (basicResp != null) + { + ResponseData responseData = ResponseData.getInstance(basicResp.getTbsResponseData()); + if (responseData != null) + { + ASN1Sequence responses = responseData.getResponses(); + + for (int i = 0; i < responses.size(); i++) + { + SingleResponse resp = SingleResponse.getInstance(responses.getObjectAt(i)); + if (resp != null) + { + if (certID.equals(resp.getCertID())) + { + ASN1GeneralizedTime nextUp = resp.getNextUpdate(); + if (nextUp != null) + { + try + { + nextUpdate = nextUp.getDate(); + LOG.fine("Found nextUpdate field on response: " + nextUpdate); + } + catch (ParseException e) + { + // should not happen + } + } + break; + } + } + } + } + } + return nextUpdate; + } + + private static final class CachedOCSPResponse + { + final OCSPResponse response; + final long timestamp; + final Date nextUpdate; + + CachedOCSPResponse(OCSPResponse response, long timestamp, Date nextUpdate) + { + this.response = response; + this.timestamp = timestamp; + this.nextUpdate = nextUpdate; + } + } + +} diff --git a/prov/src/main/java/org/bouncycastle/jce/provider/ProvOcspRevocationChecker.java b/prov/src/main/java/org/bouncycastle/jce/provider/ProvOcspRevocationChecker.java index 1d697023f0..52166afe29 100644 --- a/prov/src/main/java/org/bouncycastle/jce/provider/ProvOcspRevocationChecker.java +++ b/prov/src/main/java/org/bouncycastle/jce/provider/ProvOcspRevocationChecker.java @@ -3,7 +3,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -15,10 +14,11 @@ import java.security.cert.CertificateFactory; import java.security.cert.Extension; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.logging.Logger; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Encoding; @@ -27,7 +27,6 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.ASN1String; import org.bouncycastle.asn1.DERNull; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.cryptopro.CryptoProObjectIdentifiers; @@ -46,12 +45,9 @@ import org.bouncycastle.asn1.pkcs.RSASSAPSSparams; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStrictStyle; -import org.bouncycastle.asn1.x509.AccessDescription; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; -import org.bouncycastle.asn1.x509.AuthorityInformationAccess; import org.bouncycastle.asn1.x509.CRLReason; import org.bouncycastle.asn1.x509.Extensions; -import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; @@ -66,13 +62,11 @@ import org.bouncycastle.jcajce.util.MessageDigestUtils; import org.bouncycastle.jce.exception.ExtCertPathValidatorException; import org.bouncycastle.util.Arrays; -import org.bouncycastle.util.Properties; class ProvOcspRevocationChecker implements PKIXCertRevocationChecker { - private static final int DEFAULT_OCSP_TIMEOUT = 15000; - private static final int DEFAULT_OCSP_MAX_RESPONSE_SIZE = 32 * 1024; + private static final Logger LOG = Logger.getLogger(ProvOcspRevocationChecker.class.getName()); private static final Map oids = new HashMap(); @@ -118,16 +112,18 @@ class ProvOcspRevocationChecker oids.put(NISTObjectIdentifiers.dsa_with_sha256, "SHA256WITHDSA"); } - private final ProvRevocationChecker parent; private final JcaJceHelper helper; private PKIXCertRevocationCheckerParameters parameters; - private boolean isEnabledOCSP; - private String ocspURL; - public ProvOcspRevocationChecker(ProvRevocationChecker parent, JcaJceHelper helper) + // properties from the parent PKIXRevocationChecker + private Map ocspResponses = new HashMap(); + private List ocspExtensions = new ArrayList(); + private URI ocspResponder; + private X509Certificate ocspResponderCert; + + public ProvOcspRevocationChecker(JcaJceHelper helper) { - this.parent = parent; this.helper = helper; } @@ -139,8 +135,14 @@ public void setParameter(String name, Object value) public void initialize(PKIXCertRevocationCheckerParameters parameters) { this.parameters = parameters; - this.isEnabledOCSP = Properties.isOverrideSet("ocsp.enable"); - this.ocspURL = Properties.getPropertyValue("ocsp.responderURL"); + } + + public void update(Map ocspResponses, List ocspExtensions, URI ocspResponder, X509Certificate ocspResponderCert) + { + this.ocspResponses = ocspResponses; + this.ocspExtensions = ocspExtensions; + this.ocspResponder = ocspResponder; + this.ocspResponderCert = ocspResponderCert; } public List getSoftFailExceptions() @@ -157,71 +159,24 @@ public void init(boolean forForward) } this.parameters = null; - this.isEnabledOCSP = Properties.isOverrideSet("ocsp.enable"); - this.ocspURL = Properties.getPropertyValue("ocsp.responderURL"); - } - - public boolean isForwardCheckingSupported() - { - return false; - } - - public Set getSupportedExtensions() - { - return null; } public void check(Certificate certificate) throws CertPathValidatorException { - X509Certificate cert = (X509Certificate)certificate; - Map ocspResponses = parent.getOcspResponses(); - URI ocspUri = parent.getOcspResponder(); + X509Certificate cert = (X509Certificate) certificate; + LOG.info("[revocation check] OCSP check for cert: " + cert.getSubjectX500Principal()); - if (ocspUri == null) + if (ocspResponses.get(cert) == null) { - if (this.ocspURL != null) + LOG.info("[revocation check] No stapled OCSP response found"); + try { - try + OCSPResponse response = OcspResponseManager.getOCSPResponseForRevocationCheck(cert, parameters.getSigningCert(), ocspExtensions, ocspResponder, helper); + if (response != null) { - ocspUri = new URI(this.ocspURL); + ocspResponses.put(cert, response.getEncoded()); } - catch (URISyntaxException e) - { - throw new CertPathValidatorException("configuration error: " + e.getMessage(), - e, parameters.getCertPath(), parameters.getIndex()); - } - } - else - { - ocspUri = getOcspResponderURI(cert); - } - } - - byte[] nonce = null; - boolean preValidated = false; - if (ocspResponses.get(cert) == null && ocspUri != null) - { - // if we're here we need to make a network access, if we haven't been given a URL explicitly block it. - if (ocspURL == null - && parent.getOcspResponder() == null - && !isEnabledOCSP) - { - throw new RecoverableCertPathValidatorException("OCSP disabled by \"ocsp.enable\" setting", - null, parameters.getCertPath(), parameters.getIndex()); - } - - org.bouncycastle.asn1.x509.Certificate issuer = extractCert(); - - // TODO: configure hash algorithm - CertID id = createCertID(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1), issuer, new ASN1Integer(cert.getSerialNumber())); - - OCSPResponse response = OcspCache.getOcspResponse(id, parameters, ocspUri, parent.getOcspResponderCert(), parent.getOcspExtensions(), helper); - - try - { - ocspResponses.put(cert, response.getEncoded()); - preValidated = true; } catch (IOException e) { @@ -231,19 +186,23 @@ public void check(Certificate certificate) } else { - List exts = parent.getOcspExtensions(); - for (int i = 0; i != exts.size(); i++) - { - Extension ext = (Extension)exts.get(i); - byte[] value = ext.getValue(); + LOG.info("[revocation check] Found stapled OCSP response"); + } - if (OCSPObjectIdentifiers.id_pkix_ocsp_nonce.getId().equals(ext.getId())) - { - nonce = value; - } + // get the nonce from the request extensions to validate the response later + byte[] nonce = null; + for (int i = 0; i < ocspExtensions.size(); i++) + { + Extension ext = ocspExtensions.get(i); + byte[] value = ext.getValue(); + + if (OCSPObjectIdentifiers.id_pkix_ocsp_nonce.getId().equals(ext.getId())) + { + nonce = value; } } + // validate the OCSP responses if (!ocspResponses.isEmpty()) { OCSPResponse ocspResponse = OCSPResponse.getInstance(ocspResponses.get(cert)); @@ -261,7 +220,7 @@ public void check(Certificate certificate) { BasicOCSPResponse basicResp = BasicOCSPResponse.getInstance(respBytes.getResponse().getOctets()); - if (preValidated || validatedOcspResponse(basicResp, parameters, nonce, parent.getOcspResponderCert(), helper)) + if (validatedOcspResponse(basicResp, parameters, nonce, ocspResponderCert, helper)) { ResponseData responseData = ResponseData.getInstance(basicResp.getTbsResponseData()); @@ -290,6 +249,7 @@ public void check(Certificate certificate) if (resp.getCertStatus().getTagNo() == 0) { // we're good! + LOG.info("[revocation check] OCSP response successfully validated"); return; } if (resp.getCertStatus().getTagNo() == 1) @@ -340,43 +300,6 @@ public void check(Certificate certificate) } } - static URI getOcspResponderURI(X509Certificate cert) - { - byte[] extValue = cert.getExtensionValue(org.bouncycastle.asn1.x509.Extension.authorityInfoAccess.getId()); - if (extValue == null) - { - return null; - } - else - { - AuthorityInformationAccess aiAccess = AuthorityInformationAccess.getInstance( - ASN1OctetString.getInstance(extValue).getOctets()); - - AccessDescription[] descriptions = aiAccess.getAccessDescriptions(); - for (int i = 0; i != descriptions.length; i++) - { - AccessDescription aDesc = descriptions[i]; - if (AccessDescription.id_ad_ocsp.equals(aDesc.getAccessMethod())) - { - GeneralName name = aDesc.getAccessLocation(); - if (name.getTagNo() == GeneralName.uniformResourceIdentifier) - { - try - { - return new URI(((ASN1String)name.getName()).getString()); - } - catch (URISyntaxException e) - { - // ignore... - } - } - } - } - - return null; - } - } - static boolean validatedOcspResponse(BasicOCSPResponse basicResp, PKIXCertRevocationCheckerParameters parameters, byte[] nonce, X509Certificate responderCert, JcaJceHelper helper) throws CertPathValidatorException { diff --git a/prov/src/main/java/org/bouncycastle/jce/provider/ProvRevocationChecker.java b/prov/src/main/java/org/bouncycastle/jce/provider/ProvRevocationChecker.java index 86f03e8ae5..d734e4e03b 100644 --- a/prov/src/main/java/org/bouncycastle/jce/provider/ProvRevocationChecker.java +++ b/prov/src/main/java/org/bouncycastle/jce/provider/ProvRevocationChecker.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.logging.Logger; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.cryptopro.CryptoProObjectIdentifiers; @@ -23,13 +24,13 @@ import org.bouncycastle.jcajce.PKIXCertRevocationChecker; import org.bouncycastle.jcajce.PKIXCertRevocationCheckerParameters; import org.bouncycastle.jcajce.util.JcaJceHelper; +import org.bouncycastle.util.Properties; class ProvRevocationChecker extends PKIXRevocationChecker implements PKIXCertRevocationChecker { - private static final int DEFAULT_OCSP_TIMEOUT = 15000; - private static final int DEFAULT_OCSP_MAX_RESPONSE_SIZE = 32 * 1024; + private static final Logger LOG = Logger.getLogger(ProvRevocationChecker.class.getName()); private static final Map oids = new HashMap(); @@ -75,17 +76,15 @@ class ProvRevocationChecker oids.put(NISTObjectIdentifiers.dsa_with_sha256, "SHA256WITHDSA"); } - private final JcaJceHelper helper; private final ProvCrlRevocationChecker crlChecker; private final ProvOcspRevocationChecker ocspChecker; - - private PKIXCertRevocationCheckerParameters parameters; + private final boolean noFallbackOverride; public ProvRevocationChecker(JcaJceHelper helper) { - this.helper = helper; - this.crlChecker = new ProvCrlRevocationChecker(helper); - this.ocspChecker = new ProvOcspRevocationChecker(this, helper); + crlChecker = new ProvCrlRevocationChecker(helper); + ocspChecker = new ProvOcspRevocationChecker(helper); + noFallbackOverride = Properties.isOverrideSet("org.bouncycastle.prov.revocation.checker.no-fallback"); } public void setParameter(String name, Object value) @@ -95,9 +94,9 @@ public void setParameter(String name, Object value) public void initialize(PKIXCertRevocationCheckerParameters parameters) { - this.parameters = parameters; crlChecker.initialize(parameters); ocspChecker.initialize(parameters); + ocspChecker.update(getOcspResponses(), getOcspExtensions(), getOcspResponder(), getOcspResponderCert()); } public List getSoftFailExceptions() @@ -108,9 +107,8 @@ public List getSoftFailExceptions() public void init(boolean forForward) throws CertPathValidatorException { - this.parameters = null; - crlChecker.init(forForward); - ocspChecker.init(forForward); + crlChecker.init(forForward); + ocspChecker.init(forForward); } public boolean isForwardCheckingSupported() @@ -131,23 +129,27 @@ public void check(Certificate certificate, Collection collection) // only check end-entity certificates. if (hasOption(Option.ONLY_END_ENTITY) && cert.getBasicConstraints() != -1) { + LOG.info("[revocation check] ONLY_END_ENTITY option selected. Skipping cert: " + cert.getSubjectX500Principal()); return; } if (hasOption(Option.PREFER_CRLS)) { + LOG.info("[revocation check] PREFER_CRLS option selected. Checking CRLs for cert: " + cert.getSubjectX500Principal()); try { crlChecker.check(certificate); } catch (RecoverableCertPathValidatorException e) { - if (!hasOption(Option.NO_FALLBACK)) + LOG.severe("[revocation check] Error during CRL check for cert: " + cert.getSubjectX500Principal()); + if (!hasOption(Option.NO_FALLBACK) && !noFallbackOverride) { ocspChecker.check(certificate); } else { + LOG.warning("[revocation check] NO_FALLBACK option selected. Will not attempt to check OCSP"); throw e; } } @@ -160,12 +162,15 @@ public void check(Certificate certificate, Collection collection) } catch (RecoverableCertPathValidatorException e) { - if (!hasOption(Option.NO_FALLBACK)) + LOG.severe("[revocation check] Error during OCSP check for cert: " + cert.getSubjectX500Principal()); + if (!hasOption(Option.NO_FALLBACK) && !noFallbackOverride) { + LOG.info("[revocation check] Checking CRL for cert: " + cert.getSubjectX500Principal()); crlChecker.check(certificate); } else { + LOG.warning("[revocation check] NO_FALLBACK option selected. Will not attempt to check CRLs"); throw e; } } diff --git a/prov/src/main/java/org/bouncycastle/jce/provider/RecoverableCertPathValidatorException.java b/prov/src/main/java/org/bouncycastle/jce/provider/RecoverableCertPathValidatorException.java index 90a2478b27..412283619d 100644 --- a/prov/src/main/java/org/bouncycastle/jce/provider/RecoverableCertPathValidatorException.java +++ b/prov/src/main/java/org/bouncycastle/jce/provider/RecoverableCertPathValidatorException.java @@ -3,7 +3,7 @@ import java.security.cert.CertPath; import java.security.cert.CertPathValidatorException; -class RecoverableCertPathValidatorException +public class RecoverableCertPathValidatorException extends CertPathValidatorException { public RecoverableCertPathValidatorException(String msg, Throwable cause, CertPath certPath, int index) diff --git a/prov/src/test/java/org/bouncycastle/jce/provider/test/AllTests.java b/prov/src/test/java/org/bouncycastle/jce/provider/test/AllTests.java index 061f1fdb2f..e7f6290d0b 100644 --- a/prov/src/test/java/org/bouncycastle/jce/provider/test/AllTests.java +++ b/prov/src/test/java/org/bouncycastle/jce/provider/test/AllTests.java @@ -22,6 +22,7 @@ public static Test suite() TestSuite suite = new TestSuite("JCE Tests"); suite.addTestSuite(SimpleTestTest.class); + suite.addTestSuite(OcspResponseManagerTest.class); return new BCTestSetup(suite); } diff --git a/prov/src/test/java/org/bouncycastle/jce/provider/test/OcspResponseManagerTest.java b/prov/src/test/java/org/bouncycastle/jce/provider/test/OcspResponseManagerTest.java new file mode 100644 index 0000000000..1b00ad257d --- /dev/null +++ b/prov/src/test/java/org/bouncycastle/jce/provider/test/OcspResponseManagerTest.java @@ -0,0 +1,310 @@ +package org.bouncycastle.jce.provider.test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Random; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; +import org.bouncycastle.asn1.ocsp.OCSPResponse; +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.jcajce.util.BCJcaJceHelper; +import org.bouncycastle.jcajce.util.JcaJceHelper; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.provider.OcspResponseManager; +import org.bouncycastle.jce.provider.RecoverableCertPathValidatorException; +import org.bouncycastle.test.PrintTestResult; +import org.bouncycastle.util.encoders.Base64; +import org.junit.Assert; + +public class OcspResponseManagerTest extends TestCase +{ + private static final JcaJceHelper helper = new BCJcaJceHelper(); + // 2 certs with AIA extension with OCSP URI: http://localhost:30080/ + private static final byte[] cert = Base64.decode("MIIEsDCCAxigAwIBAgIUDnYM/pwIoSCjrZPrDzXcynrvwdkwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UEAwwKSVNTVUlOR0NBNDAeFw0yNDA2MDcxNTE4NDlaFw0yNTA2MDcxNTE0NTVaMDwxETAPBgNVBAMMCDAxMDAwMDA0MQwwCgYDVQQLDANLTUMxDDAKBgNVBAoMA1RIQTELMAkGA1UEBhMCR0IwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCucOM9+XjY3eV2kRy1IR/j8GVld3hENS4CnoHnqWcNKviBlaKGRPRUbd/w+8J7Uaw5oI7W7KoBeFaIJaXQ4V2Gn6JGEhGu6RaOHriIMObRpx31AinT1kYjIgWjHmrOB6E4LzaxkFoM+sbS2SKyZaWPgIKySzbfO+YEqmBqTVIweWpNxv/uqdFtv+HIMZyWuAATAEwdPlgbzLs6RqN7BaIutQ4PRI9V43x5B4xWNIXn+8nMd4WyEkCOCKRTvmajHlRcfvq+iOVEXtHkKS6MysLknEvc7TiPFJRR0zKgA3InPGezPS6fCQmMS4ZMphBoO9iYKfDsvJoZKKEsWkyb6sw6IP5h+/rClfEe/kTkai0CuPPQK/Pse4F/Iau106VlTfZZcr+8g3Qs9mIJIj6PMTv81nwSzrz8O/fHPPqOLAr7apUtZnT5s6KWlD2bvD4BYvfIgTl8UTBi6w3NSeglV8+nMNfeY9vCGICxpQHWLJpeMPIJW6yokM+rDd3FUN0ivRUCAwEAAaOB0DCBzTAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFJLTr+Z11eQ5xaTdZRKhiGChKZYRME4GCCsGAQUFBwEBBEIwQDA+BggrBgEFBQcwAYYyaHR0cDovL2xvY2FsaG9zdDozMDA4MC9lamJjYS9wdWJsaWN3ZWIvc3RhdHVzL29jc3AwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBRVYOKvDRtKQkT3viG6fCoo6EHDWDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQELBQADggGBAAIS9ydwWiWLo+sZ9W8j9cQvDmE1NBGGNNmB8Z+1LT3aqnjubYevs1+Nds9zHJkiiFay+61ACRtpzO13JeXabd8VmINox1qCVNl29f8a6L3JTA1Zomjj4mGHZXzh51diHawXsE+9AlAN2BwEegaqnJpfR8X70wof08cEcwdaS5ekDWld+nSI23KAKf/0bUW9rcj40WoZUs7As6dL/PIGHRebmuGsRiPzoOnDJVicWVJBx1aKmvBezu0TQF1mjwTlJeODuI3u3dInfxdWpBOJ5xNOq/jBHAEwWi5j6nR0ZNTqfXL2ITIUylL/PWMzopjxQCGELRUrmu2XVuuCtARmtHLACy24DJRaVxKFtCBboPFm0oV0N8+MemfhUV0tvZUlpUciQSp/EkfCe3k8Up2ALWnATOuF/jA3uRxOHiOxpUWMjrmk2sQj3xxD1xqWGqF6/J9aM1GpAQ/Pe9xodQ+S5oAkccKy8EfKgrN5//njBtVM/LKwHgeELsnRQoGFc/zemw=="); + private static final byte[] issuer = Base64.decode("MIIEkDCCAvigAwIBAgIUX4IMXA0kGXbVq2V/IxJrZPFV+towDQYJKoZIhvcNAQEMBQAwODEPMA0GA1UEAwwGUk9PVENBMQswCQYDVQQLDAJDQTELMAkGA1UECgwCTlIxCzAJBgNVBAYTAkdCMB4XDTI0MDYwNzE1MTg0OVoXDTI1MDYwNzE1MTg0OFowFTETMBEGA1UEAwwKSVNTVUlOR0NBNDCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJf2IrOdAQw6GcZ1m2+3y/OcAXm9pCjdfN0ydC5dqsBC62oHuMOSk/q8ZiOGrPEc/ONcTV0q0sKDNWyDZxmbQxeL2Y22eIcrS/jRDtNaIoidb0JLzL0tpdh7WNZxhLlpH2XuT60OER1Oo3ZPSi1f5O3LFbC2ZIABvRt9Ldp4yKwNQ78Zkvf855aAYjZ2RVAq0+6kqMDTmLn5HJey8ayZroKuF88Ns/lZWk+J7mjUo3XZZFVbM6k1LO/A4nK3E/Dsbv7EOxy8cke562qKpl6XxrEiZfFaxFEuwGzgi7skpayku8TEpMDW0fx5KChcQlEvuHAwyjkC4miRkGjmqVhfZUIjUZzl/I43S+5eooUqqxeTnOR8J9lfW3a8E4FZNGSn3Tx9x9N2zZHe8mziRv73C00pNHFGA1EjLjigiHHlgjgoyzKj3pEN/NMl65TS0Qxm0dPvpAn0Q7eZUYbPtRRVBESQGEQ2JTE4XmdzTF+c42edWMROkUi4Juxdsm1sJvaDXwIDAQABo4G0MIGxMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUNNt4P8rFkBaOpmL+oUia6c+KHNowTgYIKwYBBQUHAQEEQjBAMD4GCCsGAQUFBzABhjJodHRwOi8vbG9jYWxob3N0OjMwMDgwL2VqYmNhL3B1YmxpY3dlYi9zdGF0dXMvb2NzcDAdBgNVHQ4EFgQUktOv5nXV5DnFpN1lEqGIYKEplhEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4IBgQBD9H6KTN4nM+KbOwQrxsaH5F8WnlrZSJ9g5csbayVGReZJtuRzigF8IOel2/E3JUgfKUxvGDCd3ZPxHkjNLeWl6dTr4jDFsg1QL1xVdb5Bxb7FR2Og9DtRf3+InoTIe10cNngK7+V83DSmNxy7i5Ygj4KhiRm3jegbVx2wkjmtHQRhczaVNH1/qiGWILT0t46M0TYcZzIcO5/6JUwZlDWLoc4KzvSKvTaEk4mASeYl4HHqoi96FBlGV3KacqlznCsZO/k0lZ6RflJwvX3OTAwXD11iIht+CQuxnGh2ZRQ9KP62eeu4EBxf3fqOQiwU/pHx1TdLyu/n/uh0ZTFazazjhE/PuBaCy+qrdC22weyunpp7bNbRF1B65Rs9EsuertMDA3kTTyYicpK7/ihbuFYmzfZFuMoNOLtgZGTfHqPjcaEkHmlDOP0m1fiwrfWe8LjXGgjE53Yy0yFPO23BCkK7eWh74aojVICRXL+j53nn6fhJ8+tTVJmAWACPi7ct0J4="); + // OCSP response to be returned by our simple server + private static final byte[] response = Base64.decode("MIIFnAoBAKCCBZUwggWRBgkrBgEFBQcwAQEEggWCMIIFfjCCARehgZ8wgZwxCzAJBgNVBAYTAklOMRcwFQYDVQQIEw5BbmRocmEgcHJhZGVzaDESMBAGA1UEBxMJSHlkZXJhYmFkMQwwCgYDVQQKEwNUQ1MxDDAKBgNVBAsTA0FUQzEeMBwGA1UEAxMVVENTLUNBIE9DU1AgUmVzcG9uZGVyMSQwIgYJKoZIhvcNAQkBFhVvY3NwQHRjcy1jYS50Y3MuY28uaW4YDzIwMDMwNDAyMTIzNDU4WjBiMGAwOjAJBgUrDgMCGgUABBRs07IuoCWNmcEl1oHwIak1BPnX8QQUtGyl/iL9WJ1VxjxFj0hAwJ/s1AcCAQKhERgPMjAwMjA4MjkwNzA5MjZaGA8yMDAzMDQwMjEyMzQ1OFowDQYJKoZIhvcNAQEFBQADgYEAfbN0TCRFKdhsmvOdUoiJ+qvygGBzDxD/VWhXYA+16AphHLIWNABR3CgHB3zWtdy2j7DJmQ/R7qKj7dUhWLSqclAiPgFtQQ1YvSJAYfEIdyHkxv4NP0LSogxrumANcDyC9yt/W9yHjD2ICPBIqCsZLuLkOHYi5DlwWe9Zm9VFwCGgggPMMIIDyDCCA8QwggKsoAMCAQICAQYwDQYJKoZIhvcNAQEFBQAwgZQxFDASBgNVBAMTC1RDUy1DQSBPQ1NQMSYwJAYJKoZIhvcNAQkBFhd0Y3MtY2FAdGNzLWNhLnRjcy5jby5pbjEMMAoGA1UEChMDVENTMQwwCgYDVQQLEwNBVEMxEjAQBgNVBAcTCUh5ZGVyYWJhZDEXMBUGA1UECBMOQW5kaHJhIHByYWRlc2gxCzAJBgNVBAYTAklOMB4XDTAyMDgyOTA3MTE0M1oXDTAzMDgyOTA3MTE0M1owgZwxCzAJBgNVBAYTAklOMRcwFQYDVQQIEw5BbmRocmEgcHJhZGVzaDESMBAGA1UEBxMJSHlkZXJhYmFkMQwwCgYDVQQKEwNUQ1MxDDAKBgNVBAsTA0FUQzEeMBwGA1UEAxMVVENTLUNBIE9DU1AgUmVzcG9uZGVyMSQwIgYJKoZIhvcNAQkBFhVvY3NwQHRjcy1jYS50Y3MuY28uaW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM+XWW4caMRv46D7L6Bv8iwtKgmQu0SAybmFRJiz12qXzdvTLt8C75OdgmUomxp0+gW/4XlTPUqOMQWv463aZRv9Ust4f8MHEJh4ekP/NS9+d8vEO3P40ntQkmSMcFmtA9E1koUtQ3MSJlcs441JjbgUaVnmjDmmniQnZY4bU3tVAgMBAAGjgZowgZcwDAYDVR0TAQH/BAIwADALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwkwNgYIKwYBBQUHAQEEKjAoMCYGCCsGAQUFBzABhhpodHRwOi8vMTcyLjE5LjQwLjExMDo3NzAwLzAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8vMTcyLjE5LjQwLjExMC9jcmwuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQB6FovM3B4VDDZ15o12gnADZsIk9fTAczLlcrmXLNN4PgmqgnwF0Ymj3bD5SavDOXxbA65AZJ7rBNAguLUo+xVkgxmoBH7R2sBxjTCcr07NEadxM3HQkt0aX5XYEl8eRoifwqYAI9h0ziZfTNes8elNfb3DoPPjqq6VmMg0f0iMS4W8LjNPorjRB+kIosa1deAGPhq0eJ8yr0/s2QR2/WFD5P4aXc8IKWleklnIImS3zqiPrq6tl2Bm8DZj7vXlTOwmraSQxUwzCKwYob1yGvNOUQTqpG6jxn7jgDawHU1+WjWQe4Q34/pWeGLysxTraMa+Ug9kPe+jy/qRX2xwvKBZ"); + // Simple HTTP server used as OCSP responder + private static final SimpleHttpServer server = new SimpleHttpServer(); + + + public static void main(String[] args) + { + Security.addProvider(new BouncyCastleProvider()); + PrintTestResult.printResult(junit.textui.TestRunner.run(new TestSuite(OcspResponseManagerTest.class, "OcspResponseManager Tests"))); + } + + private static void cleanSystemProps() + { + System.clearProperty("ocsp.enable"); + System.clearProperty("ocsp.responderURL"); + System.clearProperty("jdk.tls.stapling.responderOverride"); + System.clearProperty("jdk.tls.stapling.responderURI"); + System.clearProperty("jdk.tls.stapling.cacheLifetime"); + } + + public void testGetOCSPResponseForRevocationCheck() throws CertificateException, NoSuchProviderException, CertPathValidatorException, IOException + { + CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC"); + X509Certificate interCert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(issuer)); + X509Certificate finalCert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert)); + + byte[] sampleNonce = new byte[16]; + Random rand = new Random(); + rand.nextBytes(sampleNonce); + NonceExtension nonceExt = new NonceExtension(sampleNonce); + + ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(response)); + OCSPResponse resp = OCSPResponse.getInstance(aIn.readObject()); + OcspHttpHandler ocspHttpHandler = new OcspHttpHandler(200, resp.getEncoded()); + + // case ocsp.enable set to false + System.setProperty("ocsp.enable", "false"); + OcspResponseManager.reset(); + RecoverableCertPathValidatorException softFailEx = Assert.assertThrows(RecoverableCertPathValidatorException.class, () -> + { + OcspResponseManager.getOCSPResponseForRevocationCheck(finalCert, interCert, Collections.emptyList(), null, helper); + }); + assertEquals("[revocation check] OCSP disabled by \"ocsp.enable\" setting", softFailEx.getMessage()); + + // revert ocsp.enable to default + System.clearProperty("ocsp.enable"); + OcspResponseManager.reset(); + + // case error when creating CertId + CertPathValidatorException hardFailEx = Assert.assertThrows(CertPathValidatorException.class, () -> + OcspResponseManager.getOCSPResponseForRevocationCheck(finalCert, null, Collections.emptyList(), null, helper) + ); + assertEquals("[revocation check] Error creating CertID for certificate: " + finalCert.getSubjectX500Principal(), hardFailEx.getMessage()); + + // case parent OCSP URI set and responder not available + URI parentOcspURI = URI.create("http://localhost:8000/"); + softFailEx = Assert.assertThrows(RecoverableCertPathValidatorException.class, () -> + OcspResponseManager.getOCSPResponseForRevocationCheck(finalCert, interCert, Collections.emptyList(), parentOcspURI, helper) + ); + assertEquals("[revocation check] Network error while trying to retrieve OCSP response for cert: " + finalCert.getSubjectX500Principal() + " from responder URL: http://localhost:8000/", softFailEx.getMessage()); + + // case parent OCSP URI set and valid response + server.start(8000, ocspHttpHandler); + OCSPResponse ocspResponse = OcspResponseManager.getOCSPResponseForRevocationCheck(finalCert, interCert, Collections.emptyList(), parentOcspURI, helper); + server.stop(); + + assertNotNull(ocspResponse); + assertEquals(OCSPResponseStatus.SUCCESSFUL, ocspResponse.getResponseStatus().getIntValue()); + + // case ocsp.responderURL property set but malformed URL + System.setProperty("ocsp.responderURL", "blabla"); + OcspResponseManager.reset(); + hardFailEx = Assert.assertThrows(CertPathValidatorException.class, () -> + OcspResponseManager.getOCSPResponseForRevocationCheck(finalCert, interCert, Collections.emptyList(), null, helper) + ); + assertEquals("[revocation check] Misconfigured property ocsp.responderURL: blabla", hardFailEx.getMessage()); + + // case ocsp.responderURL property set and valid response + System.setProperty("ocsp.responderURL", "http://localhost:8001/"); + OcspResponseManager.reset(); + server.start(8001, ocspHttpHandler); + ocspResponse = OcspResponseManager.getOCSPResponseForRevocationCheck(finalCert, interCert, Collections.emptyList(), null, helper); + server.stop(); + + assertNotNull(ocspResponse); + assertEquals(OCSPResponseStatus.SUCCESSFUL, ocspResponse.getResponseStatus().getIntValue()); + + // case AIA extension OCSP URI (http://localhost:30080/) set on the certificate and valid response + System.clearProperty("ocsp.responderURL"); + OcspResponseManager.reset(); + server.start(30080, ocspHttpHandler); + ocspResponse = OcspResponseManager.getOCSPResponseForRevocationCheck(finalCert, interCert, Collections.emptyList(), null, helper); + server.stop(); + + assertNotNull(ocspResponse); + assertEquals(OCSPResponseStatus.SUCCESSFUL, ocspResponse.getResponseStatus().getIntValue()); + + softFailEx = Assert.assertThrows(RecoverableCertPathValidatorException.class, () -> + OcspResponseManager.getOCSPResponseForRevocationCheck(finalCert, interCert, Collections.singletonList(nonceExt), null, helper) + ); + assertEquals("[revocation check] Network error while trying to retrieve OCSP response for cert: " + finalCert.getSubjectX500Principal() + " from responder URL: http://localhost:30080/ejbca/publicweb/status/ocsp", softFailEx.getMessage()); + + cleanSystemProps(); + } + + public void testGetOCSPResponseForStapling() throws CertificateException, NoSuchProviderException, IOException, InterruptedException + { + CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC"); + X509Certificate interCert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(issuer)); + X509Certificate finalCert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert)); + + byte[] sampleNonce = new byte[16]; + Random rand = new Random(); + rand.nextBytes(sampleNonce); + NonceExtension nonceExt = new NonceExtension(sampleNonce); + + ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(response)); + OCSPResponse resp = OCSPResponse.getInstance(aIn.readObject()); + OcspHttpHandler ocspHttpHandler = new OcspHttpHandler(200, resp.getEncoded()); + + // case error when creating CertId + OCSPResponse response = OcspResponseManager.getOCSPResponseForStapling(finalCert, null, null, helper); + assertNull(response); + + // case jdk.tls.stapling.responderOverride set and responder URI malformed + System.setProperty("jdk.tls.stapling.responderOverride", "true"); + System.setProperty("jdk.tls.stapling.responderURI", "blabla"); + OcspResponseManager.reset(); + + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, Extensions.getInstance(null), helper); + assertNull(response); + + + // case jdk.tls.stapling.responderOverride set and responder not available + System.setProperty("jdk.tls.stapling.responderURI", "http://localhost:8002/"); + OcspResponseManager.reset(); + + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, Extensions.getInstance(null), helper); + assertNull(response); + + // case responderOverride and valid response + server.start(8002, ocspHttpHandler); + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, Extensions.getInstance(null), helper); + server.stop(); + assertNotNull(response); + assertEquals(OCSPResponseStatus.SUCCESSFUL, response.getResponseStatus().getIntValue()); + + // case AIA extension OCSP URI (http://localhost:30080/) set on the certificate and valid response + System.clearProperty("jdk.tls.stapling.responderOverride"); + System.clearProperty("jdk.tls.stapling.responderURI"); + OcspResponseManager.reset(); + + server.start(30080, ocspHttpHandler); + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, null, helper); + server.stop(); + assertNotNull(response); + assertEquals(OCSPResponseStatus.SUCCESSFUL, response.getResponseStatus().getIntValue()); + + // cache check (don't start server) + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, null, helper); + assertNotNull(response); + assertEquals(OCSPResponseStatus.SUCCESSFUL, response.getResponseStatus().getIntValue()); + + // cache skip when nonce extension found + ASN1EncodableVector extVector = new ASN1EncodableVector(); + extVector.add(new org.bouncycastle.asn1.x509.Extension(new ASN1ObjectIdentifier(nonceExt.getId()), nonceExt.isCritical(), nonceExt.getValue())); + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, Extensions.getInstance(new DERSequence(extVector)), helper); + assertNull(response); + + // case cache lifetime 1s (response expired) + System.setProperty("jdk.tls.stapling.cacheLifetime", "1"); + OcspResponseManager.reset(); + + server.start(30080, ocspHttpHandler); + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, null, helper); + server.stop(); + assertNotNull(response); + assertEquals(OCSPResponseStatus.SUCCESSFUL, response.getResponseStatus().getIntValue()); + // sleep 1s for response to become expired + Thread.sleep(1000); + // try to get from cache + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, null, helper); + assertNull(response); + + // case responder available and 1 expired response in cache (test clean cache) + server.start(30080, ocspHttpHandler); + response = OcspResponseManager.getOCSPResponseForStapling(finalCert, interCert, null, helper); + server.stop(); + assertNotNull(response); + assertEquals(OCSPResponseStatus.SUCCESSFUL, response.getResponseStatus().getIntValue()); + + cleanSystemProps(); + } + + private static class SimpleHttpServer + { + private HttpServer server; + + public void start(int port, HttpHandler handler) throws IOException + { + server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/", handler); + server.start(); + } + + public void stop() + { + server.stop(0); + } + } + + private static class OcspHttpHandler implements HttpHandler + { + private final int responseCode; + private final byte[] responseContent; + + public OcspHttpHandler(int responseCode, byte[] responseContent) + { + this.responseCode = responseCode; + this.responseContent = responseContent; + } + + @Override + public void handle(HttpExchange exchange) throws IOException + { + exchange.sendResponseHeaders(responseCode, responseContent.length); + try (OutputStream os = exchange.getResponseBody()) + { + os.write(responseContent); + } + } + } + + private static class NonceExtension + implements java.security.cert.Extension + { + private final byte[] nonce; + + NonceExtension(byte[] nonce) + { + this.nonce = nonce; + } + + public String getId() + { + return OCSPObjectIdentifiers.id_pkix_ocsp_nonce.getId(); + } + + public boolean isCritical() + { + return false; + } + + public byte[] getValue() + { + return nonce; + } + + public void encode(OutputStream outputStream) + throws IOException + { + outputStream.write(new org.bouncycastle.asn1.x509.Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, nonce).getEncoded()); + } + } +} \ No newline at end of file diff --git a/tls/src/main/java/org/bouncycastle/jsse/provider/ProvTlsClient.java b/tls/src/main/java/org/bouncycastle/jsse/provider/ProvTlsClient.java index eb5d60eb4f..b902b6f76c 100644 --- a/tls/src/main/java/org/bouncycastle/jsse/provider/ProvTlsClient.java +++ b/tls/src/main/java/org/bouncycastle/jsse/provider/ProvTlsClient.java @@ -1,8 +1,10 @@ package org.bouncycastle.jsse.provider; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.Principal; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Collections; import java.util.Hashtable; import java.util.LinkedHashMap; @@ -19,7 +21,9 @@ import org.bouncycastle.jsse.provider.SignatureSchemeInfo.PerConnection; import org.bouncycastle.tls.AlertDescription; import org.bouncycastle.tls.AlertLevel; +import org.bouncycastle.tls.CertificateEntry; import org.bouncycastle.tls.CertificateRequest; +import org.bouncycastle.tls.CertificateStatus; import org.bouncycastle.tls.CertificateStatusRequest; import org.bouncycastle.tls.CertificateStatusRequestItemV2; import org.bouncycastle.tls.CertificateStatusType; @@ -376,8 +380,34 @@ public void notifyServerCertificate(TlsServerCertificate serverCertificate) thro String authType = JsseUtils.getAuthTypeServer( context.getSecurityParametersHandshake().getKeyExchangeAlgorithm()); - jsseSecurityParameters.statusResponses = JsseUtils.getStatusResponses( - serverCertificate.getCertificateStatus()); + if (TlsUtils.isTLSv13(context)) + { + // RFC 8446: In TLS 1.3, the server's OCSP information is carried in an extension in the CertificateEntry + // containing the associated certificate. Specifically, the body of the "status_request" extension + // from the server MUST be a CertificateStatus structure as defined in [RFC6066], which is interpreted as defined in [RFC6960]. + CertificateEntry[] certificateEntryList = serverCertificate.getCertificate().getCertificateEntryList(); + for (int i = 0; i < certificateEntryList.length - 1; i++) + { + CertificateEntry entry = certificateEntryList[i]; + Hashtable extensions = entry.getExtensions(); + if (extensions != null && extensions.get(TlsExtensionsUtils.EXT_status_request) != null) + { + byte[] extBytes = (byte[]) extensions.get(TlsExtensionsUtils.EXT_status_request); + ByteArrayInputStream bais = new ByteArrayInputStream(extBytes); + CertificateStatus certificateStatus = CertificateStatus.parse(context, bais); + if (jsseSecurityParameters.statusResponses == null) + { + jsseSecurityParameters.statusResponses = new ArrayList(); + } + jsseSecurityParameters.statusResponses.addAll(JsseUtils.getStatusResponses(certificateStatus)); + } + } + } + else + { + jsseSecurityParameters.statusResponses = JsseUtils.getStatusResponses( + serverCertificate.getCertificateStatus()); + } manager.checkServerTrusted(chain, authType); } diff --git a/tls/src/main/java/org/bouncycastle/jsse/provider/ProvTlsServer.java b/tls/src/main/java/org/bouncycastle/jsse/provider/ProvTlsServer.java index 8eacfbed4b..4880f62216 100644 --- a/tls/src/main/java/org/bouncycastle/jsse/provider/ProvTlsServer.java +++ b/tls/src/main/java/org/bouncycastle/jsse/provider/ProvTlsServer.java @@ -18,7 +18,9 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.bouncycastle.asn1.ocsp.OCSPResponse; import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.jce.provider.OcspResponseManager; import org.bouncycastle.jsse.BCSNIMatcher; import org.bouncycastle.jsse.BCSNIServerName; import org.bouncycastle.jsse.BCX509Key; @@ -28,10 +30,13 @@ import org.bouncycastle.tls.Certificate; import org.bouncycastle.tls.CertificateRequest; import org.bouncycastle.tls.CertificateStatus; +import org.bouncycastle.tls.CertificateStatusRequestItemV2; +import org.bouncycastle.tls.CertificateStatusType; import org.bouncycastle.tls.ClientCertificateType; import org.bouncycastle.tls.DefaultTlsServer; import org.bouncycastle.tls.KeyExchangeAlgorithm; import org.bouncycastle.tls.NamedGroup; +import org.bouncycastle.tls.OCSPStatusRequest; import org.bouncycastle.tls.ProtocolName; import org.bouncycastle.tls.ProtocolVersion; import org.bouncycastle.tls.SecurityParameters; @@ -71,10 +76,8 @@ class ProvTlsServer private static final boolean provServerEnableSessionResumption = PropertyUtils .getBooleanSystemProperty("org.bouncycastle.jsse.server.enableSessionResumption", true); - // TODO[jsse] Support status_request and status_request_v2 extensions -// private static final boolean provServerEnableStatusRequest = PropertyUtils.getBooleanSystemProperty( -// "jdk.tls.server.enableStatusRequestExtension", false); - private static final boolean provServerEnableStatusRequest = false; + private static final boolean provServerEnableStatusRequest = PropertyUtils + .getBooleanSystemProperty("jdk.tls.server.enableStatusRequestExtension", false); private static final boolean provServerEnableTrustedCAKeys = PropertyUtils .getBooleanSystemProperty("org.bouncycastle.jsse.server.enableTrustedCAKeysExtension", false); @@ -233,7 +236,7 @@ protected String getDetailMessageNoCipherSuite() // CAUTION: Required for Common Criteria StringBuilder sb = new StringBuilder(serverID); - + int[] offered = offeredCipherSuites; if (TlsUtils.isNullOrEmpty(offered)) { @@ -528,53 +531,115 @@ public CertificateRequest getCertificateRequest() throws IOException @Override public CertificateStatus getCertificateStatus() throws IOException { - // TODO[jsse] Support status_request and status_request_v2 extensions -// SecurityParameters securityParameters = context.getSecurityParametersHandshake(); -// int statusRequestVersion = securityParameters.getStatusRequestVersion(); -// -// if (statusRequestVersion == 2) -// { -// int count = statusRequestV2.size(); -// for (int i = 0; i < count; ++i) -// { -// CertificateStatusRequestItemV2 item = (CertificateStatusRequestItemV2)statusRequestV2.get(i); -// short statusType = item.getStatusType(); -// if (CertificateStatusType.ocsp_multi == statusType) -// { -// int chainLength = credentials.getCertificate().getLength(); -// Vector ocspResponseList = new Vector(chainLength); -// for (int j = 0; j < chainLength; ++j) -// { -// // TODO Actual OCSP response -// ocspResponseList.add(null); -// } -// -// return new CertificateStatus(CertificateStatusType.ocsp_multi, ocspResponseList); -// } -// else if (CertificateStatusType.ocsp == statusType) -// { -// // TODO Actual OCSP response -// OCSPResponse ocspResponse; -// -// return new CertificateStatus(CertificateStatusType.ocsp, ocspResponse); -// } -// } -// } -// else if (statusRequestVersion == 1) -// { -// if (CertificateStatusType.ocsp == certificateStatusRequest.getStatusType()) -// { -// OCSPStatusRequest ocspStatusRequest = certificateStatusRequest.getOCSPStatusRequest(); -// -// @SuppressWarnings("unchecked") -// Vector responderIDList = ocspStatusRequest.getResponderIDList(); -// Extensions requestExtensions = ocspStatusRequest.getRequestExtensions(); -// -// X509Certificate eeCert = JsseUtils.getEndEntity(getCrypto(), credentials.getCertificate()); -// -// // ... -// } -// } + // check supported + if (!allowCertificateStatus() && !allowMultiCertStatus()) + { + return null; + } + + // for both status_request and status_request_v2 we need at least 2 certificates in the chain in order to staple the response(s) + int chainLength = credentials.getCertificate().getLength(); + if (chainLength < 2) + { + return null; + } + + SecurityParameters securityParameters = context.getSecurityParametersHandshake(); + int statusRequestVersion = securityParameters.getStatusRequestVersion(); + + // NOTE: we can have status_request_v2 only in tls12 and NOT tls13 + if (statusRequestVersion == 2) + { + // for status_request_v2 we can have multiple status request items of type "ocsp_multi" or "ocsp", + // so we will try to find a valid "ocsp_multi" item and process that, or, if not found, a valid "ocsp" item + int count = statusRequestV2.size(); + int ocspMultiIdx = -1; + int ocspIdx = -1; + for (int i = 0; i < count; i++) + { + CertificateStatusRequestItemV2 item = (CertificateStatusRequestItemV2) statusRequestV2.get(i); + if (CertificateStatusType.ocsp_multi == item.getStatusType()) + { + // JSSE doesn't support responderIds in the request + if (item.getOCSPStatusRequest().getResponderIDList().isEmpty()) + { + ocspMultiIdx = i; + // found valid ocsp_multi request - no need to look further + break; + } + } + else if (CertificateStatusType.ocsp == item.getStatusType() && ocspIdx < 0) + { + // JSSE doesn't support responderIds in the request + if (item.getOCSPStatusRequest().getResponderIDList().isEmpty()) + { + ocspIdx = i; + } + } + } + if (ocspMultiIdx >= 0) + { + // for ocsp_multi retrieve the OCSP responses for all the certificates in the chain + CertificateStatusRequestItemV2 item = (CertificateStatusRequestItemV2) statusRequestV2.get(ocspMultiIdx); + Vector ocspResponseList = new Vector(chainLength); + for (int j = 0; j < chainLength - 1; ++j) + { + // we assume that the chain is in the correct order (each certificate is issued by the next) + X509Certificate cert = JsseUtils.getX509Certificate(getCrypto(), credentials.getCertificate().getCertificateAt(j)); + X509Certificate issuer = JsseUtils.getX509Certificate(getCrypto(), credentials.getCertificate().getCertificateAt(j + 1)); + OCSPResponse ocspResponse = OcspResponseManager.getOCSPResponseForStapling(cert, issuer, item.getOCSPStatusRequest().getRequestExtensions(), getCrypto().getHelper()); + ocspResponseList.add(ocspResponse); + } + return new CertificateStatus(CertificateStatusType.ocsp_multi, ocspResponseList); + } + if (ocspIdx >= 0) + { + // only retrieve the OCSP response for the end entity + CertificateStatusRequestItemV2 item = (CertificateStatusRequestItemV2) statusRequestV2.get(ocspIdx); + X509Certificate cert = JsseUtils.getX509Certificate(getCrypto(), credentials.getCertificate().getCertificateAt(0)); + X509Certificate issuer = JsseUtils.getX509Certificate(getCrypto(), credentials.getCertificate().getCertificateAt(1)); + OCSPResponse ocspResponse = OcspResponseManager.getOCSPResponseForStapling(cert, issuer, item.getOCSPStatusRequest().getRequestExtensions(), getCrypto().getHelper()); + return new CertificateStatus(CertificateStatusType.ocsp, ocspResponse); + } + } + // NOTE: we can have status_request for tls12 and tls13 + else if (statusRequestVersion == 1) + { + if (CertificateStatusType.ocsp == certificateStatusRequest.getStatusType()) + { + OCSPStatusRequest ocspStatusRequest = certificateStatusRequest.getOCSPStatusRequest(); + // JSSE doesn't support responderIds in the request + if (!ocspStatusRequest.getResponderIDList().isEmpty()) + { + return null; + } + if (TlsUtils.isTLSv13(context)) + { + // RFC 8446 deprecates the status_request_v2 extension and provides only the status_request extension, + // but we need to retrieve the OCSP responses for all certificates in the chain + Vector ocspResponseList = new Vector(chainLength); + for (int j = 0; j < chainLength - 1; ++j) + { + // we assume that the chain is in the correct order (each certificate is issued by the next) + X509Certificate cert = JsseUtils.getX509Certificate(getCrypto(), credentials.getCertificate().getCertificateAt(j)); + X509Certificate issuer = JsseUtils.getX509Certificate(getCrypto(), credentials.getCertificate().getCertificateAt(j + 1)); + OCSPResponse ocspResponse = OcspResponseManager.getOCSPResponseForStapling(cert, issuer, ocspStatusRequest.getRequestExtensions(), getCrypto().getHelper()); + ocspResponseList.add(ocspResponse); + } + // return an "ocsp_multi" certificate status which will be converted in the upper level to an "ocsp" + // certificate status corresponding to a certificate entry + return new CertificateStatus(CertificateStatusType.ocsp_multi, ocspResponseList); + } + else + { + // only retrieve the OCSP response for the end entity + X509Certificate cert = JsseUtils.getX509Certificate(getCrypto(), credentials.getCertificate().getCertificateAt(0)); + X509Certificate issuer = JsseUtils.getX509Certificate(getCrypto(), credentials.getCertificate().getCertificateAt(1)); + OCSPResponse ocspResponse = OcspResponseManager.getOCSPResponseForStapling(cert, issuer, ocspStatusRequest.getRequestExtensions(), getCrypto().getHelper()); + return new CertificateStatus(CertificateStatusType.ocsp, ocspResponse); + } + } + } return null; } diff --git a/tls/src/main/java/org/bouncycastle/tls/CertificateEntry.java b/tls/src/main/java/org/bouncycastle/tls/CertificateEntry.java index 6c31232421..f9909ace45 100644 --- a/tls/src/main/java/org/bouncycastle/tls/CertificateEntry.java +++ b/tls/src/main/java/org/bouncycastle/tls/CertificateEntry.java @@ -7,7 +7,7 @@ public class CertificateEntry { protected final TlsCertificate certificate; - protected final Hashtable extensions; + protected Hashtable extensions; public CertificateEntry(TlsCertificate certificate, Hashtable extensions) { @@ -29,4 +29,9 @@ public Hashtable getExtensions() { return extensions; } + + public void setExtensions(Hashtable extensions) + { + this.extensions = extensions; + } } diff --git a/tls/src/main/java/org/bouncycastle/tls/TlsServerProtocol.java b/tls/src/main/java/org/bouncycastle/tls/TlsServerProtocol.java index 3ace602565..099a4081d6 100644 --- a/tls/src/main/java/org/bouncycastle/tls/TlsServerProtocol.java +++ b/tls/src/main/java/org/bouncycastle/tls/TlsServerProtocol.java @@ -8,6 +8,7 @@ import java.util.Hashtable; import java.util.Vector; +import org.bouncycastle.asn1.ocsp.OCSPResponse; import org.bouncycastle.tls.crypto.TlsAgreement; import org.bouncycastle.tls.crypto.TlsCrypto; import org.bouncycastle.tls.crypto.TlsDHConfig; @@ -381,6 +382,44 @@ protected ServerHello generate13ServerHello(ClientHello clientHello, HandshakeMe securityParameters.statusRequestVersion = clientHelloExtensions.containsKey(TlsExtensionsUtils.EXT_status_request) ? 1 : 0; + if (securityParameters.statusRequestVersion > 0) + { + // server should handle fetching of certificate statuses + CertificateStatus certificateStatus = tlsServer.getCertificateStatus(); + if (certificateStatus != null) + { + // RFC 8446: In TLS 1.3, the server's OCSP information is carried in an extension in the CertificateEntry containing the + // associated certificate. Specifically, the body of the "status_request" extension from the server MUST be a + // CertificateStatus structure as defined in [RFC6066], which is interpreted as defined in [RFC6960]. + Vector ocspResponseList = certificateStatus.getOCSPResponseList(); + + // credentials (server certificate entries) should be already established at this point + TlsCredentials credentials = tlsServer.getCredentials(); + + // we assume that the certificate entries and the OCSP responses are in the same order + CertificateEntry[] certificateEntryList = credentials.getCertificate().getCertificateEntryList(); + for (int i = 0; i < certificateEntryList.length - 1; i++) + { + OCSPResponse ocspResponse = (OCSPResponse) ocspResponseList.get(i); + if (ocspResponse == null) + { + continue; + } + CertificateEntry entry = certificateEntryList[i]; + Hashtable certificateEntryExtensions = entry.getExtensions(); + if (certificateEntryExtensions == null) + { + certificateEntryExtensions = new Hashtable(); + entry.setExtensions(certificateEntryExtensions); + } + CertificateStatus certStatus = new CertificateStatus(CertificateStatusType.ocsp, ocspResponse); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + certStatus.encode(baos); + certificateEntryExtensions.put(TlsExtensionsUtils.EXT_status_request, baos.toByteArray()); + } + } + } + this.expectSessionTicket = false; TlsSecret pskEarlySecret = null;