Skip to content

Commit b4d8d06

Browse files
committed
Add resilient OCSP certificate revocation checker
1 parent 5798ab9 commit b4d8d06

22 files changed

Lines changed: 904 additions & 248 deletions

pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +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>
19+
<resilience4j.version>2.3.0</resilience4j.version>
2020
<junit-jupiter.version>5.13.3</junit-jupiter.version>
2121
<assertj.version>3.27.3</assertj.version>
2222
<mockito.version>5.18.0</mockito.version>
@@ -89,6 +89,11 @@
8989
</exclusion>
9090
</exclusions>
9191
</dependency>
92+
<dependency>
93+
<groupId>io.github.resilience4j</groupId>
94+
<artifactId>resilience4j-vavr</artifactId>
95+
<version>${resilience4j.version}</version>
96+
</dependency>
9297

9398
<dependency>
9499
<groupId>org.junit.jupiter</groupId>

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
package eu.webeid.ocsp;
2424

2525
import eu.webeid.ocsp.client.OcspClient;
26+
import eu.webeid.ocsp.exceptions.OCSPClientException;
2627
import eu.webeid.ocsp.protocol.DigestCalculatorImpl;
2728
import eu.webeid.ocsp.protocol.OcspRequestBuilder;
2829
import eu.webeid.ocsp.protocol.OcspResponseValidator;
@@ -131,20 +132,20 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
131132
}
132133
LOG.debug("OCSP response received successfully");
133134

134-
verifyOcspResponse(basicResponse, ocspService, certificateId, false, false);
135+
verifyOcspResponse(basicResponse, ocspService, certificateId, false, maxOcspResponseThisUpdateAge);
135136
if (ocspService.doesSupportNonce()) {
136137
checkNonce(request, basicResponse, ocspResponderUri);
137138
}
138139
LOG.debug("OCSP response verified successfully");
139140

140141
return List.of(new RevocationInfo(ocspResponderUri, Map.of(RevocationInfo.KEY_OCSP_RESPONSE, response)));
141142

142-
} catch (OCSPException | CertificateException | OperatorCreationException | IOException e) {
143+
} catch (OCSPException | CertificateException | OperatorCreationException | IOException | OCSPClientException e) {
143144
throw new UserCertificateOCSPCheckFailedException(e, ocspResponderUri);
144145
}
145146
}
146147

147-
protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId, boolean rejectUnknownOcspResponseStatus, boolean allowThisUpdateInPast) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
148+
protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId, boolean rejectUnknownOcspResponseStatus, Duration maxOcspResponseThisUpdateAge) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
148149
// The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt.
149150
//
150151
// 3.2. Signed Response Acceptance Requirements
@@ -195,7 +196,7 @@ protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspS
195196
// be available about the status of the certificate (nextUpdate) is
196197
// greater than the current time.
197198

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

200201
// Now we can accept the signed response as valid and validate the certificate status.
201202
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, ocspService.getAccessLocation(), rejectUnknownOcspResponseStatus);
@@ -240,4 +241,8 @@ protected OcspClient getOcspClient() {
240241
protected OcspServiceProvider getOcspServiceProvider() {
241242
return ocspServiceProvider;
242243
}
244+
245+
protected Duration getMaxOcspResponseThisUpdateAge() {
246+
return maxOcspResponseThisUpdateAge;
247+
}
243248
}

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,34 @@
2222

2323
package eu.webeid.ocsp.exceptions;
2424

