Skip to content

Commit 9f08c58

Browse files
committed
AUT-2552 Collect failed requests, add resilient OCSP specific exceptions
1 parent afb5be8 commit 9f08c58

11 files changed

+288
-34
lines changed

src/main/java/eu/webeid/ocsp/client/OcspClient.java

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

2323
package eu.webeid.ocsp.client;
2424

25+
import eu.webeid.ocsp.exceptions.OCSPClientException;
2526
import org.bouncycastle.cert.ocsp.OCSPReq;
2627
import org.bouncycastle.cert.ocsp.OCSPResp;
2728

28-
import java.io.IOException;
2929
import java.net.URI;
3030

3131
public interface OcspClient {
3232

33-
OCSPResp request(URI url, OCSPReq request) throws IOException;
33+
OCSPResp request(URI url, OCSPReq request) throws OCSPClientException;
3434

3535
}

src/main/java/eu/webeid/ocsp/client/OcspClientImpl.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package eu.webeid.ocsp.client;
2424

25+
import eu.webeid.ocsp.exceptions.OCSPClientException;
2526
import org.bouncycastle.cert.ocsp.OCSPReq;
2627
import org.bouncycastle.cert.ocsp.OCSPResp;
2728
import org.slf4j.Logger;
@@ -62,15 +63,21 @@ public static OcspClient build(Duration ocspRequestTimeout) {
6263
* @param uri OCSP server URL
6364
* @param ocspReq OCSP request
6465
* @return OCSP response from the server
65-
* @throws IOException if the request could not be executed due to cancellation, a connectivity problem or timeout,
66+
* @throws OCSPClientException if the request could not be executed due to cancellation, a connectivity problem or timeout,
6667
* or if the response status is not successful, or if response has wrong content type.
6768
*/
6869
@Override
69-
public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException {
70+
public OCSPResp request(URI uri, OCSPReq ocspReq) throws OCSPClientException {
71+
byte[] encodedOcspReq;
72+
try {
73+
encodedOcspReq = ocspReq.getEncoded();
74+
} catch (IOException e) {
75+
throw new OCSPClientException(e);
76+
}
7077
final HttpRequest request = HttpRequest.newBuilder()
7178
.uri(uri)
7279
.header(CONTENT_TYPE, OCSP_REQUEST_TYPE)
73-
.POST(HttpRequest.BodyPublishers.ofByteArray(ocspReq.getEncoded()))
80+
.POST(HttpRequest.BodyPublishers.ofByteArray(encodedOcspReq))
7481
.timeout(ocspRequestTimeout)
7582
.build();
7683

@@ -79,19 +86,28 @@ public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException {
7986
response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
8087
} catch (InterruptedException e) {
8188
Thread.currentThread().interrupt();
82-
throw new IOException("Interrupted while sending OCSP request", e);
89+
throw new OCSPClientException("Interrupted while sending OCSP request", e);
90+
} catch (IOException e) {
91+
throw new OCSPClientException(e);
8392
}
8493

8594
if (response.statusCode() != 200) {
86-
throw new IOException("OCSP request was not successful, response: " + response);
95+
throw new OCSPClientException("OCSP request was not successful", response.body(), response.statusCode());
8796
} else {
8897
LOG.debug("OCSP response: {}", response);
8998
}
9099
final String contentType = response.headers().firstValue(CONTENT_TYPE).orElse("");
91100
if (!contentType.startsWith(OCSP_RESPONSE_TYPE)) {
92-
throw new IOException("OCSP response content type is not " + OCSP_RESPONSE_TYPE);
101+
throw new OCSPClientException("OCSP response content type is not " + OCSP_RESPONSE_TYPE);
102+
}
103+
104+
OCSPResp ocspResp;
105+
try {
106+
ocspResp = new OCSPResp(response.body());
107+
} catch (IOException e) {
108+
throw new OCSPClientException(e);
93109
}
94-
return new OCSPResp(response.body());
110+
return ocspResp;
95111
}
96112

97113
public OcspClientImpl(HttpClient httpClient, Duration ocspRequestTimeout) {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.ocsp.exceptions;
24+
25+
public class OCSPClientException extends RuntimeException {
26+
27+
private byte[] responseBody;
28+
29+
private Integer statusCode;
30+
31+
public OCSPClientException() {
32+
}
33+
34+
public OCSPClientException(String message) {
35+
super(message);
36+
}
37+
38+
public OCSPClientException(Throwable cause) {
39+
super(cause);
40+
}
41+
42+
public OCSPClientException(String message, Throwable cause) {
43+
super(message, cause);
44+
}
45+
46+
public OCSPClientException(String message, byte[] responseBody, int statusCode) {
47+
super(message);
48+
this.responseBody = responseBody;
49+
this.statusCode = statusCode;
50+
}
51+
52+
public byte[] getResponseBody() {
53+
return responseBody;
54+
}
55+
56+
public Integer getStatusCode() {
57+
return statusCode;
58+
}
59+
}

src/main/java/eu/webeid/ocsp/exceptions/UserCertificateOCSPCheckFailedException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
*/
3434
public class UserCertificateOCSPCheckFailedException extends AuthTokenException {
3535

36+
public UserCertificateOCSPCheckFailedException() {
37+
super("User certificate revocation check has failed");
38+
}
39+
3640
public UserCertificateOCSPCheckFailedException(Throwable cause, URI ocspResponderUri) {
3741
super(withResponderUri("User certificate revocation check has failed", ocspResponderUri), cause);
3842
}

src/main/java/eu/webeid/ocsp/exceptions/UserCertificateRevokedException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
*/
3434
public class UserCertificateRevokedException extends AuthTokenException {
3535

36+
public UserCertificateRevokedException() {
37+
super("User certificate has been revoked");
38+
}
39+
3640
public UserCertificateRevokedException(URI ocspResponderUri) {
3741
super(withResponderUri("User certificate has been revoked", ocspResponderUri));
3842
}

src/main/java/eu/webeid/resilientocsp/ResilientOcspCertificateRevocationChecker.java

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424

2525
import eu.webeid.ocsp.OcspCertificateRevocationChecker;
2626
import eu.webeid.ocsp.client.OcspClient;
27-
import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException;
27+
import eu.webeid.ocsp.exceptions.OCSPClientException;
2828
import eu.webeid.ocsp.exceptions.UserCertificateRevokedException;
29-
import eu.webeid.ocsp.exceptions.UserCertificateUnknownException;
3029
import eu.webeid.ocsp.protocol.OcspRequestBuilder;
3130
import eu.webeid.ocsp.service.OcspService;
3231
import eu.webeid.ocsp.service.OcspServiceProvider;
32+
import eu.webeid.resilientocsp.exceptions.ResilientUserCertificateOCSPCheckFailedException;
33+
import eu.webeid.resilientocsp.exceptions.ResilientUserCertificateRevokedException;
3334
import eu.webeid.security.exceptions.AuthTokenException;
35+
import eu.webeid.security.validator.ValidationInfo;
3436
import eu.webeid.security.validator.revocationcheck.RevocationInfo;
3537
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
3638
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
@@ -45,18 +47,17 @@
4547
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
4648
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
4749
import org.bouncycastle.cert.ocsp.CertificateID;
48-
import org.bouncycastle.cert.ocsp.OCSPException;
4950
import org.bouncycastle.cert.ocsp.OCSPReq;
5051
import org.bouncycastle.cert.ocsp.OCSPResp;
51-
import org.bouncycastle.operator.OperatorCreationException;
5252
import org.slf4j.Logger;
5353
import org.slf4j.LoggerFactory;
5454

55-
import java.io.IOException;
5655
import java.net.URI;
5756
import java.security.cert.CertificateException;
5857
import java.security.cert.X509Certificate;
5958
import java.time.Duration;
59+
import java.util.ArrayList;
60+
import java.util.HashMap;
6061
import java.util.List;
6162
import java.util.Map;
6263

@@ -104,14 +105,27 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
104105
try {
105106
ocspService = getOcspServiceProvider().getService(subjectCertificate);
106107
} catch (CertificateException e) {
107-
throw new UserCertificateOCSPCheckFailedException(e, null);
108+
throw new ResilientUserCertificateOCSPCheckFailedException(new ValidationInfo(subjectCertificate, List.of()));
108109
}
109110
final OcspService fallbackOcspService = ocspService.getFallbackService();
110111
if (fallbackOcspService == null) {
111112
return List.of(request(ocspService, subjectCertificate, issuerCertificate, false));
112113
}
113114

114115
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(ocspService.getAccessLocation().toASCIIString());
116+
117+
List<RevocationInfo> revocationInfoList = new ArrayList<>();
118+
circuitBreaker.getEventPublisher().onError(event -> {
119+
Throwable throwable = event.getThrowable();
120+
if (throwable instanceof ResilientUserCertificateOCSPCheckFailedException e) {
121+
revocationInfoList.addAll(e.getValidationInfo().revocationInfoList());
122+
return;
123+
}
124+
revocationInfoList.add(new RevocationInfo(null, Map.ofEntries(
125+
Map.entry(RevocationInfo.KEY_OCSP_ERROR, throwable)
126+
)));
127+
});
128+
115129
CheckedFunction0<RevocationInfo> primarySupplier = () -> request(ocspService, subjectCertificate, issuerCertificate, false);
116130
CheckedFunction0<RevocationInfo> fallbackSupplier = () -> request(ocspService.getFallbackService(), subjectCertificate, issuerCertificate, true);
117131
Decorators.DecorateCheckedSupplier<RevocationInfo> decorateCheckedSupplier = Decorators.ofCheckedSupplier(primarySupplier);
@@ -120,26 +134,40 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
120134
decorateCheckedSupplier.withRetry(retry);
121135
}
122136
decorateCheckedSupplier.withCircuitBreaker(circuitBreaker)
123-
.withFallback(List.of(UserCertificateOCSPCheckFailedException.class, CallNotPermittedException.class, UserCertificateUnknownException.class), e -> fallbackSupplier.apply());
137+
.withFallback(List.of(ResilientUserCertificateOCSPCheckFailedException.class, CallNotPermittedException.class), e -> fallbackSupplier.apply());
124138

125139
CheckedFunction0<RevocationInfo> decoratedSupplier = decorateCheckedSupplier.decorate();
126140

127-
// TODO Collect the intermediate results
128-
return List.of(Try.of(decoratedSupplier).getOrElseThrow(throwable -> {
129-
if (throwable instanceof AuthTokenException) {
130-
return (AuthTokenException) throwable;
141+
Try<RevocationInfo> result = Try.of(decoratedSupplier);
142+
143+
RevocationInfo revocationInfo = result.getOrElseThrow(throwable -> {
144+
if (throwable instanceof ResilientUserCertificateOCSPCheckFailedException exception) {
145+
revocationInfoList.addAll(exception.getValidationInfo().revocationInfoList());
146+
exception.setValidationInfo(new ValidationInfo(subjectCertificate, revocationInfoList));
147+
return exception;
148+
}
149+
if (throwable instanceof ResilientUserCertificateRevokedException exception) {
150+
revocationInfoList.addAll(exception.getValidationInfo().revocationInfoList());
151+
exception.setValidationInfo(new ValidationInfo(subjectCertificate, revocationInfoList));
152+
return exception;
131153
}
132-
return new UserCertificateOCSPCheckFailedException(throwable, null);
133-
}));
154+
// TODO This should always be TaraUserCertificateOCSPCheckFailedException when reached?
155+
return new ResilientUserCertificateOCSPCheckFailedException(new ValidationInfo(subjectCertificate, revocationInfoList));
156+
});
157+
158+
revocationInfoList.add(revocationInfo);
159+
return revocationInfoList;
134160
}
135161

136-
private RevocationInfo request(OcspService ocspService, X509Certificate subjectCertificate, X509Certificate issuerCertificate, boolean allowThisUpdateInPast) throws AuthTokenException {
162+
private RevocationInfo request(OcspService ocspService, X509Certificate subjectCertificate, X509Certificate issuerCertificate, boolean allowThisUpdateInPast) throws ResilientUserCertificateOCSPCheckFailedException, ResilientUserCertificateRevokedException {
137163
URI ocspResponderUri = null;
164+
OCSPResp response = null;
165+
OCSPReq request = null;
138166
try {
139167
ocspResponderUri = requireNonNull(ocspService.getAccessLocation(), "ocspResponderUri");
140168

141169
final CertificateID certificateId = getCertificateId(subjectCertificate, issuerCertificate);
142-
final OCSPReq request = new OcspRequestBuilder()
170+
request = new OcspRequestBuilder()
143171
.withCertificateId(certificateId)
144172
.enableOcspNonce(ocspService.doesSupportNonce())
145173
.build();
@@ -149,14 +177,28 @@ private RevocationInfo request(OcspService ocspService, X509Certificate subjectC
149177
}
150178

151179
LOG.debug("Sending OCSP request");
152-
OCSPResp response = requireNonNull(getOcspClient().request(ocspResponderUri, request)); // TODO: This should trigger fallback?
180+
response = requireNonNull(getOcspClient().request(ocspResponderUri, request)); // TODO: This should trigger fallback?
153181
if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) {
154-
throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus()), ocspResponderUri);
182+
ResilientUserCertificateOCSPCheckFailedException exception = new ResilientUserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus()));
183+
RevocationInfo revocationInfo = new RevocationInfo(ocspService.getAccessLocation(), Map.ofEntries(
184+
Map.entry(RevocationInfo.KEY_OCSP_ERROR, exception),
185+
Map.entry(RevocationInfo.KEY_OCSP_REQUEST, request),
186+
Map.entry(RevocationInfo.KEY_OCSP_RESPONSE, response)
187+
));
188+
exception.setValidationInfo(new ValidationInfo(subjectCertificate, List.of(revocationInfo)));
189+
throw exception;
155190
}
156191

157192
final BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject();
158193
if (basicResponse == null) {
159-
throw new UserCertificateOCSPCheckFailedException("Missing Basic OCSP Response", ocspResponderUri);
194+
ResilientUserCertificateOCSPCheckFailedException exception = new ResilientUserCertificateOCSPCheckFailedException("Missing Basic OCSP Response");
195+
RevocationInfo revocationInfo = new RevocationInfo(ocspService.getAccessLocation(), Map.ofEntries(
196+
Map.entry(RevocationInfo.KEY_OCSP_ERROR, exception),
197+
Map.entry(RevocationInfo.KEY_OCSP_REQUEST, request),
198+
Map.entry(RevocationInfo.KEY_OCSP_RESPONSE, response)
199+
));
200+
exception.setValidationInfo(new ValidationInfo(subjectCertificate, List.of(revocationInfo)));
201+
throw exception;
160202
}
161203
LOG.debug("OCSP response received successfully");
162204

@@ -170,24 +212,44 @@ private RevocationInfo request(OcspService ocspService, X509Certificate subjectC
170212
Map.entry(RevocationInfo.KEY_OCSP_REQUEST, request),
171213
Map.entry(RevocationInfo.KEY_OCSP_RESPONSE, response)
172214
));
173-
} catch (OCSPException | CertificateException | OperatorCreationException | IOException e) {
174-
throw new UserCertificateOCSPCheckFailedException(e, ocspResponderUri);
215+
} catch (UserCertificateRevokedException e) {
216+
RevocationInfo revocationInfo = getRevocationInfo(ocspResponderUri, e, request, response);
217+
throw new ResilientUserCertificateRevokedException(new ValidationInfo(subjectCertificate, List.of(revocationInfo)));
218+
} catch (OCSPClientException e) {
219+
RevocationInfo revocationInfo = getRevocationInfo(ocspResponderUri, e, request, response);
220+
revocationInfo.ocspResponseAttributes().put(RevocationInfo.KEY_OCSP_RESPONSE, e.getResponseBody());
221+
revocationInfo.ocspResponseAttributes().put(RevocationInfo.KEY_HTTP_STATUS_CODE, e.getStatusCode());
222+
throw new ResilientUserCertificateOCSPCheckFailedException(new ValidationInfo(subjectCertificate, List.of(revocationInfo)));
223+
} catch (Exception e) {
224+
RevocationInfo revocationInfo = getRevocationInfo(ocspResponderUri, e, request, response);
225+
throw new ResilientUserCertificateOCSPCheckFailedException(new ValidationInfo(subjectCertificate, List.of(revocationInfo)));
226+
}
227+
}
228+
229+
private RevocationInfo getRevocationInfo(URI ocspResponderUri, Exception e, OCSPReq request, OCSPResp response) {
230+
RevocationInfo revocationInfo = new RevocationInfo(ocspResponderUri, new HashMap<>(Map.of(RevocationInfo.KEY_OCSP_ERROR, e)));
231+
if (request != null) {
232+
revocationInfo.ocspResponseAttributes().put(RevocationInfo.KEY_OCSP_REQUEST, request);
233+
}
234+
if (response != null) {
235+
revocationInfo.ocspResponseAttributes().put(RevocationInfo.KEY_OCSP_RESPONSE, response);
175236
}
237+
return revocationInfo;
176238
}
177239

178240
private static CircuitBreakerConfig getCircuitBreakerConfig(CircuitBreakerConfig circuitBreakerConfig) {
179241
return CircuitBreakerConfig.from(circuitBreakerConfig)
180242
// Users must not be able to modify these three values.
181243
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
182-
.ignoreExceptions(UserCertificateRevokedException.class)
244+
.ignoreExceptions(ResilientUserCertificateRevokedException.class)
183245
.automaticTransitionFromOpenToHalfOpenEnabled(true)
184246
.build();
185247
}
186248

187249
private static RetryConfig getRetryConfigConfig(RetryConfig retryConfig) {
188250
return RetryConfig.from(retryConfig)
189251
// Users must not be able to modify this value.
190-
.ignoreExceptions(UserCertificateRevokedException.class)
252+
.ignoreExceptions(ResilientUserCertificateRevokedException.class)
191253
.build();
192254
}
193255
}

0 commit comments

Comments
 (0)