Skip to content

Commit 7b8f2a0

Browse files
mrtssvenzik
andcommitted
Use plaform OCSP implementation by default, move custom OCSP implementation to eu.webeid.ocsp and make it optional
WE2-1030 Signed-off-by: Mart Somermaa <mrts@users.noreply.github.com> Co-authored-by: Sven Mitt <svenzik@users.noreply.github.com>
1 parent 61f23cd commit 7b8f2a0

53 files changed

Lines changed: 1219 additions & 715 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/coverity-analysis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- uses: actions/setup-java@v4
2424
with:
2525
distribution: zulu
26-
java-version: 11
26+
java-version: 17
2727

2828
- name: Cache Maven packages
2929
uses: actions/cache@v4

.github/workflows/maven-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- uses: actions/setup-java@v4
2424
with:
2525
distribution: zulu
26-
java-version: 11
26+
java-version: 17
2727

2828
- name: Cache Maven packages
2929
uses: actions/cache@v4

.github/workflows/maven-deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- uses: actions/setup-java@v4
1818
with:
1919
distribution: zulu
20-
java-version: 11
20+
java-version: 17
2121

2222
- name: Cache Maven packages
2323
uses: actions/cache@v4

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
<modelVersion>4.0.0</modelVersion>
88
<artifactId>authtoken-validation</artifactId>
99
<groupId>eu.webeid.security</groupId>
10-
<version>3.2.1</version>
10+
<version>4.0.0-SNAPSHOT</version>
1111
<packaging>jar</packaging>
1212
<name>authtoken-validation</name>
1313
<description>Web eID authentication token validation library for Java</description>
1414

1515
<properties>
16-
<java.version>11</java.version>
16+
<java.version>17</java.version>
1717
<jjwt.version>0.13.0</jjwt.version>
1818
<bouncycastle.version>1.84</bouncycastle.version>
1919
<jackson.version>2.22.1</jackson.version>

src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidator.java renamed to src/main/java/eu/webeid/ocsp/OcspCertificateRevocationChecker.java

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
// SPDX-FileCopyrightText: Estonian Information System Authority
22
// SPDX-License-Identifier: MIT
33

4-
package eu.webeid.security.validator.certvalidators;
4+
package eu.webeid.ocsp;
55

6+
import eu.webeid.ocsp.client.OcspClient;
7+
import eu.webeid.ocsp.protocol.DigestCalculatorImpl;
8+
import eu.webeid.ocsp.protocol.OcspRequestBuilder;
9+
import eu.webeid.ocsp.protocol.OcspResponseValidator;
610
import eu.webeid.security.exceptions.AuthTokenException;
7-
import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
11+
import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException;
812
import eu.webeid.security.util.DateAndTime;
9-
import eu.webeid.security.validator.ocsp.DigestCalculatorImpl;
10-
import eu.webeid.security.validator.ocsp.OcspClient;
11-
import eu.webeid.security.validator.ocsp.OcspRequestBuilder;
12-
import eu.webeid.security.validator.ocsp.OcspResponseValidator;
13-
import eu.webeid.security.validator.ocsp.OcspServiceProvider;
14-
import eu.webeid.security.validator.ocsp.service.OcspService;
13+
import eu.webeid.ocsp.service.OcspServiceProvider;
14+
import eu.webeid.ocsp.service.OcspService;
15+
import eu.webeid.security.validator.revocationcheck.CertificateRevocationChecker;
16+
import eu.webeid.security.validator.revocationcheck.RevocationInfo;
1517
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
1618
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
1719
import org.bouncycastle.asn1.x509.Extension;
@@ -30,52 +32,64 @@
3032

3133
import java.io.IOException;
3234
import java.math.BigInteger;
35+
import java.net.URI;
3336
import java.security.Security;
3437
import java.security.cert.CertificateEncodingException;
3538
import java.security.cert.CertificateException;
3639
import java.security.cert.X509Certificate;
3740
import java.time.Duration;
3841
import java.util.Date;
39-
import java.util.Objects;
42+
import java.util.List;
43+
import java.util.Map;
4044

