Skip to content

Commit 42ffdda

Browse files
committed
feat(OCSP): add AuthTokenValidatorBuilder.withOcspClient() for overriding the OCSP client. Fixes #31
Signed-off-by: Mart Somermaa <[email protected]>
1 parent ad8798d commit 42ffdda

File tree

9 files changed

+129
-24
lines changed

9 files changed

+129
-24
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ The following additional configuration options are available in `AuthTokenValida
298298
299299
- `withoutUserCertificateRevocationCheckWithOcsp()` – turns off user certificate revocation check with OCSP. OCSP check is enabled by default and the OCSP responder access location URL is extracted from the user certificate AIA extension unless a designated OCSP service is activated.
300300
- `withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration)` – activates the provided designated OCSP responder service configuration for user certificate revocation check with OCSP. The designated service is only used for checking the status of the certificates whose issuers are supported by the service, for other certificates the default AIA extension service access location will be used. See configuration examples in `testutil.OcspServiceMaker.getDesignatedOcspServiceConfiguration()`.
301+
- `withOcspClient(OcspClient ocspClient)` - uses the provided OCSP client instance during user certificate revocation check with OCSP. The provided client instance must be thread-safe. This gives the possibility to either configure the request timeouts, proxies etc of the `OkHttpClient` instance used by `OkHttpOcspClient` or provide an implementation that uses an altogether different HTTP client, for example the built-in `HttpClient` provided by Java 9+. See examples in `OcspClientOverrideTest`.
301302
- `withOcspRequestTimeout(Duration ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds.
302303
- `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` – adds the given policies to the list of disallowed user certificate policies. In order for the user certificate to be considered valid, it must not contain any policies present in this list. Contains the Estonian Mobile-ID policies by default as it must not be possible to authenticate with a Mobile-ID certificate when an eID smart card is expected.
303304
- `withNonceDisabledOcspUrls(URI... urls)` – adds the given URLs to the list of OCSP responder access location URLs for which the nonce protocol extension will be disabled. Some OCSP responders don't support the nonce extension. Contains the ESTEID-2015 OCSP responder URL by default.

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<artifactId>authtoken-validation</artifactId>
77
<groupId>org.webeid.security</groupId>
8-
<version>2.0.1</version>
8+
<version>2.1.0</version>
99
<packaging>jar</packaging>
1010
<name>authtoken-validation</name>
1111
<description>Web eID authentication token validation library for Java</description>

src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
package eu.webeid.security.validator;
2424

2525
import eu.webeid.security.exceptions.JceException;
26+
import eu.webeid.security.validator.ocsp.OcspClient;
27+
import eu.webeid.security.validator.ocsp.OkHttpOcspClient;
2628
import eu.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration;
2729
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
2830
import org.slf4j.Logger;
@@ -42,6 +44,7 @@ public class AuthTokenValidatorBuilder {
4244
private static final Logger LOG = LoggerFactory.getLogger(AuthTokenValidatorBuilder.class);
4345

4446
private final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration();
47+
private OcspClient ocspClient;
4548

4649
/**
4750
* Sets the expected site origin, i.e. the domain that the application is running on.
@@ -152,6 +155,19 @@ public AuthTokenValidatorBuilder withDesignatedOcspServiceConfiguration(Designat
152155
return this;
153156
}
154157

158+
/**
159+
* Uses the provided OCSP client instance during user certificate revocation check with OCSP.
160+
* The provided client instance must be thread-safe.
161+
*
162+
* @param ocspClient OCSP client instance
163+
* @return the builder instance for method chaining
164+
*/
165+
public AuthTokenValidatorBuilder withOcspClient(OcspClient ocspClient) {
166+
this.ocspClient = ocspClient;
167+
LOG.debug("Using the OCSP client provided by API consumer");
168+
return this;
169+
}
170+
155171
/**
156172
* Validates the configuration and builds the {@link AuthTokenValidator} object with it.
157173
* The returned {@link AuthTokenValidator} object is immutable/thread-safe.
@@ -163,7 +179,10 @@ public AuthTokenValidatorBuilder withDesignatedOcspServiceConfiguration(Designat
163179
*/
164180
public AuthTokenValidator build() throws NullPointerException, IllegalArgumentException, JceException {
165181
configuration.validate();
166-
return new AuthTokenValidatorImpl(configuration);
182+
if (configuration.isUserCertificateRevocationCheckWithOcspEnabled() && ocspClient == null) {
183+
ocspClient = OkHttpOcspClient.build(configuration.getOcspRequestTimeout());
184+
}
185+
return new AuthTokenValidatorImpl(configuration, ocspClient);
167186
}
168187

169188
}

src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import eu.webeid.security.validator.certvalidators.SubjectCertificateTrustedValidator;
3737
import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
3838
import eu.webeid.security.validator.ocsp.OcspClient;
39-
import eu.webeid.security.validator.ocsp.OcspClientImpl;
4039
import eu.webeid.security.validator.ocsp.OcspServiceProvider;
4140
import eu.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration;
4241
import org.slf4j.Logger;
@@ -46,6 +45,7 @@
4645
import java.security.cert.CertStore;
4746
import java.security.cert.TrustAnchor;
4847
import java.security.cert.X509Certificate;
48+
import java.util.Objects;
4949
import java.util.Set;
5050

5151
/**
@@ -73,8 +73,9 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
7373

7474
/**
7575
* @param configuration configuration parameters for the token validator
76+
* @param ocspClient client for communicating with the OCSP service
7677
*/
77-
AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration) throws JceException {
78+
AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration, OcspClient ocspClient) throws JceException {
7879
// Copy the configuration object to make AuthTokenValidatorImpl immutable and thread-safe.
7980
this.configuration = configuration.copy();
8081

@@ -89,7 +90,8 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
8990
);
9091

