Skip to content

Commit 92e01f4

Browse files
aarmammadislm
andcommitted
AUT-2473 Add ResilientOcspRevocationChecker
Co-authored-by: Madis Jaagup Laurson <madisjaagup.laurson@nortal.com>
1 parent 310224e commit 92e01f4

File tree

9 files changed

+403
-9
lines changed

9 files changed

+403
-9
lines changed

pom.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<bouncycastle.version>1.81</bouncycastle.version>
1717
<jackson.version>2.19.1</jackson.version>
1818
<slf4j.version>2.0.17</slf4j.version>
19+
<resilience4j.version>1.7.0</resilience4j.version>
1920
<junit-jupiter.version>5.13.3</junit-jupiter.version>
2021
<assertj.version>3.27.3</assertj.version>
2122
<mockito.version>5.18.0</mockito.version>
@@ -65,6 +66,29 @@
6566
<artifactId>bcpkix-jdk18on</artifactId>
6667
<version>${bouncycastle.version}</version>
6768
</dependency>
69+
<dependency>
70+
<groupId>io.github.resilience4j</groupId>
71+
<artifactId>resilience4j-all</artifactId>
72+
<version>${resilience4j.version}</version>
73+
<exclusions>
74+
<exclusion>
75+
<groupId>io.github.resilience4j</groupId>
76+
<artifactId>resilience4j-bulkhead</artifactId>
77+
</exclusion>
78+
<exclusion>
79+
<groupId>io.github.resilience4j</groupId>
80+
<artifactId>resilience4j-cache</artifactId>
81+
</exclusion>
82+
<exclusion>
83+
<groupId>io.github.resilience4j</groupId>
84+
<artifactId>resilience4j-ratelimiter</artifactId>
85+
</exclusion>
86+
<exclusion>
87+
<groupId>io.github.resilience4j</groupId>
88+
<artifactId>resilience4j-timelimiter</artifactId>
89+
</exclusion>
90+
</exclusions>
91+
</dependency>
6892

6993
<dependency>
7094
<groupId>org.junit.jupiter</groupId>

src/main/java/eu/webeid/ocsp/OcspCertificateRevocationChecker.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
import static eu.webeid.security.util.DateAndTime.requirePositiveDuration;
6565
import static java.util.Objects.requireNonNull;
6666

67-
public final class OcspCertificateRevocationChecker implements CertificateRevocationChecker {
67+
public class OcspCertificateRevocationChecker implements CertificateRevocationChecker {
6868

6969
public static final Duration DEFAULT_TIME_SKEW = Duration.ofMinutes(15);
7070
public static final Duration DEFAULT_THIS_UPDATE_AGE = Duration.ofMinutes(2);
@@ -144,7 +144,7 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
144144
}
145145
}
146146

147-
private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
147+
protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
148148
// The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt.
149149
//
150150
// 3.2. Signed Response Acceptance Requirements
@@ -202,7 +202,7 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer
202202
LOG.debug("OCSP check result is GOOD");
203203
}
204204

