Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<junit-jupiter.version>5.14.4</junit-jupiter.version>
<assertj.version>3.27.7</assertj.version>
<mockito.version>5.23.0</mockito.version>
<awaitility.version>4.3.0</awaitility.version>
<maven-surefire-plugin.version>3.6.0</maven-surefire-plugin.version>
<maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
<maven-source-plugin.version>3.4.0</maven-source-plugin.version>
Expand Down Expand Up @@ -115,6 +116,12 @@
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
Expand Down
90 changes: 51 additions & 39 deletions src/main/java/eu/webeid/ocsp/OcspCertificateRevocationChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import eu.webeid.ocsp.protocol.OcspResponseValidator;
import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException;
import eu.webeid.ocsp.exceptions.UserCertificateOCSPException;
import eu.webeid.security.util.DateAndTime;
import eu.webeid.ocsp.service.OcspServiceProvider;
import eu.webeid.ocsp.service.OcspService;
Expand Down Expand Up @@ -50,13 +51,13 @@ public class OcspCertificateRevocationChecker implements CertificateRevocationCh

public static final Duration DEFAULT_TIME_SKEW = Duration.ofMinutes(15);
public static final Duration DEFAULT_THIS_UPDATE_AGE = Duration.ofMinutes(2);
public static final Duration DEFAULT_NEXT_UPDATE_AGE = Duration.ofMinutes(15);

private static final Logger LOG = LoggerFactory.getLogger(OcspCertificateRevocationChecker.class);

private final OcspClient ocspClient;
private final OcspServiceProvider ocspServiceProvider;
private final Duration allowedOcspResponseTimeSkew;
private final Duration maxOcspResponseThisUpdateAge;

static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Expand All @@ -66,12 +67,10 @@ public class OcspCertificateRevocationChecker implements CertificateRevocationCh

public OcspCertificateRevocationChecker(OcspClient ocspClient,
OcspServiceProvider ocspServiceProvider,
Duration allowedOcspResponseTimeSkew,
Duration maxOcspResponseThisUpdateAge) {
Duration allowedOcspResponseTimeSkew) {
this.ocspClient = requireNonNull(ocspClient, "ocspClient");
this.ocspServiceProvider = requireNonNull(ocspServiceProvider, "ocspServiceProvider");
this.allowedOcspResponseTimeSkew = requirePositiveDuration(allowedOcspResponseTimeSkew, "allowedOcspResponseTimeSkew");
this.maxOcspResponseThisUpdateAge = requirePositiveDuration(maxOcspResponseThisUpdateAge, "maxOcspResponseThisUpdateAge");
}

