Skip to content

Commit 5798ab9

Browse files
aarmammadislm
authored andcommitted
Add resilient OCSP certificate revocation checker
1 parent f55d636 commit 5798ab9

21 files changed

Lines changed: 772 additions & 34 deletions

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: 15 additions & 8 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);
@@ -131,7 +131,7 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
131131
}
132132
LOG.debug("OCSP response received successfully");
133133

134-
verifyOcspResponse(basicResponse, ocspService, certificateId);
134+
verifyOcspResponse(basicResponse, ocspService, certificateId, false, false);
135135
if (ocspService.doesSupportNonce()) {
136136
checkNonce(request, basicResponse, ocspResponderUri);
137137
}
@@ -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, boolean rejectUnknownOcspResponseStatus, boolean allowThisUpdateInPast) 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
@@ -195,14 +195,14 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer
195195
// be available about the status of the certificate (nextUpdate) is
196196
// greater than the current time.
197197

198-
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, ocspService.getAccessLocation());
198+
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, ocspService.getAccessLocation(), allowThisUpdateInPast);
199199

200200
// Now we can accept the signed response as valid and validate the certificate status.
201-
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, ocspService.getAccessLocation());
201+
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, ocspService.getAccessLocation(), rejectUnknownOcspResponseStatus);
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/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(appendResponderUri("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(appendResponderUri("User certificate has been revoked", ocspResponderUri));
3842
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
import eu.webeid.security.exceptions.AuthTokenException;
26+
27+
import java.net.URI;
28+
29+
import static eu.webeid.ocsp.exceptions.OcspResponderUriMessageAppender.appendResponderUri;
30+
31+
public class UserCertificateUnknownException extends AuthTokenException {
32+
33+
public UserCertificateUnknownException(String msg, URI ocspResponderUri) {
34+
super(appendResponderUri("User certificate status is unknown: " + msg, ocspResponderUri));
35+
}
36+
}

src/main/java/eu/webeid/ocsp/protocol/OcspResponseValidator.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import eu.webeid.ocsp.exceptions.OCSPCertificateException;
2626
import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException;
2727
import eu.webeid.ocsp.exceptions.UserCertificateRevokedException;
28+
import eu.webeid.ocsp.exceptions.UserCertificateUnknownException;
29+
import eu.webeid.security.exceptions.AuthTokenException;
2830
import eu.webeid.security.util.DateAndTime;
2931
import org.bouncycastle.cert.X509CertificateHolder;
3032
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
@@ -77,7 +79,7 @@ public static void validateResponseSignature(BasicOCSPResp basicResponse, X509Ce
7779
}
7880
}
7981

80-
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
82+
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge, URI ocspResponderUri, boolean allowThisUpdateInPast) throws UserCertificateOCSPCheckFailedException {
8183
// From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt:
8284
// 4.2.2. Notes on OCSP Responses
8385
// 4.2.2.1. Time
@@ -98,7 +100,7 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
98100
"thisUpdate '" + thisUpdate + "' is too far in the future, " +
99101
"latest allowed: '" + latestAcceptableTimeSkew + "'", ocspResponderUri);
100102
}
101-
if (thisUpdate.isBefore(minimumValidThisUpdateTime)) {
103+
if (!allowThisUpdateInPast && thisUpdate.isBefore(minimumValidThisUpdateTime)) {
102104
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
103105
"thisUpdate '" + thisUpdate + "' is too old, " +
104106
"minimum time allowed: '" + minimumValidThisUpdateTime + "'", ocspResponderUri);
@@ -118,7 +120,7 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
118120
}
119121
}
120122

121-
public static void validateSubjectCertificateStatus(SingleResp certStatusResponse, URI ocspResponderUri) throws UserCertificateRevokedException {
123+
public static void validateSubjectCertificateStatus(SingleResp certStatusResponse, URI ocspResponderUri, boolean rejectUnknownOcspResponseStatus) throws AuthTokenException {
122124
final CertificateStatus status = certStatusResponse.getCertStatus();
123125
if (status == null) {
124126
return;
@@ -128,9 +130,11 @@ public static void validateSubjectCertificateStatus(SingleResp certStatusRespons
128130
new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason(), ocspResponderUri) :
129131
new UserCertificateRevokedException(ocspResponderUri));
130132
} else if (status instanceof UnknownStatus) {
131-
throw new UserCertificateRevokedException("Unknown status", ocspResponderUri);
133+
throw rejectUnknownOcspResponseStatus ? new UserCertificateUnknownException("Unknown status", ocspResponderUri)
134+
: new UserCertificateRevokedException("Unknown status", ocspResponderUri);
132135
} else {
133-
throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown", ocspResponderUri);
136+
throw rejectUnknownOcspResponseStatus ? new UserCertificateUnknownException("Status is neither good, revoked nor unknown", ocspResponderUri)
137+
: new UserCertificateRevokedException("Status is neither good, revoked nor unknown", ocspResponderUri);
134138
}
135139
}
136140

0 commit comments

Comments
 (0)