205-
private static void checkNonce(OCSPReq request, BasicOCSPResp response, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
205+
protected static void checkNonce(OCSPReq request, BasicOCSPResp response, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
206206
final Extension requestNonce = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
207207
final Extension responseNonce = response.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
208208
if (requestNonce == null || responseNonce == null) {
@@ -215,14 +215,14 @@ private static void checkNonce(OCSPReq request, BasicOCSPResp response, URI ocsp
215215
}
216216
}
217217

218-
private static CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws CertificateEncodingException, IOException, OCSPException {
218+
protected static CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws CertificateEncodingException, IOException, OCSPException {
219219
final BigInteger serial = subjectCertificate.getSerialNumber();
220220
final DigestCalculator digestCalculator = DigestCalculatorImpl.sha1();
221221
return new CertificateID(digestCalculator,
222222
new X509CertificateHolder(issuerCertificate.getEncoded()), serial);
223223
}
224224

225-
private static String ocspStatusToString(int status) {
225+
protected static String ocspStatusToString(int status) {
226226
return switch (status) {
227227
case OCSPResp.MALFORMED_REQUEST -> "malformed request";
228228
case OCSPResp.INTERNAL_ERROR -> "internal error";
@@ -233,4 +233,11 @@ private static String ocspStatusToString(int status) {
233233
};
234234
}
235235

236+
protected OcspClient getOcspClient() {
237+
return ocspClient;
238+
}
239+
240+
protected OcspServiceProvider getOcspServiceProvider() {
241+
return ocspServiceProvider;
242+
}
236243
}

src/main/java/eu/webeid/ocsp/service/AiaOcspService.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package eu.webeid.ocsp.service;
2424

25+
import eu.webeid.resilientocsp.service.FallbackOcspService;
2526
import eu.webeid.security.certificate.CertificateValidator;
2627
import eu.webeid.security.exceptions.AuthTokenException;
2728
import eu.webeid.ocsp.exceptions.OCSPCertificateException;
@@ -52,13 +53,15 @@ public class AiaOcspService implements OcspService {
5253
private final CertStore trustedCACertificateCertStore;
5354
private final URI url;
5455
private final boolean supportsNonce;
56+
private final FallbackOcspService fallbackOcspService;
5557

56-
public AiaOcspService(AiaOcspServiceConfiguration configuration, X509Certificate certificate) throws AuthTokenException {
58+
public AiaOcspService(AiaOcspServiceConfiguration configuration, X509Certificate certificate, FallbackOcspService fallbackOcspService) throws AuthTokenException {
5759
Objects.requireNonNull(configuration);
5860
this.trustedCACertificateAnchors = configuration.getTrustedCACertificateAnchors();
5961
this.trustedCACertificateCertStore = configuration.getTrustedCACertificateCertStore();
6062
this.url = getOcspAiaUrlFromCertificate(Objects.requireNonNull(certificate));
6163
this.supportsNonce = !configuration.getNonceDisabledOcspUrls().contains(this.url);
64+
this.fallbackOcspService = fallbackOcspService;
6265
}
6366

6467
@Override
@@ -71,6 +74,11 @@ public URI getAccessLocation() {
7174
return url;
7275
}
7376

77+
@Override
78+
public FallbackOcspService getFallbackService() {
79+
return fallbackOcspService;
80+
}
81+
7482
@Override
7583
public void validateResponderCertificate(X509CertificateHolder cert, Date now) throws AuthTokenException {
7684
try {

src/main/java/eu/webeid/ocsp/service/OcspService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,8 @@ public interface OcspService {
3636

3737
void validateResponderCertificate(X509CertificateHolder cert, Date now) throws AuthTokenException;
3838

39+
default OcspService getFallbackService() {
40+
return null;
41+
}
42+
3943
}

src/main/java/eu/webeid/ocsp/service/OcspServiceProvider.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,39 @@
2222

2323
package eu.webeid.ocsp.service;
2424

25+
import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException;
26+
import eu.webeid.resilientocsp.service.FallbackOcspService;
27+
import eu.webeid.resilientocsp.service.FallbackOcspServiceConfiguration;
2528
import eu.webeid.security.exceptions.AuthTokenException;
2629

30+
import java.net.URI;
2731
import java.security.cert.CertificateEncodingException;
2832
import java.security.cert.X509Certificate;
33+
import java.util.Collection;
34+
import java.util.Map;
2935
import java.util.Objects;
36+
import java.util.stream.Collectors;
37+
38+
import static eu.webeid.ocsp.protocol.OcspUrl.getOcspUri;
3039

3140
public class OcspServiceProvider {
3241

3342
private final DesignatedOcspService designatedOcspService;
3443
private final AiaOcspServiceConfiguration aiaOcspServiceConfiguration;
44+
private final Map<URI, FallbackOcspService> fallbackOcspServiceMap;
3545

3646
public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration, AiaOcspServiceConfiguration aiaOcspServiceConfiguration) {
47+
this(designatedOcspServiceConfiguration, aiaOcspServiceConfiguration, null);
48+
}
49+
50+
public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration, AiaOcspServiceConfiguration aiaOcspServiceConfiguration, Collection<FallbackOcspServiceConfiguration> fallbackOcspServiceConfigurations) {
3751
designatedOcspService = designatedOcspServiceConfiguration != null ?
3852
new DesignatedOcspService(designatedOcspServiceConfiguration)
3953
: null;
4054
this.aiaOcspServiceConfiguration = Objects.requireNonNull(aiaOcspServiceConfiguration, "aiaOcspServiceConfiguration");
55+
this.fallbackOcspServiceMap = fallbackOcspServiceConfigurations != null ? fallbackOcspServiceConfigurations.stream()
56+
.collect(Collectors.toMap(FallbackOcspServiceConfiguration::getOcspServiceAccessLocation, FallbackOcspService::new))
57+
: Map.of();
4158
}
4259

4360
/**
@@ -47,13 +64,16 @@ public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServ
4764
* @param certificate subject certificate that is to be checked with OCSP
4865
* @return either the designated or AIA OCSP service instance
4966
* @throws AuthTokenException when AIA URL is not found in certificate
50-
* @throws CertificateEncodingException when certificate is invalid
67+
* @throws IllegalArgumentException when certificate is invalid
5168
*/
5269
public OcspService getService(X509Certificate certificate) throws AuthTokenException, CertificateEncodingException {
5370
if (designatedOcspService != null && designatedOcspService.supportsIssuerOf(certificate)) {
5471
return designatedOcspService;
5572
}
56-
return new AiaOcspService(aiaOcspServiceConfiguration, certificate);
73+
URI ocspServiceUri = getOcspUri(certificate).orElseThrow(() ->
74+
new UserCertificateOCSPCheckFailedException("Getting the AIA OCSP responder field from the certificate failed"));
75+
FallbackOcspService fallbackOcspService = fallbackOcspServiceMap.get(ocspServiceUri);
76+
return new AiaOcspService(aiaOcspServiceConfiguration, certificate, fallbackOcspService);
5777
}
5878

5979
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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.resilientocsp;
24+
25+
import eu.webeid.ocsp.OcspCertificateRevocationChecker;
26+
import eu.webeid.ocsp.client.OcspClient;
27+
import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException;
28+
import eu.webeid.ocsp.exceptions.UserCertificateRevokedException;
29+
import eu.webeid.ocsp.protocol.OcspRequestBuilder;
30+
import eu.webeid.ocsp.service.OcspService;
31+
import eu.webeid.ocsp.service.OcspServiceProvider;
32+
import eu.webeid.security.exceptions.AuthTokenException;
33+
import eu.webeid.security.validator.revocationcheck.RevocationInfo;
34+
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
35+
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
36+
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
37+
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
38+
import io.github.resilience4j.decorators.Decorators;
39+
import io.github.resilience4j.retry.Retry;
40+
import io.github.resilience4j.retry.RetryConfig;
41+
import io.github.resilience4j.retry.RetryRegistry;
42+
import io.vavr.CheckedFunction0;
43+
import io.vavr.control.Try;
44+
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
45+
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
46+
import org.bouncycastle.cert.ocsp.CertificateID;
47+
import org.bouncycastle.cert.ocsp.OCSPException;
48+
import org.bouncycastle.cert.ocsp.OCSPReq;
49+
import org.bouncycastle.cert.ocsp.OCSPResp;
50+
import org.bouncycastle.operator.OperatorCreationException;
51+
import org.slf4j.Logger;
52+
import org.slf4j.LoggerFactory;
53+
54+
import java.io.IOException;
55+
import java.net.URI;
56+
import java.security.cert.CertificateException;
57+
import java.security.cert.X509Certificate;
58+
import java.time.Duration;
59+
import java.util.List;
60+
import java.util.Map;
61+
62+
import static java.util.Objects.requireNonNull;
63+
64+
public class ResilientOcspCertificateRevocationChecker extends OcspCertificateRevocationChecker {
65+
66+
private static final Logger LOG = LoggerFactory.getLogger(ResilientOcspCertificateRevocationChecker.class);
67+
68+
private final CircuitBreakerRegistry circuitBreakerRegistry;
69+
private final RetryRegistry retryRegistry;
70+
71+
public ResilientOcspCertificateRevocationChecker(OcspClient ocspClient,
72+
OcspServiceProvider ocspServiceProvider,
73+
CircuitBreakerConfig circuitBreakerConfig,
74+
RetryConfig retryConfig,
75+
Duration allowedOcspResponseTimeSkew,
76+
Duration maxOcspResponseThisUpdateAge) {
77+
super(ocspClient, ocspServiceProvider, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge);
78+
this.circuitBreakerRegistry = CircuitBreakerRegistry.custom()
79+
.withCircuitBreakerConfig(getCircuitBreakerConfig(circuitBreakerConfig))
80+
.build();
81+
this.retryRegistry = retryConfig != null ? RetryRegistry.custom()
82+
.withRetryConfig(getRetryConfigConfig(retryConfig))
83+
.build() : null;
84+
if (!LOG.isDebugEnabled()) {
85+
return;
86+
}
87+
this.circuitBreakerRegistry.getEventPublisher()
88+
.onEntryAdded(entryAddedEvent -> {
89+
CircuitBreaker circuitBreaker = entryAddedEvent.getAddedEntry();
90+
LOG.debug("CircuitBreaker {} added", circuitBreaker.getName());
91+
circuitBreaker.getEventPublisher()
92+
.onEvent(event -> LOG.debug(event.toString()));
93+
});
94+
}
95+
96+
@Override
97+
public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjectCertificate,
98+
X509Certificate issuerCertificate) throws AuthTokenException {
99+
OcspService ocspService;
100+
try {
101+
ocspService = getOcspServiceProvider().getService(subjectCertificate);
102+
} catch (CertificateException e) {
103+
throw new UserCertificateOCSPCheckFailedException(e, null);
104+
}
105+
final OcspService fallbackOcspService = ocspService.getFallbackService();
106+
if (fallbackOcspService == null) {
107+
return List.of(request(ocspService, subjectCertificate, issuerCertificate));
108+
}
109+
110+
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(ocspService.getAccessLocation().toASCIIString());
111+
CheckedFunction0<RevocationInfo> primarySupplier = () -> request(ocspService, subjectCertificate, issuerCertificate);
112+
CheckedFunction0<RevocationInfo> fallbackSupplier = () -> request(ocspService.getFallbackService(), subjectCertificate, issuerCertificate);
113+
Decorators.DecorateCheckedSupplier<RevocationInfo> decorateCheckedSupplier = Decorators.ofCheckedSupplier(primarySupplier);
114+
if (retryRegistry != null) {
115+
Retry retry = retryRegistry.retry(ocspService.getAccessLocation().toASCIIString());
116+
decorateCheckedSupplier.withRetry(retry);
117+
}
118+
decorateCheckedSupplier.withCircuitBreaker(circuitBreaker)
119+
.withFallback(List.of(UserCertificateOCSPCheckFailedException.class, CallNotPermittedException.class), e -> fallbackSupplier.apply());
120+
121+
CheckedFunction0<RevocationInfo> decoratedSupplier = decorateCheckedSupplier.decorate();
122+
123+
// TODO Collect the intermediate results
124+
return List.of(Try.of(decoratedSupplier).getOrElseThrow(throwable -> {
125+
if (throwable instanceof AuthTokenException) {
126+
return (AuthTokenException) throwable;
127+
}
128+
return new UserCertificateOCSPCheckFailedException(throwable, null);
129+
}));
130+
}
131+
132+
private RevocationInfo request(OcspService ocspService, X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws AuthTokenException {
133+
URI ocspResponderUri = null;
134+
try {
135+
ocspResponderUri = requireNonNull(ocspService.getAccessLocation(), "ocspResponderUri");
136+
137+
final CertificateID certificateId = getCertificateId(subjectCertificate, issuerCertificate);
138+
final OCSPReq request = new OcspRequestBuilder()
139+
.withCertificateId(certificateId)
140+
.enableOcspNonce(ocspService.doesSupportNonce())
141+
.build();
142+
143+
if (!ocspService.doesSupportNonce()) {
144+
LOG.debug("Disabling OCSP nonce extension");
145+
}
146+
147+
LOG.debug("Sending OCSP request");
148+
OCSPResp response = requireNonNull(getOcspClient().request(ocspResponderUri, request)); // TODO: This should trigger fallback?
149+
if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) {
150+
throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus()), ocspResponderUri);
151+
}
152+
153+
final BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject();
154+
if (basicResponse == null) {
155+
throw new UserCertificateOCSPCheckFailedException("Missing Basic OCSP Response", ocspResponderUri);
156+
}
157+
LOG.debug("OCSP response received successfully");
158+
159+
verifyOcspResponse(basicResponse, ocspService, certificateId);
160+
if (ocspService.doesSupportNonce()) {
161+
checkNonce(request, basicResponse, ocspResponderUri);
162+
}
163+
LOG.debug("OCSP response verified successfully");
164+
165+
return new RevocationInfo(ocspResponderUri, Map.ofEntries(
166+
Map.entry(RevocationInfo.KEY_OCSP_REQUEST, request),
167+
Map.entry(RevocationInfo.KEY_OCSP_RESPONSE, response)
168+
));
169+
} catch (OCSPException | CertificateException | OperatorCreationException | IOException e) {
170+
throw new UserCertificateOCSPCheckFailedException(e, ocspResponderUri);
171+
}
172+
}
173+
174+
private static CircuitBreakerConfig getCircuitBreakerConfig(CircuitBreakerConfig circuitBreakerConfig) {
175+
return CircuitBreakerConfig.from(circuitBreakerConfig)
176+
// Users must not be able to modify these three values.
177+
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
178+
.ignoreExceptions(UserCertificateRevokedException.class)
179+
.automaticTransitionFromOpenToHalfOpenEnabled(true)
180+
.build();
181+
}
182+
183+
private static RetryConfig getRetryConfigConfig(RetryConfig retryConfig) {
184+
return RetryConfig.from(retryConfig)
185+
// Users must not be able to modify this value.
186+
.ignoreExceptions(UserCertificateRevokedException.class)
187+
.build();
188+
}
189+
}

0 commit comments

Comments
 (0)