Skip to content

Commit 7dbf346

Browse files
committed
Add rejectUnknownOcspResponseStatus configuration option
1 parent 59bf12d commit 7dbf346

File tree

8 files changed

+165
-17
lines changed

8 files changed

+165
-17
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) 2020-2025 Estonian Information System Authority
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
23+
package eu.webeid.security.exceptions;
24+
25+
/**
26+
* Thrown when the user certificate has been revoked.
27+
*/
28+
public class UserCertificateUnknownException extends AuthTokenException {
29+
30+
public UserCertificateUnknownException(String msg) {
31+
super(msg);
32+
}
33+
}

src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public final class AuthTokenValidationConfiguration {
5252
private Duration ocspRequestTimeout = Duration.ofSeconds(5);
5353
private Duration allowedOcspResponseTimeSkew = Duration.ofMinutes(15);
5454
private Duration maxOcspResponseThisUpdateAge = Duration.ofMinutes(2);
55+
private boolean rejectUnknownOcspResponseStatus;
5556
private DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration;
5657
private Collection<FallbackOcspServiceConfiguration> fallbackOcspServiceConfigurations = new HashSet<>();
5758
private CircuitBreakerConfig circuitBreakerConfig;
@@ -74,6 +75,7 @@ private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other)
7475
this.ocspRequestTimeout = other.ocspRequestTimeout;
7576
this.allowedOcspResponseTimeSkew = other.allowedOcspResponseTimeSkew;
7677
this.maxOcspResponseThisUpdateAge = other.maxOcspResponseThisUpdateAge;
78+
this.rejectUnknownOcspResponseStatus = other.rejectUnknownOcspResponseStatus;
7779
this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration;
7880
this.fallbackOcspServiceConfigurations = Set.copyOf(other.fallbackOcspServiceConfigurations);
7981
this.circuitBreakerConfig = other.circuitBreakerConfig;
@@ -153,6 +155,14 @@ public void setCircuitBreakerConfig(CircuitBreakerConfig circuitBreakerConfig) {
153155
this.circuitBreakerConfig = circuitBreakerConfig;
154156
}
155157