25-
public class OCSPClientException extends RuntimeException {
25+
public class OCSPClientException extends Exception {
2626

27-
private byte[] responseBody;
27+
private final byte[] responseBody;
2828

29-
private Integer statusCode;
29+
private final Integer statusCode;
3030

3131
public OCSPClientException() {
32+
this(null, null);
3233
}
3334

3435
public OCSPClientException(String message) {
35-
super(message);
36+
this(message, null, null);
3637
}
3738

3839
public OCSPClientException(Throwable cause) {
39-
super(cause);
40+
this(null, cause, null, null);
4041
}
4142

4243
public OCSPClientException(String message, Throwable cause) {
43-
super(message, cause);
44+
this(message, cause, null, null);
45+
}
46+
47+
public OCSPClientException(String message, byte[] responseBody, Integer statusCode) {
48+
this(message, null, responseBody, statusCode);
4449
}
4550

46-
public OCSPClientException(String message, byte[] responseBody, int statusCode) {
47-
super(message);
51+
public OCSPClientException(String message, Throwable cause, byte[] responseBody, Integer statusCode) {
52+
super(message, cause);
4853
this.responseBody = responseBody;
4954
this.statusCode = statusCode;
5055
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package eu.webeid.ocsp.protocol;
2+
3+
import org.bouncycastle.asn1.x500.X500Name;
4+
5+
import java.security.cert.X509Certificate;
6+
import java.util.Objects;
7+
8+
public class IssuerDistinguishedName {
9+
10+
public static X500Name getIssuerDistinguishedName(X509Certificate certificate) {
11+
Objects.requireNonNull(certificate, "certificate");
12+
return X500Name.getInstance(certificate.getIssuerX500Principal().getEncoded());
13+
}
14+
15+
private IssuerDistinguishedName() {
16+
throw new IllegalStateException("Utility class");
17+
}
18+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public static void validateResponseSignature(BasicOCSPResp basicResponse, X509Ce
7979
}
8080
}
8181

82-
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge, URI ocspResponderUri, boolean allowThisUpdateInPast) throws UserCertificateOCSPCheckFailedException {
82+
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
8383
// From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt:
8484
// 4.2.2. Notes on OCSP Responses
8585
// 4.2.2.1. Time
@@ -100,7 +100,7 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
100100
"thisUpdate '" + thisUpdate + "' is too far in the future, " +
101101
"latest allowed: '" + latestAcceptableTimeSkew + "'", ocspResponderUri);
102102
}
103-
if (!allowThisUpdateInPast && thisUpdate.isBefore(minimumValidThisUpdateTime)) {
103+
if (thisUpdate.isBefore(minimumValidThisUpdateTime)) {
104104
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
105105
"thisUpdate '" + thisUpdate + "' is too old, " +
106106
"minimum time allowed: '" + minimumValidThisUpdateTime + "'", ocspResponderUri);

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

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

2323
package eu.webeid.ocsp.service;
2424

25-
import eu.webeid.resilientocsp.service.FallbackOcspService;
2625
import eu.webeid.security.certificate.CertificateValidator;
2726
import eu.webeid.security.exceptions.AuthTokenException;
2827
import eu.webeid.ocsp.exceptions.OCSPCertificateException;
2928
import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException;
3029
import eu.webeid.ocsp.protocol.OcspResponseValidator;
3130
import eu.webeid.security.validator.revocationcheck.RevocationMode;
31+
import org.bouncycastle.asn1.x500.X500Name;
3232
import org.bouncycastle.cert.X509CertificateHolder;
3333
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
3434

@@ -39,8 +39,10 @@
3939
import java.security.cert.X509Certificate;
4040
import java.util.Date;
4141
import java.util.Objects;
42+
import java.util.Optional;
4243
import java.util.Set;
4344

45+
import static eu.webeid.ocsp.protocol.IssuerDistinguishedName.getIssuerDistinguishedName;
4446
import static eu.webeid.ocsp.protocol.OcspUrl.getOcspUri;
4547

4648
/**
@@ -60,8 +62,9 @@ public AiaOcspService(AiaOcspServiceConfiguration configuration, X509Certificate
6062
this.trustedCACertificateAnchors = configuration.getTrustedCACertificateAnchors();
6163
this.trustedCACertificateCertStore = configuration.getTrustedCACertificateCertStore();
6264
this.url = getOcspAiaUrlFromCertificate(Objects.requireNonNull(certificate));
63-
this.supportsNonce = !configuration.getNonceDisabledOcspUrls().contains(this.url);
6465
this.fallbackOcspService = fallbackOcspService;
66+
X500Name issuerDN = getIssuerDistinguishedName(certificate);
67+
this.supportsNonce = !configuration.getNonceDisabledIssuerDNs().contains(issuerDN);
6568
}
6669

6770
@Override
@@ -75,8 +78,8 @@ public URI getAccessLocation() {
7578
}
7679

7780
@Override
78-
public FallbackOcspService getFallbackService() {
79-
return fallbackOcspService;
81+
public Optional<FallbackOcspService> getFallbackService() {
82+
return Optional.ofNullable(fallbackOcspService);
8083
}
8184

8285
@Override

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

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

2323
package eu.webeid.ocsp.service;
2424

25-
import java.net.URI;
25+
import org.bouncycastle.asn1.x500.X500Name;
26+
2627
import java.security.cert.CertStore;
2728
import java.security.cert.TrustAnchor;
2829
import java.util.Collection;
@@ -31,18 +32,18 @@
3132

3233
public class AiaOcspServiceConfiguration {
3334

34-
private final Collection<URI> nonceDisabledOcspUrls;
35+
private final Collection<X500Name> nonceDisabledIssuerDNs;
3536
private final Set<TrustAnchor> trustedCACertificateAnchors;
3637
private final CertStore trustedCACertificateCertStore;
3738

38-
public AiaOcspServiceConfiguration(Collection<URI> nonceDisabledOcspUrls, Set<TrustAnchor> trustedCACertificateAnchors, CertStore trustedCACertificateCertStore) {
39-
this.nonceDisabledOcspUrls = Objects.requireNonNull(nonceDisabledOcspUrls);
39+
public AiaOcspServiceConfiguration(Collection<X500Name> nonceDisabledIssuerDNs, Set<TrustAnchor> trustedCACertificateAnchors, CertStore trustedCACertificateCertStore) {
40+
this.nonceDisabledIssuerDNs = Objects.requireNonNull(nonceDisabledIssuerDNs);
4041
this.trustedCACertificateAnchors = Objects.requireNonNull(trustedCACertificateAnchors);
4142
this.trustedCACertificateCertStore = Objects.requireNonNull(trustedCACertificateCertStore);
4243
}
4344

44-
public Collection<URI> getNonceDisabledOcspUrls() {
45-
return nonceDisabledOcspUrls;
45+
public Collection<X500Name> getNonceDisabledIssuerDNs() {
46+
return nonceDisabledIssuerDNs;
4647
}
4748

4849
public Set<TrustAnchor> getTrustedCACertificateAnchors() {

src/main/java/eu/webeid/resilientocsp/service/FallbackOcspService.java renamed to src/main/java/eu/webeid/ocsp/service/FallbackOcspService.java

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,23 @@
2020
* SOFTWARE.
2121
*/
2222

23-
package eu.webeid.resilientocsp.service;
23+
package eu.webeid.ocsp.service;
2424

2525
import eu.webeid.ocsp.exceptions.OCSPCertificateException;
26-
import eu.webeid.ocsp.service.OcspService;
26+
import eu.webeid.ocsp.protocol.OcspResponseValidator;
27+
import eu.webeid.security.certificate.CertificateValidator;
2728
import eu.webeid.security.exceptions.AuthTokenException;
29+
import eu.webeid.security.validator.revocationcheck.RevocationMode;
2830
import org.bouncycastle.cert.X509CertificateHolder;
2931
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
3032

3133
import java.net.URI;
34+
import java.security.cert.CertStore;
3235
import java.security.cert.CertificateException;
36+
import java.security.cert.TrustAnchor;
3337
import java.security.cert.X509Certificate;
3438
import java.util.Date;
39+
import java.util.Set;
3540

3641

3742
import static eu.webeid.security.certificate.CertificateValidator.requireCertificateIsValidOnDate;
@@ -42,11 +47,19 @@ public class FallbackOcspService implements OcspService {
4247
private final URI url;
4348
private final boolean supportsNonce;
4449
private final X509Certificate trustedResponderCertificate;
50+
private final FallbackOcspService nextFallback;
51+
private final Set<TrustAnchor> trustedCACertificateAnchors;
52+
private final CertStore trustedCACertificateCertStore;
4553

4654
public FallbackOcspService(FallbackOcspServiceConfiguration configuration) {
47-
this.url = configuration.getFallbackOcspServiceAccessLocation();
55+
this.url = configuration.getAccessLocation();
4856
this.supportsNonce = configuration.doesSupportNonce();
4957
this.trustedResponderCertificate = configuration.getResponderCertificate();
58+
this.nextFallback = configuration.getNextFallbackConfiguration() != null
59+
? new FallbackOcspService(configuration.getNextFallbackConfiguration())
60+
: null;
61+
this.trustedCACertificateAnchors = configuration.getTrustedCACertificateAnchors();
62+
this.trustedCACertificateCertStore = configuration.getTrustedCACertificateCertStore();
5063
}
5164

5265
@Override
@@ -63,15 +76,40 @@ public URI getAccessLocation() {
6376
public void validateResponderCertificate(X509CertificateHolder cert, Date now) throws AuthTokenException {
6477
try {
6578
final X509Certificate responderCertificate = certificateConverter.getCertificate(cert);
66-
// Certificate pinning is implemented simply by comparing the certificates or their public keys,
67-
// see https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning.
68-
if (!trustedResponderCertificate.equals(responderCertificate)) {
69-
throw new OCSPCertificateException("Responder certificate from the OCSP response is not equal to " +
70-
"the configured fallback OCSP responder certificate");
71-
}
7279
requireCertificateIsValidOnDate(responderCertificate, now, "Fallback OCSP responder");
80+
if (trustedResponderCertificate != null) {
81+
validatePinnedResponderCertificate(responderCertificate);
82+
} else {
83+
validateResponderCertificateAgainstTrustedCa(responderCertificate, now);
84+
}
7385
} catch (CertificateException e) {
7486
throw new OCSPCertificateException("X509CertificateHolder conversion to X509Certificate failed", e);
7587
}
7688
}
89+
90+
private void validatePinnedResponderCertificate(X509Certificate responderCertificate) throws OCSPCertificateException {
91+
// Certificate pinning is implemented simply by comparing the certificates or their public keys,
92+
// see https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning.
93+
if (!trustedResponderCertificate.equals(responderCertificate)) {
94+
throw new OCSPCertificateException("Responder certificate from the OCSP response is not equal to " +
95+
"the configured fallback OCSP responder certificate");
96+
}
97+
}
98+
99+
private void validateResponderCertificateAgainstTrustedCa(X509Certificate responderCertificate, Date now) throws AuthTokenException {
100+
OcspResponseValidator.validateHasSigningExtension(responderCertificate);
101+
CertificateValidator.validateCertificateTrustAndRevocation(
102+
responderCertificate,
103+
trustedCACertificateAnchors,
104+
trustedCACertificateCertStore,
105+
now,
106+
RevocationMode.DISABLED,
107+
null,
108+
null
109+
);
110+
}
111+
112+
public FallbackOcspService getNextFallback() {
113+
return nextFallback;
114+
}
77115
}

0 commit comments

Comments
 (0)