9192
if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) {
92-
ocspClient = OcspClientImpl.build(configuration.getOcspRequestTimeout());
93+
// The OCSP client may be provided by the API consumer.
94+
this.ocspClient = Objects.requireNonNull(ocspClient, "OCSP client must not be null when OCSP check is enabled");
9395
ocspServiceProvider = new OcspServiceProvider(
9496
configuration.getDesignatedOcspServiceConfiguration(),
9597
new AiaOcspServiceConfiguration(configuration.getNonceDisabledOcspUrls(),

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@
3838
import java.time.Duration;
3939
import java.util.Objects;
4040

41-
public class OcspClientImpl implements OcspClient {
41+
public class OkHttpOcspClient implements OcspClient {
4242

43-
private static final Logger LOG = LoggerFactory.getLogger(OcspClientImpl.class);
43+
private static final Logger LOG = LoggerFactory.getLogger(OkHttpOcspClient.class);
4444
private static final MediaType OCSP_REQUEST_TYPE = MediaType.get("application/ocsp-request");
4545
private static final MediaType OCSP_RESPONSE_TYPE = MediaType.get("application/ocsp-response");
4646

4747
private final OkHttpClient httpClient;
4848

4949
public static OcspClient build(Duration ocspRequestTimeout) {
50-
return new OcspClientImpl(
50+
return new OkHttpOcspClient(
5151
new OkHttpClient.Builder()
5252
.connectTimeout(ocspRequestTimeout)
5353
.callTimeout(ocspRequestTimeout)
@@ -58,8 +58,8 @@ public static OcspClient build(Duration ocspRequestTimeout) {
5858
/**
5959
* Use OkHttpClient to fetch the OCSP response from the OCSP responder service.
6060
*
61-
* @param uri OCSP server URL
62-
* @param ocspReq OCSP request
61+
* @param uri OCSP server URL
62+
* @param ocspReq OCSP request
6363
* @return OCSP response from the server
6464
* @throws IOException if the request could not be executed due to cancellation, a connectivity problem or timeout,
6565
* or if the response status is not successful, or if response has wrong content type.
@@ -89,7 +89,7 @@ public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException {
8989
}
9090
}
9191

92-
private OcspClientImpl(OkHttpClient httpClient) {
92+
public OkHttpOcspClient(OkHttpClient httpClient) {
9393
this.httpClient = httpClient;
9494
}
9595

src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import eu.webeid.security.exceptions.OCSPCertificateException;
2828
import eu.webeid.security.validator.AuthTokenValidator;
2929
import eu.webeid.security.validator.AuthTokenValidatorBuilder;
30+
import eu.webeid.security.validator.ocsp.OcspClient;
3031
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
3132

3233
import java.io.IOException;
@@ -59,6 +60,12 @@ public static AuthTokenValidator getAuthTokenValidator(String url, X509Certifica
5960
.build();
6061
}
6162

63+
public static AuthTokenValidator getAuthTokenValidatorWithOverriddenOcspClient(OcspClient ocspClient) throws CertificateException, JceException, IOException {
64+
return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates())
65+
.withOcspClient(ocspClient)
66+
.build();
67+
}
68+
6269
public static AuthTokenValidator getAuthTokenValidatorWithOcspCheck() throws CertificateException, JceException, IOException {
6370
return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates())
6471
.build();

src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ void whenCertificateFieldIsEmpty_thenParsingFails() throws AuthTokenException {
9191
}
9292

9393
@Test
94-
void whenCertificateFieldIsArray_thenParsingFails() throws AuthTokenException {
94+
void whenCertificateFieldIsArray_thenParsingFails() {
9595
assertThatThrownBy(() -> replaceTokenField(AUTH_TOKEN, "\"X5C\"", "[1,2,3,4]"))
9696
.isInstanceOf(AuthTokenParseException.class)
9797
.hasMessage("Error parsing Web eID authentication token")
98-
.getCause()
98+
.cause()
9999
.isInstanceOf(MismatchedInputException.class)
100100
.hasMessageStartingWith("Cannot deserialize value of type `java.lang.String` from Array value");
101101
}
@@ -106,7 +106,7 @@ void whenCertificateFieldIsNumber_thenParsingFails() throws AuthTokenException {
106106
assertThatThrownBy(() -> validator
107107
.validate(token, VALID_CHALLENGE_NONCE))
108108
.isInstanceOf(CertificateDecodingException.class)
109-
.getCause()
109+
.cause()
110110
.isInstanceOf(CertificateException.class)
111111
.hasMessage("Could not parse certificate: java.io.IOException: Empty input");
112112
}
@@ -117,7 +117,7 @@ void whenCertificateFieldIsNotBase64_thenParsingFails() throws AuthTokenExceptio
117117
assertThatThrownBy(() -> validator
118118
.validate(token, VALID_CHALLENGE_NONCE))
119119
.isInstanceOf(CertificateDecodingException.class)
120-
.getCause()
120+
.cause()
121121
.isInstanceOf(IllegalArgumentException.class)
122122
.hasMessage("Illegal base64 character 20");
123123
}
@@ -128,7 +128,7 @@ void whenCertificateFieldIsNotCertificate_thenParsingFails() throws AuthTokenExc
128128
assertThatThrownBy(() -> validator
129129
.validate(token, VALID_CHALLENGE_NONCE))
130130
.isInstanceOf(CertificateDecodingException.class)
131-
.getCause()
131+
.cause()
132132
.isInstanceOf(CertificateException.class)
133133
.hasMessage("Could not parse certificate: java.io.IOException: Empty input");
134134
}

src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@
2525
import com.google.common.io.ByteStreams;
2626
import eu.webeid.security.exceptions.JceException;
2727
import eu.webeid.security.validator.ocsp.OcspClient;
28-
import eu.webeid.security.validator.ocsp.OcspClientImpl;
29-
import okhttp3.*;
28+
import eu.webeid.security.validator.ocsp.OkHttpOcspClient;
29+
import okhttp3.MediaType;
30+
import okhttp3.Protocol;
31+
import okhttp3.Request;
32+
import okhttp3.Response;
33+
import okhttp3.ResponseBody;
3034
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
3135
import org.bouncycastle.cert.ocsp.OCSPException;
3236
import org.bouncycastle.cert.ocsp.OCSPResp;
@@ -58,7 +62,7 @@ class SubjectCertificateNotRevokedValidatorTest {
5862

5963
private static final MediaType OCSP_RESPONSE = MediaType.get("application/ocsp-response");
6064

61-
private final OcspClient ocspClient = OcspClientImpl.build(Duration.ofSeconds(5));
65+
private final OcspClient ocspClient = OkHttpOcspClient.build(Duration.ofSeconds(5));
6266
private SubjectCertificateTrustedValidator trustedValidator;
6367
private X509Certificate estEid2018Cert;
6468

@@ -102,7 +106,7 @@ void whenOcspUrlIsInvalid_thenThrows() throws Exception {
102106
assertThatCode(() ->
103107
validator.validateCertificateNotRevoked(estEid2018Cert))
104108
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
105-
.getCause()
109+
.cause()
106110
.isInstanceOf(IOException.class)
107111
.hasMessageMatching("invalid.invalid: (Name or service not known|"
108112
+ "Temporary failure in name resolution)");
@@ -115,7 +119,7 @@ void whenOcspRequestFails_thenThrows() throws Exception {
115119
assertThatCode(() ->
116120
validator.validateCertificateNotRevoked(estEid2018Cert))
117121
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
118-
.getCause()
122+
.cause()
119123
.isInstanceOf(IOException.class)
120124
.hasMessageStartingWith("OCSP request was not successful, response: Response{");
121125
}
@@ -129,7 +133,7 @@ void whenOcspRequestHasInvalidBody_thenThrows() throws Exception {
129133
assertThatCode(() ->
130134
validator.validateCertificateNotRevoked(estEid2018Cert))
131135
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
132-
.getCause()
136+
.cause()
133137
.isInstanceOf(IOException.class)
134138
.hasMessage("DEF length 110 object truncated by 105");
135139
}
@@ -179,7 +183,7 @@ void whenOcspResponseHasInvalidResponderCert_thenThrows() throws Exception {
179183
assertThatCode(() ->
180184
validator.validateCertificateNotRevoked(estEid2018Cert))
181185
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
182-
.getCause()
186+
.cause()
183187
.isInstanceOf(OCSPException.class)
184188
.hasMessage("exception processing sig: java.lang.IllegalArgumentException: invalid info structure in RSA public key");
185189
}
@@ -193,7 +197,7 @@ void whenOcspResponseHasInvalidTag_thenThrows() throws Exception {
193197
assertThatCode(() ->
194198
validator.validateCertificateNotRevoked(estEid2018Cert))
195199
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
196-
.getCause()
200+
.cause()
197201
.isInstanceOf(OCSPException.class)
198202
.hasMessage("problem decoding object: java.io.IOException: unknown tag 23 encountered");
199203
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (c) 2022 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.security.validator.ocsp;
24+
25+
import eu.webeid.security.exceptions.JceException;
26+
import eu.webeid.security.testutil.AbstractTestWithValidator;
27+
import eu.webeid.security.testutil.AuthTokenValidators;
28+
import eu.webeid.security.validator.AuthTokenValidator;
29+
import okhttp3.OkHttpClient;
30+
import org.bouncycastle.cert.ocsp.OCSPReq;
31+
import org.bouncycastle.cert.ocsp.OCSPResp;
32+
import org.junit.jupiter.api.Disabled;
33+
import org.junit.jupiter.api.Test;
34+
35+
import java.io.IOException;
36+
import java.net.URI;
37+
import java.security.cert.CertificateException;
38+
39+
import static org.assertj.core.api.Assertions.assertThatCode;
40+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
41+
42+
class OcspClientOverrideTest extends AbstractTestWithValidator {
43+
44+
@Test
45+
void whenOcspClientIsOverridden_thenItIsUsed() throws JceException, CertificateException, IOException {
46+
final AuthTokenValidator validator = AuthTokenValidators.getAuthTokenValidatorWithOverriddenOcspClient(new OcpClientThatThrows());
47+
assertThatThrownBy(() -> validator.validate(validAuthToken, VALID_CHALLENGE_NONCE))
48+
.cause()
49+
.isInstanceOf(OcpClientThatThrowsException.class);
50+
}
51+
52+
@Test
53+
@Disabled("Demonstrates how to configure the OkHttpClient instance for OkHttpOcspClient")
54+
void whenOkHttpOcspClientIsExtended_thenOcspCallSucceeds() throws JceException, CertificateException, IOException {
55+
final AuthTokenValidator validator = AuthTokenValidators.getAuthTokenValidatorWithOverriddenOcspClient(
56+
new OkHttpOcspClient(new OkHttpClient.Builder().build())
57+
);
58+
assertThatCode(() -> validator.validate(validAuthToken, VALID_CHALLENGE_NONCE))
59+
.doesNotThrowAnyException();
60+
}
61+
62+
private static class OcpClientThatThrows implements OcspClient {
63+
@Override
64+
public OCSPResp request(URI url, OCSPReq request) throws IOException {
65+
throw new OcpClientThatThrowsException();
66+
}
67+
}
68+
69+
private static class OcpClientThatThrowsException extends IOException {
70+
}
71+
72+
}

0 commit comments

Comments
 (0)