41-
public final class SubjectCertificateNotRevokedValidator {
45+
import static eu.webeid.security.util.DateAndTime.requirePositiveDuration;
46+
import static java.util.Objects.requireNonNull;
4247

43-
private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateNotRevokedValidator.class);
48+
public final class OcspCertificateRevocationChecker implements CertificateRevocationChecker {
49+
50+
public static final Duration DEFAULT_TIME_SKEW = Duration.ofMinutes(15);
51+
public static final Duration DEFAULT_THIS_UPDATE_AGE = Duration.ofMinutes(2);
52+
53+
private static final Logger LOG = LoggerFactory.getLogger(OcspCertificateRevocationChecker.class);
4454

45-
private final SubjectCertificateTrustedValidator trustValidator;
4655
private final OcspClient ocspClient;
4756
private final OcspServiceProvider ocspServiceProvider;
4857
private final Duration allowedOcspResponseTimeSkew;
4958
private final Duration maxOcspResponseThisUpdateAge;
5059

5160
static {
52-
Security.addProvider(new BouncyCastleProvider());
61+
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
62+
Security.addProvider(new BouncyCastleProvider());
63+
}
5364
}
5465

55-
public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator,
56-
OcspClient ocspClient,
57-
OcspServiceProvider ocspServiceProvider,
58-
Duration allowedOcspResponseTimeSkew,
59-
Duration maxOcspResponseThisUpdateAge) {
60-
this.trustValidator = trustValidator;
61-
this.ocspClient = ocspClient;
62-
this.ocspServiceProvider = ocspServiceProvider;
63-
this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew;
64-
this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge;
66+
public OcspCertificateRevocationChecker(OcspClient ocspClient,
67+
OcspServiceProvider ocspServiceProvider,
68+
Duration allowedOcspResponseTimeSkew,
69+
Duration maxOcspResponseThisUpdateAge) {
70+
this.ocspClient = requireNonNull(ocspClient, "ocspClient");
71+
this.ocspServiceProvider = requireNonNull(ocspServiceProvider, "ocspServiceProvider");
72+
this.allowedOcspResponseTimeSkew = requirePositiveDuration(allowedOcspResponseTimeSkew, "allowedOcspResponseTimeSkew");
73+
this.maxOcspResponseThisUpdateAge = requirePositiveDuration(maxOcspResponseThisUpdateAge, "maxOcspResponseThisUpdateAge");
6574
}
6675

6776
/**
68-
* Validates that the user certificate from the authentication token is not revoked with OCSP.
77+
* Validates with OCSP that the user certificate from the authentication token is not revoked.
6978
*
7079
* @param subjectCertificate user certificate to be validated
7180
* @throws AuthTokenException when user certificate is revoked or revocation check fails.
7281
*/
73-
public void validateCertificateNotRevoked(X509Certificate subjectCertificate) throws AuthTokenException {
82+
@Override
83+
public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws AuthTokenException {
84+
requireNonNull(subjectCertificate, "subjectCertificate");
85+
requireNonNull(issuerCertificate, "issuerCertificate");
86+
87+
URI ocspResponderUri = null;
7488
try {
7589
OcspService ocspService = ocspServiceProvider.getService(subjectCertificate);
90+
ocspResponderUri = requireNonNull(ocspService.getAccessLocation(), "ocspResponderUri");
7691

77-
final CertificateID certificateId = getCertificateId(subjectCertificate,
78-
Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerCertificate()));
92+
final CertificateID certificateId = getCertificateId(subjectCertificate, issuerCertificate);
7993

8094
final OCSPReq request = new OcspRequestBuilder()
8195
.withCertificateId(certificateId)
@@ -87,21 +101,27 @@ public void validateCertificateNotRevoked(X509Certificate subjectCertificate) th
87101
}
88102

89103
LOG.debug("Sending OCSP request");
90-
final OCSPResp response = Objects.requireNonNull(ocspClient.request(ocspService.getAccessLocation(), request));
104+
final OCSPResp response = requireNonNull(ocspClient.request(ocspResponderUri, request), "OCSPResp");
91105
if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) {
92-
throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus()));
106+
throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus()), ocspResponderUri);
93107
}
94108

95109
final BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject();
96110
if (basicResponse == null) {
97-
throw new UserCertificateOCSPCheckFailedException("Missing Basic OCSP Response");
111+
throw new UserCertificateOCSPCheckFailedException("Missing Basic OCSP Response", ocspResponderUri);
98112
}
113+
LOG.debug("OCSP response received successfully");
114+
99115
verifyOcspResponse(basicResponse, ocspService, certificateId);
100116
if (ocspService.doesSupportNonce()) {
101-
checkNonce(request, basicResponse);
117+
checkNonce(request, basicResponse, ocspResponderUri);
102118
}
119+
LOG.debug("OCSP response verified successfully");
120+
121+
return List.of(new RevocationInfo(ocspResponderUri, Map.of(RevocationInfo.KEY_OCSP_RESPONSE, response)));
122+
103123
} catch (OCSPException | CertificateException | OperatorCreationException | IOException e) {
104-
throw new UserCertificateOCSPCheckFailedException(e);
124+
throw new UserCertificateOCSPCheckFailedException(e, ocspResponderUri);
105125
}
106126
}
107127

