Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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;
}
}
20 changes: 15 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,15 @@ 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
);
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 +95,14 @@ 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).

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 +114,9 @@ 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
);
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.AuthTokenException;

public class UserCertificateOCSPException extends AuthTokenException {

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

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

}
61 changes: 53 additions & 8 deletions src/main/java/eu/webeid/ocsp/protocol/OcspResponseValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;

public final class OcspResponseValidator {
Expand All @@ -35,14 +36,57 @@ public final class OcspResponseValidator {
* https://oidref.com/1.3.6.1.5.5.7.3.9
*/
private static final String OID_OCSP_SIGNING = "1.3.6.1.5.5.7.3.9";
private static final int KEY_USAGE_DIGITAL_SIGNATURE_BIT_INDEX = 0;
private static final int KEY_USAGE_KEY_CERT_SIGN_BIT_INDEX = 5;
private static final String ERROR_PREFIX = "Certificate status update time check failed: ";

public static void validateHasSigningExtension(X509Certificate certificate) throws OCSPCertificateException {
public static void validateBasicConstraintsNotCA(X509Certificate certificate) throws OCSPCertificateException {
Objects.requireNonNull(certificate, "certificate");
// X509Certificate.getBasicConstraints() returns -1 when the Basic Constraints extension is absent
// or when cA=FALSE, and a non-negative value only when cA=TRUE (the pathLenConstraint, or
// Integer.MAX_VALUE when cA=TRUE without pathLenConstraint).
if (certificate.getBasicConstraints() >= 0) {
throw new OCSPCertificateException("Certificate " + certificate.getSubjectX500Principal() +
" must not be a CA certificate (Basic Constraints CA:TRUE is not allowed for OCSP responder)");
}
}

public static void validateKeyUsageDigitalSignature(X509Certificate certificate) throws OCSPCertificateException {
Objects.requireNonNull(certificate, "certificate");
final boolean[] keyUsage = certificate.getKeyUsage();
if (keyUsage == null) {

@mrts mrts Oct 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should requiring the Key Usage extension be configurable?

RFC 6960 §4.2.2.2 requires the OCSP Signing EKU for delegated responders, but does not require a Key Usage extension. RFC 5280 §4.2.1.12 requires consistent usage when both extensions are present.

The existing SK OCSP responder certificate has the OCSP Signing EKU but no Key Usage extension. Making this requirement configurable would preserve compatibility with it while allowing integrators to enforce the stricter policy.

throw new OCSPCertificateException("Certificate " + certificate.getSubjectX500Principal() +
" does not contain the Key Usage extension required for OCSP response signing");
}
if (keyUsage.length <= KEY_USAGE_DIGITAL_SIGNATURE_BIT_INDEX || !keyUsage[KEY_USAGE_DIGITAL_SIGNATURE_BIT_INDEX]) {
throw new OCSPCertificateException("Certificate " + certificate.getSubjectX500Principal() +
" Key Usage extension does not contain Digital Signature, which is required for OCSP response signing");
}
}

public static void validateKeyUsageNotCertificateSigning(X509Certificate certificate) throws OCSPCertificateException {
Objects.requireNonNull(certificate, "certificate");
final boolean[] keyUsage = certificate.getKeyUsage();
if (keyUsage == null) {
return;
}
if (keyUsage.length > KEY_USAGE_KEY_CERT_SIGN_BIT_INDEX && keyUsage[KEY_USAGE_KEY_CERT_SIGN_BIT_INDEX]) {
throw new OCSPCertificateException("Certificate " + certificate.getSubjectX500Principal() +
" Key Usage extension contains Certificate Signing, which is not allowed for OCSP responder");
}
}

public static void validateExtendedKeyUsageOcspSigning(X509Certificate certificate) throws OCSPCertificateException {
Objects.requireNonNull(certificate, "certificate");
try {
if (certificate.getExtendedKeyUsage() == null || !certificate.getExtendedKeyUsage().contains(OID_OCSP_SIGNING)) {
final List<String> extendedKeyUsage = certificate.getExtendedKeyUsage();
if (extendedKeyUsage == null) {
throw new OCSPCertificateException("Certificate " + certificate.getSubjectX500Principal() +
" does not contain the Extended Key Usage extension required for OCSP response signing");
}
if (!extendedKeyUsage.contains(OID_OCSP_SIGNING)) {
throw new OCSPCertificateException("Certificate " + certificate.getSubjectX500Principal() +
" does not contain the key usage extension for OCSP response signing");
" Extended Key Usage extension does not contain OCSP Signing, which is required for OCSP response signing");
}
} catch (CertificateParsingException e) {
throw new OCSPCertificateException("Certificate parsing failed:", e);
Expand All @@ -58,7 +102,7 @@ public static void validateResponseSignature(BasicOCSPResp basicResponse, X509Ce
}
}

public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisUpdateAge, Duration maxNextUpdateAge, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
// From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt:
// 4.2.2. Notes on OCSP Responses
// 4.2.2.1. Time
Expand All @@ -69,9 +113,9 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
// If nextUpdate is not set, the responder is indicating that newer
// revocation information is available all the time.
final Instant now = DateAndTime.DefaultClock.getInstance().now().toInstant();
final Instant earliestAcceptableTimeSkew = now.minus(allowedTimeSkew);
final Instant latestAcceptableTimeSkew = now.plus(allowedTimeSkew);
final Instant minimumValidThisUpdateTime = now.minus(maxThisupdateAge);
final Instant minimumValidThisUpdateTime = now.minus(maxThisUpdateAge);
final Instant minimumValidNextUpdateTime = now.minus(maxNextUpdateAge);

final Instant thisUpdate = certStatusResponse.getThisUpdate().toInstant();
if (thisUpdate.isAfter(latestAcceptableTimeSkew)) {
Expand All @@ -89,9 +133,10 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
return;
}
final Instant nextUpdate = certStatusResponse.getNextUpdate().toInstant();
if (nextUpdate.isBefore(earliestAcceptableTimeSkew)) {
if (nextUpdate.isBefore(minimumValidNextUpdateTime)) {
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
"nextUpdate '" + nextUpdate + "' is in the past", ocspResponderUri);
"nextUpdate '" + nextUpdate + "' is too old, " +
"minimum time allowed: '" + minimumValidNextUpdateTime + "'", ocspResponderUri);
}
if (nextUpdate.isBefore(thisUpdate)) {
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
Expand Down
Loading
Loading