/**
Expand All @@ -86,22 +85,16 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
requireNonNull(subjectCertificate, "subjectCertificate");
requireNonNull(issuerCertificate, "issuerCertificate");

URI ocspResponderUri = null;
try {
OcspService ocspService = ocspServiceProvider.getService(subjectCertificate, issuerCertificate);
ocspResponderUri = requireNonNull(ocspService.getAccessLocation(), "ocspResponderUri");

final CertificateID certificateId = getCertificateId(subjectCertificate, issuerCertificate);

final OCSPReq request = new OcspRequestBuilder()
.withCertificateId(certificateId)
.enableOcspNonce(ocspService.doesSupportNonce())
.build();
final OcspService ocspService = ocspServiceProvider.getService(subjectCertificate, issuerCertificate);
final CertificateID certificateId = getCertificateId(subjectCertificate, issuerCertificate);
final URI ocspResponderUri = ocspService.getAccessLocation();
final OCSPReq request = getOcspRequest(certificateId, ocspService);

if (!ocspService.doesSupportNonce()) {
LOG.debug("Disabling OCSP nonce extension");
}
if (!ocspService.doesSupportNonce()) {
LOG.debug("Disabling OCSP nonce extension");
}

try {
LOG.debug("Sending OCSP request");
final OCSPResp response = requireNonNull(ocspClient.request(ocspResponderUri, request), "OCSPResp");
if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) {
Expand All @@ -113,20 +106,33 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
}
LOG.debug("OCSP response received successfully");

verifyOcspResponse(basicResponse, ocspService, certificateId, issuerCertificate, maxOcspResponseThisUpdateAge);
verifyOcspResponse(basicResponse, ocspService, certificateId, issuerCertificate);
if (ocspService.doesSupportNonce()) {
checkNonce(request, basicResponse, ocspResponderUri);
}
LOG.debug("OCSP response verified successfully");

return List.of(new RevocationInfo(ocspResponderUri, Map.of(RevocationInfo.KEY_OCSP_RESPONSE, response)));

} catch (OCSPException | CertificateException | OperatorCreationException | IOException | OCSPClientException e) {
} catch (OCSPException | CertificateException | OperatorCreationException | OCSPClientException e) {
throw new UserCertificateOCSPCheckFailedException(e, ocspResponderUri);
}
}

protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId, X509Certificate issuerCertificate, Duration maxOcspResponseThisUpdateAge) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
protected static OCSPReq getOcspRequest(CertificateID certificateId, OcspService ocspService) throws UserCertificateOCSPException {
final OCSPReq request;
try {
request = new OcspRequestBuilder()
.withCertificateId(certificateId)
.enableOcspNonce(ocspService.doesSupportNonce())
.build();
} catch (OCSPException e) {
throw new UserCertificateOCSPException("Unable to create OCSP request", e);
}
return request;
}

protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId, X509Certificate issuerCertificate) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
// The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt.
//
// 3.2. Signed Response Acceptance Requirements
Expand All @@ -147,7 +153,16 @@ protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspS
ocspService.getAccessLocation());
}

// 2. The signature on the response is valid.
// Steps 3 and 4 are performed BEFORE step 2. The responder certificate is
// attacker-controlled until validated; verifying the response signature first lets
// an untrusted key drive expensive or unsafe crypto (e.g., a huge RSA public exponent)
// that later rejection can't undo.

// 3. The identity of the signer matches the intended recipient of the
// request.
//
// 4. The signer is currently authorized to provide a response for the
// certificate in question.

// We assume that the responder includes its certificate in the certs field of the response
// that helps us to verify it. According to RFC 2560 this field is optional, but including it
Expand All @@ -158,13 +173,6 @@ protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspS
}
// The first certificate is the responder certificate, other certificates, if given, are the certificate's chain.
final X509CertificateHolder responderCert = basicResponse.getCerts()[0];
OcspResponseValidator.validateResponseSignature(basicResponse, responderCert, ocspService.getAccessLocation());

// 3. The identity of the signer matches the intended recipient of the
// request.
//
// 4. The signer is currently authorized to provide a response for the
// certificate in question.

// Use the clock instance so that the date can be mocked in tests.
final Date now = DateAndTime.DefaultClock.getInstance().now();
Expand All @@ -175,14 +183,18 @@ protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspS
throw new UserCertificateOCSPCheckFailedException(e, ocspService.getAccessLocation());
}

// 2. The signature on the response is valid.

OcspResponseValidator.validateResponseSignature(basicResponse, responderCert, ocspService.getAccessLocation());

// 5. The time at which the status being indicated is known to be
// correct (thisUpdate) is sufficiently recent.
//
// 6. When available, the time at or before which newer information will
// be available about the status of the certificate (nextUpdate) is
// greater than the current time.

OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, ocspService.getAccessLocation());
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, ocspService.getMaxThisUpdateAge(), ocspService.getMaxNextUpdateAge(), ocspService.getAccessLocation());

// Now we can accept the signed response as valid and validate the certificate status.
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, ocspService.getAccessLocation());
Expand All @@ -202,11 +214,15 @@ protected static void checkNonce(OCSPReq request, BasicOCSPResp response, URI oc
}
}

protected static CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws CertificateEncodingException, IOException, OCSPException {
final BigInteger serial = subjectCertificate.getSerialNumber();
final DigestCalculator digestCalculator = DigestCalculatorImpl.sha1();
return new CertificateID(digestCalculator,
new X509CertificateHolder(issuerCertificate.getEncoded()), serial);
protected static CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws UserCertificateOCSPException {
try {
final BigInteger serial = subjectCertificate.getSerialNumber();
final DigestCalculator digestCalculator = DigestCalculatorImpl.sha1();
return new CertificateID(digestCalculator,
new X509CertificateHolder(issuerCertificate.getEncoded()), serial);
} catch (CertificateEncodingException | IOException | OCSPException e) {
throw new UserCertificateOCSPException("Unable to compute certificateId for subject certificate", e);
}
}

protected static String ocspStatusToString(int status) {
Expand All @@ -227,8 +243,4 @@ protected OcspClient getOcspClient() {
protected OcspServiceProvider getOcspServiceProvider() {
return ocspServiceProvider;
}

protected Duration getMaxOcspResponseThisUpdateAge() {
return maxOcspResponseThisUpdateAge;
}
}
29 changes: 24 additions & 5 deletions src/main/java/eu/webeid/ocsp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Use `withCertificateRevocationChecker(...)` to supply your own implementation or
## Contents

- [Choosing a revocation checker](#choosing-a-revocation-checker)
- [OCSP responder revocation check limitation](#ocsp-responder-revocation-check-limitation)
- [Implementing CertificateRevocationChecker](#implementing-certificaterevocationchecker)
- [Custom OCSP checker](#custom-ocsp-checker)
- [Custom PKIX revocation checker](#custom-pkix-revocation-checker)
Expand All @@ -35,6 +36,10 @@ The two custom checker options and `withoutUserCertificateRevocationCheck()` are

Platform OCSP networking and response freshness are controlled by the JDK provider; see [Platform OCSP timeouts](#platform-ocsp-timeouts). The v3 API builder-level OCSP client, timeout, designated-service and response-age options are no longer available; use the custom OCSP checker below when those controls are needed. Its timing constants do not configure the platform checker.

## OCSP responder revocation check limitation

The bundled custom OCSP checker and OpenJDK 17's OCSP implementation do not check delegated responder certificate revocation, even when `id-pkix-ocsp-nocheck` is absent. A revoked responder with a compromised key can therefore produce signed responses that are accepted. Applications requiring responder revocation checking must enforce it separately or supply a `CertificateRevocationChecker` that does so.

## Implementing CertificateRevocationChecker

Implement [CertificateRevocationChecker](../security/validator/revocationcheck/CertificateRevocationChecker.java) and supply it through `AuthTokenValidatorBuilder.withCertificateRevocationChecker(...)`. This extension point can use your own revocation service, transport or validation policy.
Expand Down Expand Up @@ -75,14 +80,16 @@ List<X509Certificate> trustedCAs = List.of(trustedIntermediateCACertificates());
AiaOcspServiceConfiguration aiaConfiguration = new AiaOcspServiceConfiguration(
Set.of(), // AIA responder URLs for which request and response nonce checks are disabled.
CertificateValidator.buildTrustAnchorsFromCertificates(trustedCAs),
CertificateValidator.buildCertStoreFromCertificates(trustedCAs)
CertificateValidator.buildCertStoreFromCertificates(trustedCAs),
OcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE,
OcspCertificateRevocationChecker.DEFAULT_NEXT_UPDATE_AGE,
true // Require the Key Usage extension in responder certificates.
);
OcspServiceProvider services = new OcspServiceProvider(null, aiaConfiguration);
OcspCertificateRevocationChecker checker = new OcspCertificateRevocationChecker(
OcspClientImpl.build(Duration.ofSeconds(5)),
services,
OcspCertificateRevocationChecker.DEFAULT_TIME_SKEW,
OcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE
OcspCertificateRevocationChecker.DEFAULT_TIME_SKEW
);

AuthTokenValidator validator = new AuthTokenValidatorBuilder()
Expand All @@ -94,7 +101,16 @@ AuthTokenValidator validator = new AuthTokenValidatorBuilder()

The five-second connection and response timeout above is an explicit example setting. For a custom Java `HttpClient`, use `new OcspClientImpl(httpClient, responseTimeout)` and configure the connection timeout on that client. Alternatively, supply your own `OcspClient` implementation. See [OcspClientOverrideTest](../../../../../test/java/eu/webeid/ocsp/client/OcspClientOverrideTest.java).

The custom checker's suggested constants are 15 minutes for clock/update skew and 2 minutes for maximum `thisUpdate` age; pass different positive durations to its constructor to change them. These checks are implemented by [OcspResponseValidator](protocol/OcspResponseValidator.java).
The custom checker's suggested constant for clock/update skew is 15 minutes; pass a different positive duration to its constructor to change it.

The maximum ages of the OCSP response's `thisUpdate` and `nextUpdate` times are configured per OCSP service, via the constructor of `AiaOcspServiceConfiguration`, `DesignatedOcspServiceConfiguration` or `FallbackOcspServiceConfiguration`:

- `maxThisUpdateAge` – the maximum age of the OCSP response's `thisUpdate` time before the response is too old to rely on. `OcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE` is 2 minutes.
- `maxNextUpdateAge` – the maximum age of the OCSP response's `nextUpdate` time before the response is too old to rely on. `OcspCertificateRevocationChecker.DEFAULT_NEXT_UPDATE_AGE` is 15 minutes, which is equal to the default allowed time skew.

These checks are implemented by [OcspResponseValidator](protocol/OcspResponseValidator.java).

The examples require OCSP responder certificates to have the Key Usage extension. To accept responder certificates that lack the Key Usage extension, pass `false` as the last `isKeyUsageRequired` argument of the `AiaOcspServiceConfiguration`, `DesignatedOcspServiceConfiguration` or `FallbackOcspServiceConfiguration` constructor. If the Key Usage extension is present, it must contain Digital Signature or Non Repudiation and must not contain Certificate Signing. The Extended Key Usage extension must still contain OCSP Signing, and the certificate must still not be a CA certificate. For AIA services, the setting applies only to delegated responder certificates, not to the issuer CA signing its own responses. The setting of a fallback configuration does not propagate to its `nextFallbackConfiguration`.

For a designated responder, replace the `services` definition above with the following configuration. `responderCertificate` must be the service's trusted signing certificate and `supportedIssuers` the collection of issuer certificates served by it:

Expand All @@ -106,7 +122,10 @@ DesignatedOcspServiceConfiguration designated = new DesignatedOcspServiceConfigu
URI.create("https://ocsp.example.org"),
responderCertificate,
supportedIssuers,
true // This service supports nonces.
true, // This service supports nonces.
OcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE,
OcspCertificateRevocationChecker.DEFAULT_NEXT_UPDATE_AGE,
true // Require the Key Usage extension in responder certificates.
);
OcspServiceProvider services = new OcspServiceProvider(designated, aiaConfiguration);
```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: Estonian Information System Authority
// SPDX-License-Identifier: MIT

package eu.webeid.ocsp.exceptions;

import eu.webeid.security.exceptions.CertificateRevocationCheckFailedException;

public class UserCertificateOCSPException extends CertificateRevocationCheckFailedException {

public UserCertificateOCSPException(String message) {
super(message);
}

public UserCertificateOCSPException(String message, Throwable exception) {
super(message, exception);
}

}
Loading
Loading