@@ -118,11 +138,12 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer
118138
// As we sent the request for only a single certificate, we expect only a single response.
119139
if (basicResponse.getResponses().length != 1) {
120140
throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one response, "
121-
+ "received " + basicResponse.getResponses().length + " responses instead");
141+
+ "received " + basicResponse.getResponses().length + " responses instead", ocspService.getAccessLocation());
122142
}
123143
final SingleResp certStatusResponse = basicResponse.getResponses()[0];
124144
if (!requestCertificateId.equals(certStatusResponse.getCertID())) {
125-
throw new UserCertificateOCSPCheckFailedException("OCSP responded with certificate ID that differs from the requested ID");
145+
throw new UserCertificateOCSPCheckFailedException("OCSP responded with certificate ID that differs from the requested ID",
146+
ocspService.getAccessLocation());
126147
}
127148

128149
// 2. The signature on the response is valid.
@@ -132,11 +153,11 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer
132153
// is standard practice.
133154
if (basicResponse.getCerts().length < 1) {
134155
throw new UserCertificateOCSPCheckFailedException("OCSP response must contain the responder certificate, "
135-
+ "but none was provided");
156+
+ "but none was provided", ocspService.getAccessLocation());
136157
}
137158
// The first certificate is the responder certificate, other certificates, if given, are the certificate's chain.
138159
final X509CertificateHolder responderCert = basicResponse.getCerts()[0];
139-
OcspResponseValidator.validateResponseSignature(basicResponse, responderCert);
160+
OcspResponseValidator.validateResponseSignature(basicResponse, responderCert, ocspService.getAccessLocation());
140161

141162
// 3. The identity of the signer matches the intended recipient of the
142163
// request.
@@ -155,23 +176,23 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer
155176
// be available about the status of the certificate (nextUpdate) is
156177
// greater than the current time.
157178

158-
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge);
179+
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, ocspService.getAccessLocation());
159180

160181
// Now we can accept the signed response as valid and validate the certificate status.
161-
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse);
182+
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, ocspService.getAccessLocation());
162183
LOG.debug("OCSP check result is GOOD");
163184
}
164185

165-
private static void checkNonce(OCSPReq request, BasicOCSPResp response) throws UserCertificateOCSPCheckFailedException {
186+
private static void checkNonce(OCSPReq request, BasicOCSPResp response, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
166187
final Extension requestNonce = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
167188
final Extension responseNonce = response.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
168189
if (requestNonce == null || responseNonce == null) {
169190
throw new UserCertificateOCSPCheckFailedException("OCSP request or response nonce extension missing, " +
170-
"possible replay attack");
191+
"possible replay attack", ocspResponderUri);
171192
}
172193
if (!requestNonce.equals(responseNonce)) {
173194
throw new UserCertificateOCSPCheckFailedException("OCSP request and response nonces differ, " +
174-
"possible replay attack");
195+
"possible replay attack", ocspResponderUri);
175196
}
176197
}
177198

@@ -183,20 +204,14 @@ private static CertificateID getCertificateId(X509Certificate subjectCertificate
183204
}
184205

185206
private static String ocspStatusToString(int status) {
186-
switch (status) {
187-
case OCSPResp.MALFORMED_REQUEST:
188-
return "malformed request";
189-
case OCSPResp.INTERNAL_ERROR:
190-
return "internal error";
191-
case OCSPResp.TRY_LATER:
192-
return "service unavailable";
193-
case OCSPResp.SIG_REQUIRED:
194-
return "request signature missing";
195-
case OCSPResp.UNAUTHORIZED:
196-
return "unauthorized";
197-
default:
198-
return "unknown";
199-
}
207+
return switch (status) {
208+
case OCSPResp.MALFORMED_REQUEST -> "malformed request";
209+
case OCSPResp.INTERNAL_ERROR -> "internal error";
210+
case OCSPResp.TRY_LATER -> "service unavailable";
211+
case OCSPResp.SIG_REQUIRED -> "request signature missing";
212+
case OCSPResp.UNAUTHORIZED -> "unauthorized";
213+
default -> "unknown";
214+
};
200215
}
201216

202217
}

src/main/java/eu/webeid/security/validator/ocsp/OcspClient.java renamed to src/main/java/eu/webeid/ocsp/client/OcspClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-FileCopyrightText: Estonian Information System Authority
22
// SPDX-License-Identifier: MIT
33

