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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ The default validator uses the platform OCSP implementation to check certificate

Use `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` to add disallowed certificate policies. Estonian Mobile-ID policies are disallowed by default because smart-card authentication must not accept Mobile-ID certificates.

The user certificate must have the Digital Signature key usage by default. Use `withDigitalSignatureKeyUsageRequired(false)` to accept authentication certificates that do not assert it; the Key Usage extension must still be present, and an Extended Key Usage extension, if present, must still contain client authentication.

For more advanced revocation requirements, supply a `CertificateRevocationChecker` with `withCertificateRevocationChecker(...)`. The [OCSP configuration guide](src/main/java/eu/webeid/ocsp/README.md) covers custom implementations, the bundled OCSP checker, custom PKIX checkers, responder selection, HTTP settings, and nonce policies.

## Possible validation errors
Expand Down
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);
}

}
Loading