158+
public boolean isRejectUnknownOcspResponseStatus() {
159+
return rejectUnknownOcspResponseStatus;
160+
}
161+
162+
public void setRejectUnknownOcspResponseStatus(boolean rejectUnknownOcspResponseStatus) {
163+
this.rejectUnknownOcspResponseStatus = rejectUnknownOcspResponseStatus;
164+
}
165+
156166
/**
157167
* Checks that the configuration parameters are valid.
158168
*

src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,18 @@ public AuthTokenValidatorBuilder withCircuitBreakerConfig(int slidingWindowSize,
226226
return this;
227227
}
228228

229+
/**
230+
* // TODO: Describe the configuration option
231+
*
232+
* @param rejectUnknownOcspResponseStatus configures whether only GOOD or REVOKED are accepted as valid OCSP response statuses
233+
* @return the builder instance for method chaining
234+
*/
235+
public AuthTokenValidatorBuilder withRejectUnknownOcspResponseStatus(boolean rejectUnknownOcspResponseStatus) {
236+
configuration.setRejectUnknownOcspResponseStatus(rejectUnknownOcspResponseStatus);
237+
LOG.debug("Using the reject unknown OCSP response status validation configuration");
238+
return this;
239+
}
240+
229241
/**
230242
* Uses the provided OCSP client instance during user certificate revocation check with OCSP.
231243
* The provided client instance must be thread-safe.

src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,11 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
9595
trustedCACertificateAnchors,
9696
trustedCACertificateCertStore),
9797
configuration.getFallbackOcspServiceConfigurations());
98-
resilientOcspService = new ResilientOcspService(ocspClient, ocspServiceProvider, configuration.getCircuitBreakerConfig(), configuration.getAllowedOcspResponseTimeSkew(),
99-
configuration.getMaxOcspResponseThisUpdateAge());
98+
resilientOcspService = new ResilientOcspService(ocspClient, ocspServiceProvider,
99+
configuration.getCircuitBreakerConfig(),
100+
configuration.getAllowedOcspResponseTimeSkew(),
101+
configuration.getMaxOcspResponseThisUpdateAge(),
102+
configuration.isRejectUnknownOcspResponseStatus());
100103
}
101104

102105
authTokenSignatureValidator = new AuthTokenSignatureValidator(configuration.getSiteOrigin());

src/main/java/eu/webeid/security/validator/ocsp/OcspResponseValidator.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import eu.webeid.security.exceptions.OCSPCertificateException;
2727
import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
2828
import eu.webeid.security.exceptions.UserCertificateRevokedException;
29+
import eu.webeid.security.exceptions.UserCertificateUnknownException;
2930
import eu.webeid.security.util.DateAndTime;
3031
import eu.webeid.security.validator.ocsp.service.OcspService;
3132
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
@@ -54,7 +55,7 @@
5455

5556
public final class OcspResponseValidator {
5657

57-
public static void validateOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, Duration allowedOcspResponseTimeSkew, Duration maxOcspResponseThisUpdateAge, CertificateID requestCertificateId) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
58+
public static void validateOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, Duration allowedOcspResponseTimeSkew, Duration maxOcspResponseThisUpdateAge, boolean rejectUnknownOcspResponseStatus, CertificateID requestCertificateId) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
5859
// The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt.
5960
//
6061
// 3.2. Signed Response Acceptance Requirements
@@ -107,7 +108,7 @@ public static void validateOcspResponse(BasicOCSPResp basicResponse, OcspService
107108
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge);
108109

109110
// Now we can accept the signed response as valid and validate the certificate status.
110-
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse);
111+
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, rejectUnknownOcspResponseStatus);
111112
}
112113

113114
/**
@@ -180,7 +181,7 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
180181
}
181182
}
182183

183-
public static void validateSubjectCertificateStatus(SingleResp certStatusResponse) throws UserCertificateRevokedException {
184+
public static void validateSubjectCertificateStatus(SingleResp certStatusResponse, boolean rejectUnknownOcspResponseStatus) throws AuthTokenException {
184185
final CertificateStatus status = certStatusResponse.getCertStatus();
185186
if (status == null) {
186187
return;
@@ -191,9 +192,11 @@ public static void validateSubjectCertificateStatus(SingleResp certStatusRespons
191192
new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason()) :
192193
new UserCertificateRevokedException());
193194
} else if (status instanceof UnknownStatus) {
194-
throw new UserCertificateRevokedException("Unknown status");
195+
throw rejectUnknownOcspResponseStatus ? new UserCertificateUnknownException("User certificate has been revoked: Unknown status")
196+
: new UserCertificateRevokedException("Unknown status");
195197
} else {
196-
throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown");
198+
throw rejectUnknownOcspResponseStatus ? new UserCertificateUnknownException("Status is neither good, revoked nor unknown")
199+
: new UserCertificateRevokedException("Status is neither good, revoked nor unknown");
197200
}
198201
}
199202

src/main/java/eu/webeid/security/validator/ocsp/ResilientOcspService.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import eu.webeid.security.exceptions.AuthTokenException;
2626
import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
2727
import eu.webeid.security.exceptions.UserCertificateRevokedException;
28+
import eu.webeid.security.exceptions.UserCertificateUnknownException;
2829
import eu.webeid.security.validator.ocsp.service.OcspService;
2930
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
3031
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
@@ -61,13 +62,15 @@ public class ResilientOcspService {
6162
private final OcspServiceProvider ocspServiceProvider;
6263
private final Duration allowedOcspResponseTimeSkew;
6364
private final Duration maxOcspResponseThisUpdateAge;
65+
private final boolean rejectUnknownOcspResponseStatus;
6466
private final CircuitBreakerRegistry circuitBreakerRegistry;
6567

66-
public ResilientOcspService(OcspClient ocspClient, OcspServiceProvider ocspServiceProvider, CircuitBreakerConfig circuitBreakerConfig, Duration allowedOcspResponseTimeSkew, Duration maxOcspResponseThisUpdateAge) {
68+
public ResilientOcspService(OcspClient ocspClient, OcspServiceProvider ocspServiceProvider, CircuitBreakerConfig circuitBreakerConfig, Duration allowedOcspResponseTimeSkew, Duration maxOcspResponseThisUpdateAge, boolean rejectUnknownOcspResponseStatus) {
6769
this.ocspClient = ocspClient;
6870
this.ocspServiceProvider = ocspServiceProvider;
6971
this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew;
7072
this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge;
73+
this.rejectUnknownOcspResponseStatus = rejectUnknownOcspResponseStatus;
7174
this.circuitBreakerRegistry = CircuitBreakerRegistry.custom()
7275
.withCircuitBreakerConfig(getCircuitBreakerConfig(circuitBreakerConfig))
7376
.build();
@@ -91,7 +94,7 @@ public OcspService validateSubjectCertificateNotRevoked(X509Certificate subjectC
9194
CheckedFunction0<OcspService> fallbackSupplier = () -> request(ocspService.getFallbackService(), subjectCertificate, issuerCertificate);
9295
CheckedFunction0<OcspService> decoratedSupplier = Decorators.ofCheckedSupplier(primarySupplier)
9396
.withCircuitBreaker(circuitBreaker)
94-
.withFallback(List.of(UserCertificateOCSPCheckFailedException.class, CallNotPermittedException.class), e -> fallbackSupplier.apply()) // TODO: Any other exceptions to trigger fallback? Resilience4j does not support Predicate<Exception> shouldFallback = e -> !(e instanceof UserCertificateRevokedException); in withFallback API.
97+
.withFallback(List.of(UserCertificateOCSPCheckFailedException.class, CallNotPermittedException.class, UserCertificateUnknownException.class), e -> fallbackSupplier.apply()) // TODO: Any other exceptions to trigger fallback? Resilience4j does not support Predicate<Exception> shouldFallback = e -> !(e instanceof UserCertificateRevokedException); in withFallback API.
9598
.decorate();
9699

97100
return Try.of(decoratedSupplier).getOrElseThrow(throwable -> {
@@ -129,7 +132,7 @@ private OcspService request(OcspService ocspService, X509Certificate subjectCert
129132
throw new UserCertificateOCSPCheckFailedException("Missing Basic OCSP Response");
130133
}
131134

132-
OcspResponseValidator.validateOcspResponse(basicResponse, ocspService, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, certificateId);
135+
OcspResponseValidator.validateOcspResponse(basicResponse, ocspService, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, rejectUnknownOcspResponseStatus, certificateId);
133136
LOG.debug("OCSP check result is GOOD");
134137

135138
if (ocspService.doesSupportNonce()) {
@@ -153,7 +156,7 @@ private static CircuitBreakerConfig getCircuitBreakerConfig(CircuitBreakerConfig
153156
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
154157
.slidingWindowSize(100)
155158
.minimumNumberOfCalls(10)
156-
.ignoreExceptions(UserCertificateRevokedException.class) // TODO: Revoked status is a valid response, not a failure. Any other exceptions to ignore?
159+
.ignoreExceptions(UserCertificateRevokedException.class) // TODO: Revoked status is a valid response, not a failure and should be ignored. Any other exceptions to ignore?
157160
.automaticTransitionFromOpenToHalfOpenEnabled(true);
158161

159162
if (circuitBreakerConfig != null) { // TODO: What do we allow to configure?

src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,11 @@ private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedVal
348348
}
349349

350350
private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidator(OcspClient client, OcspServiceProvider ocspServiceProvider) {
351-
ResilientOcspService resilientOcspService = new ResilientOcspService(client, ocspServiceProvider, null, CONFIGURATION.getAllowedOcspResponseTimeSkew(), CONFIGURATION.getMaxOcspResponseThisUpdateAge());
351+
ResilientOcspService resilientOcspService = new ResilientOcspService(client, ocspServiceProvider,
352+
null,
353+
CONFIGURATION.getAllowedOcspResponseTimeSkew(),
354+
CONFIGURATION.getMaxOcspResponseThisUpdateAge(),
355+
CONFIGURATION.isRejectUnknownOcspResponseStatus());
352356
return new SubjectCertificateNotRevokedValidator(resilientOcspService, trustedValidator);
353357
}
354358

0 commit comments

Comments
 (0)