4-
package eu.webeid.security.validator.ocsp;
4+
package eu.webeid.ocsp.client;
55

66
import org.bouncycastle.cert.ocsp.OCSPReq;
77
import org.bouncycastle.cert.ocsp.OCSPResp;

src/main/java/eu/webeid/security/validator/ocsp/OcspClientImpl.java renamed to src/main/java/eu/webeid/ocsp/client/OcspClientImpl.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-FileCopyrightText: Estonian Information System Authority
22
// SPDX-License-Identifier: MIT
33

4-
package eu.webeid.security.validator.ocsp;
4+
package eu.webeid.ocsp.client;
55

66
import org.bouncycastle.cert.ocsp.OCSPReq;
77
import org.bouncycastle.cert.ocsp.OCSPResp;
@@ -15,6 +15,9 @@
1515
import java.net.http.HttpResponse;
1616
import java.time.Duration;
1717

18+
import static eu.webeid.security.util.DateAndTime.requirePositiveDuration;
19+
import static java.util.Objects.requireNonNull;
20+
1821
public class OcspClientImpl implements OcspClient {
1922

2023
private static final Logger LOG = LoggerFactory.getLogger(OcspClientImpl.class);
@@ -26,6 +29,7 @@ public class OcspClientImpl implements OcspClient {
2629
private final Duration ocspRequestTimeout;
2730

2831
public static OcspClient build(Duration ocspRequestTimeout) {
32+
requirePositiveDuration(ocspRequestTimeout, "ocspRequestTimeout");
2933
return new OcspClientImpl(
3034
HttpClient.newBuilder()
3135
.connectTimeout(ocspRequestTimeout)
@@ -72,8 +76,8 @@ public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException {
7276
}
7377

7478
public OcspClientImpl(HttpClient httpClient, Duration ocspRequestTimeout) {
75-
this.httpClient = httpClient;
76-
this.ocspRequestTimeout = ocspRequestTimeout;
79+
this.httpClient = requireNonNull(httpClient, "httpClient");
80+
this.ocspRequestTimeout = requirePositiveDuration(ocspRequestTimeout, "ocspRequestTimeout");
7781
}
7882

7983
}

src/main/java/eu/webeid/security/exceptions/OCSPCertificateException.java renamed to src/main/java/eu/webeid/ocsp/exceptions/OCSPCertificateException.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// SPDX-FileCopyrightText: Estonian Information System Authority
22
// SPDX-License-Identifier: MIT
33

4-
package eu.webeid.security.exceptions;
4+
package eu.webeid.ocsp.exceptions;
5+
6+
import eu.webeid.security.exceptions.AuthTokenException;
57

68
public class OCSPCertificateException extends AuthTokenException {
79

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-FileCopyrightText: Estonian Information System Authority
2+
// SPDX-License-Identifier: MIT
3+
4+
package eu.webeid.ocsp.exceptions;
5+
6+
import java.net.URI;
7+
8+
/**
9+
* Helper class for adding OCSP responder URL to messages.
10+
*/
11+
final class OcspResponderUriMessageAppender {
12+
13+
static String appendResponderUri(String message, URI ocspResponderUri) {
14+
if (ocspResponderUri == null) {
15+
return message;
16+
}
17+
return message + " (OCSP responder: " + ocspResponderUri + ")";
18+
}
19+
20+
private OcspResponderUriMessageAppender() {
21+
throw new IllegalStateException("Utility class");
22+
}
23+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-FileCopyrightText: Estonian Information System Authority
2+
// SPDX-License-Identifier: MIT
3+
4+
package eu.webeid.ocsp.exceptions;
5+
6+
import eu.webeid.security.exceptions.AuthTokenException;
7+
8+
import java.net.URI;
9+
10+
import static eu.webeid.ocsp.exceptions.OcspResponderUriMessageAppender.appendResponderUri;
11+
12+
/**
13+
* Thrown when user certificate revocation check with OCSP fails.
14+
*/
15+
public class UserCertificateOCSPCheckFailedException extends AuthTokenException {
16+
17+
public UserCertificateOCSPCheckFailedException(Throwable cause, URI ocspResponderUri) {
18+
super(appendResponderUri("User certificate revocation check has failed", ocspResponderUri), cause);
19+
}
20+
21+
public UserCertificateOCSPCheckFailedException(String message, URI ocspResponderUri) {
22+
super(appendResponderUri("User certificate revocation check has failed: " + message, ocspResponderUri));
23+
}
24+
25+
public UserCertificateOCSPCheckFailedException(String message) {
26+
super(message);
27+
}
28+
29+
}

0 commit comments

Comments
 (0)