Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ The following additional configuration options are available in `AuthTokenValida

- `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.
- `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()`.
- `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`.
- `withOcspRequestTimeout(Duration ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds.
- `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.
- `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.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>authtoken-validation</artifactId>
<groupId>org.webeid.security</groupId>
<version>2.0.1</version>
<version>2.1.0</version>
<packaging>jar</packaging>
<name>authtoken-validation</name>
<description>Web eID authentication token validation library for Java</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
package eu.webeid.security.validator;

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

private final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration();
private OcspClient ocspClient;

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

/**
* Uses the provided OCSP client instance during user certificate revocation check with OCSP.
* The provided client instance must be thread-safe.
*
* @param ocspClient OCSP client instance
* @return the builder instance for method chaining
*/
public AuthTokenValidatorBuilder withOcspClient(OcspClient ocspClient) {
this.ocspClient = ocspClient;
LOG.debug("Using the OCSP client provided by API consumer");
return this;
}

/**
* Validates the configuration and builds the {@link AuthTokenValidator} object with it.
* The returned {@link AuthTokenValidator} object is immutable/thread-safe.
Expand All @@ -163,7 +179,10 @@ public AuthTokenValidatorBuilder withDesignatedOcspServiceConfiguration(Designat
*/
public AuthTokenValidator build() throws NullPointerException, IllegalArgumentException, JceException {
configuration.validate();
return new AuthTokenValidatorImpl(configuration);
if (configuration.isUserCertificateRevocationCheckWithOcspEnabled() && ocspClient == null) {
ocspClient = OkHttpOcspClient.build(configuration.getOcspRequestTimeout());
}
return new AuthTokenValidatorImpl(configuration, ocspClient);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import eu.webeid.security.validator.certvalidators.SubjectCertificateTrustedValidator;
import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
import eu.webeid.security.validator.ocsp.OcspClient;
import eu.webeid.security.validator.ocsp.OcspClientImpl;
import eu.webeid.security.validator.ocsp.OcspServiceProvider;
import eu.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration;
import org.slf4j.Logger;
Expand All @@ -46,6 +45,7 @@
import java.security.cert.CertStore;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Objects;
import java.util.Set;

/**
Expand Down Expand Up @@ -73,8 +73,9 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {

/**
* @param configuration configuration parameters for the token validator
* @param ocspClient client for communicating with the OCSP service
*/
AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration) throws JceException {
AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration, OcspClient ocspClient) throws JceException {
// Copy the configuration object to make AuthTokenValidatorImpl immutable and thread-safe.
this.configuration = configuration.copy();

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

if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) {
ocspClient = OcspClientImpl.build(configuration.getOcspRequestTimeout());
// The OCSP client may be provided by the API consumer.
this.ocspClient = Objects.requireNonNull(ocspClient, "OCSP client must not be null when OCSP check is enabled");
ocspServiceProvider = new OcspServiceProvider(
configuration.getDesignatedOcspServiceConfiguration(),
new AiaOcspServiceConfiguration(configuration.getNonceDisabledOcspUrls(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@
import java.time.Duration;
import java.util.Objects;

public class OcspClientImpl implements OcspClient {
public class OkHttpOcspClient implements OcspClient {

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

private final OkHttpClient httpClient;

public static OcspClient build(Duration ocspRequestTimeout) {
return new OcspClientImpl(
return new OkHttpOcspClient(
new OkHttpClient.Builder()
.connectTimeout(ocspRequestTimeout)
.callTimeout(ocspRequestTimeout)
Expand All @@ -58,8 +58,8 @@ public static OcspClient build(Duration ocspRequestTimeout) {
/**
* Use OkHttpClient to fetch the OCSP response from the OCSP responder service.
*
* @param uri OCSP server URL
* @param ocspReq OCSP request
* @param uri OCSP server URL
* @param ocspReq OCSP request
* @return OCSP response from the server
* @throws IOException if the request could not be executed due to cancellation, a connectivity problem or timeout,
* or if the response status is not successful, or if response has wrong content type.
Expand Down Expand Up @@ -89,7 +89,7 @@ public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException {
}
}

private OcspClientImpl(OkHttpClient httpClient) {
public OkHttpOcspClient(OkHttpClient httpClient) {
this.httpClient = httpClient;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import eu.webeid.security.exceptions.OCSPCertificateException;
import eu.webeid.security.validator.AuthTokenValidator;
import eu.webeid.security.validator.AuthTokenValidatorBuilder;
import eu.webeid.security.validator.ocsp.OcspClient;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;

import java.io.IOException;
Expand Down Expand Up @@ -59,6 +60,12 @@ public static AuthTokenValidator getAuthTokenValidator(String url, X509Certifica
.build();
}

public static AuthTokenValidator getAuthTokenValidatorWithOverriddenOcspClient(OcspClient ocspClient) throws CertificateException, JceException, IOException {
return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates())
.withOcspClient(ocspClient)
.build();
}

public static AuthTokenValidator getAuthTokenValidatorWithOcspCheck() throws CertificateException, JceException, IOException {
return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ void whenCertificateFieldIsEmpty_thenParsingFails() throws AuthTokenException {
}

@Test
void whenCertificateFieldIsArray_thenParsingFails() throws AuthTokenException {
void whenCertificateFieldIsArray_thenParsingFails() {
assertThatThrownBy(() -> replaceTokenField(AUTH_TOKEN, "\"X5C\"", "[1,2,3,4]"))
.isInstanceOf(AuthTokenParseException.class)
.hasMessage("Error parsing Web eID authentication token")
.getCause()
.cause()
.isInstanceOf(MismatchedInputException.class)
.hasMessageStartingWith("Cannot deserialize value of type `java.lang.String` from Array value");
}
Expand All @@ -106,7 +106,7 @@ void whenCertificateFieldIsNumber_thenParsingFails() throws AuthTokenException {
assertThatThrownBy(() -> validator
.validate(token, VALID_CHALLENGE_NONCE))
.isInstanceOf(CertificateDecodingException.class)
.getCause()
.cause()
.isInstanceOf(CertificateException.class)
.hasMessage("Could not parse certificate: java.io.IOException: Empty input");
}
Expand All @@ -117,7 +117,7 @@ void whenCertificateFieldIsNotBase64_thenParsingFails() throws AuthTokenExceptio
assertThatThrownBy(() -> validator
.validate(token, VALID_CHALLENGE_NONCE))
.isInstanceOf(CertificateDecodingException.class)
.getCause()
.cause()
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Illegal base64 character 20");
}
Expand All @@ -128,7 +128,7 @@ void whenCertificateFieldIsNotCertificate_thenParsingFails() throws AuthTokenExc
assertThatThrownBy(() -> validator
.validate(token, VALID_CHALLENGE_NONCE))
.isInstanceOf(CertificateDecodingException.class)
.getCause()
.cause()
.isInstanceOf(CertificateException.class)
.hasMessage("Could not parse certificate: java.io.IOException: Empty input");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
import com.google.common.io.ByteStreams;
import eu.webeid.security.exceptions.JceException;
import eu.webeid.security.validator.ocsp.OcspClient;
import eu.webeid.security.validator.ocsp.OcspClientImpl;
import okhttp3.*;
import eu.webeid.security.validator.ocsp.OkHttpOcspClient;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPResp;
Expand Down Expand Up @@ -58,7 +62,7 @@ class SubjectCertificateNotRevokedValidatorTest {

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

private final OcspClient ocspClient = OcspClientImpl.build(Duration.ofSeconds(5));
private final OcspClient ocspClient = OkHttpOcspClient.build(Duration.ofSeconds(5));
private SubjectCertificateTrustedValidator trustedValidator;
private X509Certificate estEid2018Cert;

Expand Down Expand Up @@ -102,7 +106,7 @@ void whenOcspUrlIsInvalid_thenThrows() throws Exception {
assertThatCode(() ->
validator.validateCertificateNotRevoked(estEid2018Cert))
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
.getCause()
.cause()
.isInstanceOf(IOException.class)
.hasMessageMatching("invalid.invalid: (Name or service not known|"
+ "Temporary failure in name resolution)");
Expand All @@ -115,7 +119,7 @@ void whenOcspRequestFails_thenThrows() throws Exception {
assertThatCode(() ->
validator.validateCertificateNotRevoked(estEid2018Cert))
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
.getCause()
.cause()
.isInstanceOf(IOException.class)
.hasMessageStartingWith("OCSP request was not successful, response: Response{");
}
Expand All @@ -129,7 +133,7 @@ void whenOcspRequestHasInvalidBody_thenThrows() throws Exception {
assertThatCode(() ->
validator.validateCertificateNotRevoked(estEid2018Cert))
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
.getCause()
.cause()
.isInstanceOf(IOException.class)
.hasMessage("DEF length 110 object truncated by 105");
}
Expand Down Expand Up @@ -179,7 +183,7 @@ void whenOcspResponseHasInvalidResponderCert_thenThrows() throws Exception {
assertThatCode(() ->
validator.validateCertificateNotRevoked(estEid2018Cert))
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
.getCause()
.cause()
.isInstanceOf(OCSPException.class)
.hasMessage("exception processing sig: java.lang.IllegalArgumentException: invalid info structure in RSA public key");
}
Expand All @@ -193,7 +197,7 @@ void whenOcspResponseHasInvalidTag_thenThrows() throws Exception {
assertThatCode(() ->
validator.validateCertificateNotRevoked(estEid2018Cert))
.isInstanceOf(UserCertificateOCSPCheckFailedException.class)
.getCause()
.cause()
.isInstanceOf(OCSPException.class)
.hasMessage("problem decoding object: java.io.IOException: unknown tag 23 encountered");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2022 Estonian Information System Authority
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package eu.webeid.security.validator.ocsp;

import eu.webeid.security.exceptions.JceException;
import eu.webeid.security.testutil.AbstractTestWithValidator;
import eu.webeid.security.testutil.AuthTokenValidators;
import eu.webeid.security.validator.AuthTokenValidator;
import okhttp3.OkHttpClient;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.net.URI;
import java.security.cert.CertificateException;

import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class OcspClientOverrideTest extends AbstractTestWithValidator {

@Test
void whenOcspClientIsOverridden_thenItIsUsed() throws JceException, CertificateException, IOException {
final AuthTokenValidator validator = AuthTokenValidators.getAuthTokenValidatorWithOverriddenOcspClient(new OcpClientThatThrows());
assertThatThrownBy(() -> validator.validate(validAuthToken, VALID_CHALLENGE_NONCE))
.cause()
.isInstanceOf(OcpClientThatThrowsException.class);
}

@Test
@Disabled("Demonstrates how to configure the OkHttpClient instance for OkHttpOcspClient")
void whenOkHttpOcspClientIsExtended_thenOcspCallSucceeds() throws JceException, CertificateException, IOException {
final AuthTokenValidator validator = AuthTokenValidators.getAuthTokenValidatorWithOverriddenOcspClient(
new OkHttpOcspClient(new OkHttpClient.Builder().build())
);
assertThatCode(() -> validator.validate(validAuthToken, VALID_CHALLENGE_NONCE))
.doesNotThrowAnyException();
}

private static class OcpClientThatThrows implements OcspClient {
@Override
public OCSPResp request(URI url, OCSPReq request) throws IOException {
throw new OcpClientThatThrowsException();
}
}

private static class OcpClientThatThrowsException extends IOException {
}

}