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
68 changes: 37 additions & 31 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 Down Expand Up @@ -182,7 +188,7 @@ protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspS
// 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 +208,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 +237,4 @@ protected OcspClient getOcspClient() {
protected OcspServiceProvider getOcspServiceProvider() {
return ocspServiceProvider;
}

protected Duration getMaxOcspResponseThisUpdateAge() {
return maxOcspResponseThisUpdateAge;
}
}
24 changes: 19 additions & 5 deletions src/main/java/eu/webeid/ocsp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,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 +96,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 +117,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