nonceDisabledOcspUrls = Sets.newHashSet(AIA_ESTEID_2015);
AuthTokenValidationConfiguration() {
}
@@ -68,14 +71,15 @@ final class AuthTokenValidationConfiguration {
private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) {
this.siteOrigin = other.siteOrigin;
this.nonceCache = other.nonceCache;
- this.trustedCACertificates = new HashSet<>(other.trustedCACertificates);
+ this.trustedCACertificates = Collections.unmodifiableSet(new HashSet<>(other.trustedCACertificates));
this.isUserCertificateRevocationCheckWithOcspEnabled = other.isUserCertificateRevocationCheckWithOcspEnabled;
this.ocspRequestTimeout = other.ocspRequestTimeout;
this.allowedClientClockSkew = other.allowedClientClockSkew;
+ this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration;
this.isSiteCertificateFingerprintValidationEnabled = other.isSiteCertificateFingerprintValidationEnabled;
this.siteCertificateSha256Fingerprint = other.siteCertificateSha256Fingerprint;
- this.disallowedSubjectCertificatePolicies = new HashSet<>(other.disallowedSubjectCertificatePolicies);
- this.nonceDisabledOcspUrls = new HashSet<>(other.nonceDisabledOcspUrls);
+ this.disallowedSubjectCertificatePolicies = Collections.unmodifiableSet(new HashSet<>(other.disallowedSubjectCertificatePolicies));
+ this.nonceDisabledOcspUrls = Collections.unmodifiableSet(new HashSet<>(other.nonceDisabledOcspUrls));
}
void setSiteOrigin(URI siteOrigin) {
@@ -122,6 +126,14 @@ Duration getAllowedClientClockSkew() {
return allowedClientClockSkew;
}
+ public DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() {
+ return designatedOcspServiceConfiguration;
+ }
+
+ public void setDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration) {
+ this.designatedOcspServiceConfiguration = designatedOcspServiceConfiguration;
+ }
+
boolean isSiteCertificateFingerprintValidationEnabled() {
return isSiteCertificateFingerprintValidationEnabled;
}
diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidator.java b/src/main/java/org/webeid/security/validator/AuthTokenValidator.java
index 8e5ba604..69d8c55d 100644
--- a/src/main/java/org/webeid/security/validator/AuthTokenValidator.java
+++ b/src/main/java/org/webeid/security/validator/AuthTokenValidator.java
@@ -23,7 +23,7 @@
package org.webeid.security.validator;
import org.webeid.security.exceptions.TokenValidationException;
-import org.webeid.security.util.CertUtil;
+import org.webeid.security.certificate.CertificateData;
import org.webeid.security.util.TitleCase;
import java.security.cert.X509Certificate;
@@ -37,7 +37,7 @@ public interface AuthTokenValidator {
* Validates the Web eID authentication token signed by the subject and returns
* the subject certificate that can be used for retrieving information about the subject.
*
- * See {@link CertUtil} and {@link TitleCase} for convenience methods for retrieving user
+ * See {@link CertificateData} and {@link TitleCase} for convenience methods for retrieving user
* information from the certificate.
*
* @param tokenWithSignature the Web eID authentication token, in OpenID X509 ID Token format, with signature
diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java b/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java
index 9c5be7ef..c9e632c6 100644
--- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java
+++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2021 The Web eID Project
+ * Copyright (c) 2020-2021 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
@@ -26,6 +26,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.webeid.security.exceptions.JceException;
+import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration;
import javax.cache.Cache;
import java.net.URI;
@@ -33,6 +34,7 @@
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Collections;
+import java.util.stream.Collectors;
/**
* Builder for constructing {@link AuthTokenValidator} instances.
@@ -73,9 +75,10 @@ public AuthTokenValidatorBuilder withNonceCache(Cache cac
}
/**
- * Adds the given certificates to the list of trusted subject certificate intermediate Certificate Authorities.
- * In order for the user certificate to be considered valid, the certificate of the issuer of the user certificate
- * must be present in this list.
+ * Adds the given certificates to the list of trusted intermediate Certificate Authorities
+ * used during validation of subject and OCSP responder certificates.
+ * In order for a user or OCSP responder certificate to be considered valid, the certificate
+ * of the issuer of the certificate must be present in this list.
*
* At least one trusted intermediate Certificate Authority must be provided as a mandatory configuration parameter.
*
@@ -84,7 +87,12 @@ public AuthTokenValidatorBuilder withNonceCache(Cache cac
*/
public AuthTokenValidatorBuilder withTrustedCertificateAuthorities(X509Certificate... certificates) {
Collections.addAll(configuration.getTrustedCACertificates(), certificates);
- LOG.debug("Trusted intermediate certificate authorities set to {}", configuration.getTrustedCACertificates());
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Trusted intermediate certificate authorities set to {}",
+ configuration.getTrustedCACertificates().stream()
+ .map(X509Certificate::getSubjectDN)
+ .collect(Collectors.toList()));
+ }
return this;
}
@@ -125,7 +133,7 @@ public AuthTokenValidatorBuilder withoutUserCertificateRevocationCheckWithOcsp()
*/
public AuthTokenValidatorBuilder withOcspRequestTimeout(Duration ocspRequestTimeout) {
configuration.setOcspRequestTimeout(ocspRequestTimeout);
- LOG.debug("OCSP request timeout set to {}.", ocspRequestTimeout);
+ LOG.debug("OCSP request timeout set to {}", ocspRequestTimeout);
return this;
}
@@ -142,6 +150,21 @@ public AuthTokenValidatorBuilder withNonceDisabledOcspUrls(URI... urls) {
return this;
}
+ /**
+ * Activates the provided designated OCSP service 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, falling back to the default OCSP service access location from
+ * the certificate's AIA extension if not.
+ *
+ * @param serviceConfiguration configuration of the designated OCSP service
+ * @return the builder instance for method chaining
+ */
+ public AuthTokenValidatorBuilder withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration) {
+ configuration.setDesignatedOcspServiceConfiguration(serviceConfiguration);
+ LOG.debug("Using designated OCSP service configuration");
+ return this;
+ }
+
/**
* Sets the tolerated clock skew of the client computer when verifying the token expiration field {@code exp}.
*
diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidatorData.java b/src/main/java/org/webeid/security/validator/AuthTokenValidatorData.java
index 9115758a..9b0fb5d3 100644
--- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorData.java
+++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorData.java
@@ -36,7 +36,7 @@ public final class AuthTokenValidatorData {
private String origin;
private String siteCertificateFingerprint;
- AuthTokenValidatorData(X509Certificate subjectCertificate) {
+ public AuthTokenValidatorData(X509Certificate subjectCertificate) {
this.subjectCertificate = subjectCertificate;
}
diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java b/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java
index d2952021..917e1d2c 100644
--- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java
+++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java
@@ -22,23 +22,24 @@
package org.webeid.security.validator;
-import com.google.common.base.Suppliers;
-import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.webeid.security.exceptions.JceException;
import org.webeid.security.exceptions.TokenParseException;
import org.webeid.security.exceptions.TokenValidationException;
+import org.webeid.security.validator.ocsp.OcspClient;
+import org.webeid.security.validator.ocsp.OcspClientImpl;
+import org.webeid.security.validator.ocsp.OcspServiceProvider;
+import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration;
import org.webeid.security.validator.validators.*;
-import java.security.GeneralSecurityException;
import java.security.cert.CertStore;
-import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Set;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
+
+import static org.webeid.security.certificate.CertificateValidator.buildCertStoreFromCertificates;
+import static org.webeid.security.certificate.CertificateValidator.buildTrustAnchorsFromCertificates;
/**
* Provides the default implementation of {@link AuthTokenValidator}.
@@ -50,16 +51,16 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
private static final Logger LOG = LoggerFactory.getLogger(AuthTokenValidatorImpl.class);
private final AuthTokenValidationConfiguration configuration;
- /*
- * OkHttp performs best when a single OkHttpClient instance is created and reused for all HTTP calls.
- * This is because each client holds its own connection pool and thread pools.
- * Reusing connections and threads reduces latency and saves memory.
- */
- private final Supplier httpClientSupplier;
private final ValidatorBatch simpleSubjectCertificateValidators;
private final ValidatorBatch tokenBodyValidators;
private final Set trustedCACertificateAnchors;
private final CertStore trustedCACertificateCertStore;
+ // OcspClient uses OkHttp internally.
+ // OkHttp performs best when a single OkHttpClient instance is created and reused for all HTTP calls.
+ // This is because each client holds its own connection pool and thread pools.
+ // Reusing connections and threads reduces latency and saves memory.
+ private OcspClient ocspClient;
+ private OcspServiceProvider ocspServiceProvider;
/**
* @param configuration configuration parameters for the token validator
@@ -67,19 +68,14 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration) throws JceException {
// Copy the configuration object to make AuthTokenValidatorImpl immutable and thread-safe.
this.configuration = configuration.copy();
- // Lazy initialization, avoid constructing the OkHttpClient object when certificate revocation check is not enabled.
- // Returns a supplier which caches the instance retrieved during the first call to get() and returns
- // that value on subsequent calls to get(). The returned supplier is thread-safe.
- // The OkHttpClient build() method will be invoked at most once.
- this.httpClientSupplier = Suppliers.memoize(() -> new OkHttpClient.Builder()
- .connectTimeout(configuration.getOcspRequestTimeout())
- .callTimeout(configuration.getOcspRequestTimeout())
- .build()
- );
+
+ // Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator and AiaOcspService.
+ trustedCACertificateAnchors = buildTrustAnchorsFromCertificates(configuration.getTrustedCACertificates());
+ trustedCACertificateCertStore = buildCertStoreFromCertificates(configuration.getTrustedCACertificates());
simpleSubjectCertificateValidators = ValidatorBatch.createFrom(
- FunctionalSubjectCertificateValidators::validateCertificateExpiry,
- FunctionalSubjectCertificateValidators::validateCertificatePurpose,
+ new CertificateExpiryValidator(trustedCACertificateAnchors)::validateCertificateExpiry,
+ SubjectCertificatePurposeValidator::validateCertificatePurpose,
new SubjectCertificatePolicyValidator(configuration.getDisallowedSubjectCertificatePolicies())::validateCertificatePolicies
);
tokenBodyValidators = ValidatorBatch.createFrom(
@@ -89,19 +85,13 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
new SiteCertificateFingerprintValidator(configuration.getSiteCertificateSha256Fingerprint())::validateSiteCertificateFingerprint
);
- // Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator.
- trustedCACertificateAnchors = configuration.getTrustedCACertificates()
- .stream()
- .map(cert -> new TrustAnchor(cert, null))
- .collect(Collectors.toSet());
- try {
- // We use the default JCE provider as there is no reason to use Bouncy Castle, moreover BC requires
- // the validated certificate to be in the certificate store which breaks the clean immutable usage of
- // trustedCACertificateCertStore in SubjectCertificateTrustedValidator.
- trustedCACertificateCertStore = CertStore.getInstance("Collection",
- new CollectionCertStoreParameters(configuration.getTrustedCACertificates()));
- } catch (GeneralSecurityException e) {
- throw new JceException(e);
+ if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) {
+ ocspClient = OcspClientImpl.build(configuration.getOcspRequestTimeout());
+ ocspServiceProvider = new OcspServiceProvider(
+ configuration.getDesignatedOcspServiceConfiguration(),
+ new AiaOcspServiceConfiguration(configuration.getNonceDisabledOcspUrls(),
+ trustedCACertificateAnchors,
+ trustedCACertificateCertStore));
}
}
@@ -153,7 +143,7 @@ private ValidatorBatch getCertTrustValidators() {
return ValidatorBatch.createFrom(
certTrustedValidator::validateCertificateTrusted
).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(),
- new SubjectCertificateNotRevokedValidator(certTrustedValidator, httpClientSupplier.get(), configuration.getNonceDisabledOcspUrls())::validateCertificateNotRevoked
+ new SubjectCertificateNotRevokedValidator(certTrustedValidator, ocspClient, ocspServiceProvider)::validateCertificateNotRevoked
);
}
}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/Digester.java b/src/main/java/org/webeid/security/validator/ocsp/Digester.java
index feb7248c..777b6b58 100644
--- a/src/main/java/org/webeid/security/validator/ocsp/Digester.java
+++ b/src/main/java/org/webeid/security/validator/ocsp/Digester.java
@@ -1,25 +1,3 @@
-/*
- * Copyright (c) 2020 The Web eID Project
- *
- * 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.
- */
-
/*
* Copyright 2017 The Netty Project
* Copyright 2020 The Web eID project
diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java b/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java
new file mode 100644
index 00000000..7b5ad6d6
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp;
+
+import org.bouncycastle.cert.ocsp.OCSPReq;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+
+import java.io.IOException;
+import java.net.URI;
+
+public interface OcspClient {
+
+ OCSPResp request(URI url, OCSPReq request) throws IOException;
+
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java b/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java
new file mode 100644
index 00000000..cb63cba4
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp;
+
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.bouncycastle.cert.ocsp.OCSPReq;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import java.util.Objects;
+
+public class OcspClientImpl implements OcspClient {
+
+ private static final Logger LOG = LoggerFactory.getLogger(OcspClientImpl.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(
+ new OkHttpClient.Builder()
+ .connectTimeout(ocspRequestTimeout)
+ .callTimeout(ocspRequestTimeout)
+ .build()
+ );
+ }
+
+ /**
+ * Use OkHttpClient to fetch the OCSP response from the OCSP responder service.
+ *
+ * @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.
+ */
+ @Override
+ public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException {
+ final RequestBody requestBody = RequestBody.create(ocspReq.getEncoded(), OCSP_REQUEST_TYPE);
+ final Request request = new Request.Builder()
+ .url(uri.toURL())
+ .post(requestBody)
+ .build();
+
+ try (final Response response = httpClient.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("OCSP request was not successful, response: " + response);
+ } else {
+ LOG.debug("OCSP response: {}", response);
+ }
+ try (final ResponseBody responseBody = Objects.requireNonNull(response.body(), "response body")) {
+ Objects.requireNonNull(responseBody.contentType(), "response content type");
+ if (!OCSP_RESPONSE_TYPE.type().equals(responseBody.contentType().type()) ||
+ !OCSP_RESPONSE_TYPE.subtype().equals(responseBody.contentType().subtype())) {
+ throw new IOException("OCSP response content type is not " + OCSP_RESPONSE_TYPE);
+ }
+ return new OCSPResp(responseBody.bytes());
+ }
+ }
+ }
+
+ private OcspClientImpl(OkHttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java b/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java
index 3805cbb5..bfbb0dd1 100644
--- a/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java
+++ b/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java
@@ -1,28 +1,6 @@
-/*
- * Copyright (c) 2020 The Web eID Project
- *
- * 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.
- */
-
/*
* Copyright 2017 The Netty Project
- * Copyright 2020 The Web eID project
+ * Copyright (c) 2020-2021 Estonian Information System Authority
*
* The Netty Project and The Web eID Project license this file to you under the
* Apache License, version 2.0 (the "License"); you may not use this file except
@@ -43,18 +21,12 @@
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
-import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
-import org.bouncycastle.operator.DigestCalculator;
-import java.io.IOException;
-import java.math.BigInteger;
import java.security.SecureRandom;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
import java.util.Objects;
/**
@@ -66,29 +38,11 @@ public final class OcspRequestBuilder {
private static final SecureRandom GENERATOR = new SecureRandom();
- private SecureRandom randomGenerator = GENERATOR;
- private DigestCalculator digestCalculator = Digester.sha1();
- private X509Certificate subjectCertificate;
- private X509Certificate issuerCertificate;
private boolean ocspNonceEnabled = true;
+ private CertificateID certificateId;
- public OcspRequestBuilder generator(SecureRandom generator) {
- this.randomGenerator = generator;
- return this;
- }
-
- public OcspRequestBuilder calculator(DigestCalculator calculator) {
- this.digestCalculator = calculator;
- return this;
- }
-
- public OcspRequestBuilder certificate(X509Certificate certificate) {
- this.subjectCertificate = certificate;
- return this;
- }
-
- public OcspRequestBuilder issuer(X509Certificate issuer) {
- this.issuerCertificate = issuer;
+ public OcspRequestBuilder withCertificateId(CertificateID certificateId) {
+ this.certificateId = certificateId;
return this;
}
@@ -98,21 +52,12 @@ public OcspRequestBuilder enableOcspNonce(boolean ocspNonceEnabled) {
}
/**
- * ATTENTION: The returned {@link OCSPReq} is not re-usable/cacheable! It contains a one-time nonce
- * and CA's will (should) reject subsequent requests that have the same nonce value.
+ * The returned {@link OCSPReq} is not re-usable/cacheable. It contains a one-time nonce
+ * and responders will reject subsequent requests that have the same nonce value.
*/
- public OCSPReq build() throws OCSPException, IOException, CertificateEncodingException {
- final DigestCalculator calculator = Objects.requireNonNull(this.digestCalculator, "digestCalculator");
- final X509Certificate certificate = Objects.requireNonNull(this.subjectCertificate, "subjectCertificate");
- final X509Certificate issuer = Objects.requireNonNull(this.issuerCertificate, "issuerCertificate");
-
- final BigInteger serial = certificate.getSerialNumber();
-
- final CertificateID certId = new CertificateID(calculator,
- new X509CertificateHolder(issuer.getEncoded()), serial);
-
+ public OCSPReq build() throws OCSPException {
final OCSPReqBuilder builder = new OCSPReqBuilder();
- builder.addRequest(certId);
+ builder.addRequest(Objects.requireNonNull(certificateId, "certificateId"));
if (ocspNonceEnabled) {
addNonce(builder);
@@ -122,16 +67,13 @@ public OCSPReq build() throws OCSPException, IOException, CertificateEncodingExc
}
private void addNonce(OCSPReqBuilder builder) {
- final SecureRandom generator = Objects.requireNonNull(this.randomGenerator, "randomGenerator");
-
final byte[] nonce = new byte[8];
- generator.nextBytes(nonce);
+ GENERATOR.nextBytes(nonce);
final Extension[] extensions = new Extension[]{
new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false,
new DEROctetString(nonce))
};
-
builder.setRequestExtensions(new Extensions(extensions));
}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspResponseValidator.java b/src/main/java/org/webeid/security/validator/ocsp/OcspResponseValidator.java
new file mode 100644
index 00000000..8f7610cc
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/OcspResponseValidator.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp;
+
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.ocsp.BasicOCSPResp;
+import org.bouncycastle.cert.ocsp.CertificateStatus;
+import org.bouncycastle.cert.ocsp.OCSPException;
+import org.bouncycastle.cert.ocsp.RevokedStatus;
+import org.bouncycastle.cert.ocsp.SingleResp;
+import org.bouncycastle.cert.ocsp.UnknownStatus;
+import org.bouncycastle.operator.ContentVerifierProvider;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
+import org.webeid.security.exceptions.OCSPCertificateException;
+import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
+import org.webeid.security.exceptions.UserCertificateRevokedException;
+
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Objects;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+public final class OcspResponseValidator {
+
+ /**
+ * Indicates that a X.509 Certificates corresponding private key may be used by an authority to sign OCSP responses.
+ *
+ * https://oidref.com/1.3.6.1.5.5.7.3.9
+ */
+ private static final String OID_OCSP_SIGNING = "1.3.6.1.5.5.7.3.9";
+
+ private static final long ALLOWED_TIME_SKEW = TimeUnit.MINUTES.toMillis(15);
+
+ public static void validateHasSigningExtension(X509Certificate certificate) throws OCSPCertificateException {
+ Objects.requireNonNull(certificate, "certificate");
+ try {
+ if (certificate.getExtendedKeyUsage() == null || !certificate.getExtendedKeyUsage().contains(OID_OCSP_SIGNING)) {
+ throw new OCSPCertificateException("Certificate " + certificate.getSubjectDN() +
+ " does not contain the key usage extension for OCSP response signing");
+ }
+ } catch (CertificateParsingException e) {
+ throw new OCSPCertificateException("Certificate parsing failed:", e);
+ }
+ }
+
+ public static void validateResponseSignature(BasicOCSPResp basicResponse, X509CertificateHolder responderCert) throws CertificateException, OperatorCreationException, OCSPException, UserCertificateOCSPCheckFailedException {
+ final ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder()
+ .setProvider("BC")
+ .build(responderCert);
+ if (!basicResponse.isSignatureValid(verifierProvider)) {
+ throw new UserCertificateOCSPCheckFailedException("OCSP response signature is invalid");
+ }
+ }
+
+ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Date producedAt) throws UserCertificateOCSPCheckFailedException {
+ // From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt:
+ // 4.2.2. Notes on OCSP Responses
+ // 4.2.2.1. Time
+ // Responses whose nextUpdate value is earlier than
+ // the local system time value SHOULD be considered unreliable.
+ // Responses whose thisUpdate time is later than the local system time
+ // SHOULD be considered unreliable.
+ // If nextUpdate is not set, the responder is indicating that newer
+ // revocation information is available all the time.
+ final Date notAllowedBefore = new Date(producedAt.getTime() - ALLOWED_TIME_SKEW);
+ final Date notAllowedAfter = new Date(producedAt.getTime() + ALLOWED_TIME_SKEW);
+ if (notAllowedAfter.before(certStatusResponse.getThisUpdate()) ||
+ notAllowedBefore.after(certStatusResponse.getNextUpdate() != null ?
+ certStatusResponse.getNextUpdate() :
+ certStatusResponse.getThisUpdate())) {
+ throw new UserCertificateOCSPCheckFailedException("Certificate status update time check failed: " +
+ "notAllowedBefore: " + toUtcString(notAllowedBefore) +
+ ", notAllowedAfter: " + toUtcString(notAllowedAfter) +
+ ", thisUpdate: " + toUtcString(certStatusResponse.getThisUpdate()) +
+ ", nextUpdate: " + toUtcString(certStatusResponse.getNextUpdate()));
+ }
+ }
+
+ public static void validateSubjectCertificateStatus(SingleResp certStatusResponse) throws UserCertificateRevokedException {
+ final CertificateStatus status = certStatusResponse.getCertStatus();
+ if (status == null) {
+ return;
+ }
+ if (status instanceof RevokedStatus) {
+ RevokedStatus revokedStatus = (RevokedStatus) status;
+ throw (revokedStatus.hasRevocationReason() ?
+ new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason()) :
+ new UserCertificateRevokedException());
+ } else if (status instanceof UnknownStatus) {
+ throw new UserCertificateRevokedException("Unknown status");
+ } else {
+ throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown");
+ }
+ }
+
+ private static String toUtcString(Date date) {
+ if (date == null) {
+ return String.valueOf((Object) null);
+ }
+ final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
+ dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return dateFormatter.format(date);
+ }
+
+ private OcspResponseValidator() {
+ throw new IllegalStateException("Utility class");
+ }
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java b/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java
new file mode 100644
index 00000000..b85831a6
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp;
+
+import org.webeid.security.exceptions.TokenValidationException;
+import org.webeid.security.validator.ocsp.service.AiaOcspService;
+import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration;
+import org.webeid.security.validator.ocsp.service.DesignatedOcspService;
+import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration;
+import org.webeid.security.validator.ocsp.service.OcspService;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Objects;
+
+public class OcspServiceProvider {
+
+ private final DesignatedOcspService designatedOcspService;
+ private final AiaOcspServiceConfiguration aiaOcspServiceConfiguration;
+
+ public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration, AiaOcspServiceConfiguration aiaOcspServiceConfiguration) {
+ designatedOcspService = designatedOcspServiceConfiguration != null ?
+ new DesignatedOcspService(designatedOcspServiceConfiguration)
+ : null;
+ this.aiaOcspServiceConfiguration = Objects.requireNonNull(aiaOcspServiceConfiguration, "aiaOcspServiceConfiguration");
+ }
+
+ /**
+ * A static factory method that returns either the designated or AIA OCSP service instance depending on whether
+ * the designated OCSP service is configured and supports the issuer of the certificate.
+ *
+ * @param certificate subject certificate that is to be checked with OCSP
+ * @return either the designated or AIA OCSP service instance
+ * @throws TokenValidationException when AIA URL is not found in certificate
+ * @throws CertificateEncodingException when certificate is invalid
+ */
+ public OcspService getService(X509Certificate certificate) throws TokenValidationException, CertificateEncodingException {
+ if (designatedOcspService != null && designatedOcspService.supportsIssuerOf(certificate)) {
+ return designatedOcspService;
+ }
+ return new AiaOcspService(aiaOcspServiceConfiguration, certificate);
+ }
+
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java b/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java
new file mode 100644
index 00000000..ca8fcc83
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp;
+
+import org.bouncycastle.asn1.ASN1String;
+import org.bouncycastle.asn1.x509.AccessDescription;
+import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.cert.X509CertificateHolder;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Objects;
+
+public final class OcspUrl {
+
+ public static final URI AIA_ESTEID_2015 = URI.create("http://aia.sk.ee/esteid2015");
+
+ /**
+ * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one.
+ */
+ public static URI getOcspUri(X509Certificate certificate) {
+ Objects.requireNonNull(certificate, "certificate");
+ final X509CertificateHolder certificateHolder;
+ try {
+ certificateHolder = new X509CertificateHolder(certificate.getEncoded());
+ final AuthorityInformationAccess authorityInformationAccess =
+ AuthorityInformationAccess.fromExtensions(certificateHolder.getExtensions());
+ for (AccessDescription accessDescription :
+ authorityInformationAccess.getAccessDescriptions()) {
+ if (accessDescription.getAccessMethod().equals(AccessDescription.id_ad_ocsp) &&
+ accessDescription.getAccessLocation().getTagNo() == GeneralName.uniformResourceIdentifier) {
+ final String accessLocationUrl = ((ASN1String) accessDescription.getAccessLocation().getName())
+ .getString();
+ return URI.create(accessLocationUrl);
+ }
+ }
+ } catch (IOException | CertificateEncodingException | IllegalArgumentException | NullPointerException e) {
+ return null;
+ }
+ return null;
+ }
+
+ private OcspUrl() {
+ throw new IllegalStateException("Utility class");
+ }
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java b/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java
deleted file mode 100644
index 168cf369..00000000
--- a/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (c) 2020 The Web eID Project
- *
- * 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.
- */
-
-/*
- * Copyright 2017 The Netty Project
- * Copyright 2020 The Web eID project
- *
- * The Netty Project and The Web eID Project license this file to you under the
- * Apache License, version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package org.webeid.security.validator.ocsp;
-
-import okhttp3.*;
-import org.bouncycastle.asn1.*;
-import org.bouncycastle.asn1.x509.Extension;
-import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
-import org.bouncycastle.cert.ocsp.OCSPReq;
-import org.bouncycastle.cert.ocsp.OCSPResp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.security.cert.X509Certificate;
-import java.util.Objects;
-
-public final class OcspUtils {
-
- private static final Logger LOG = LoggerFactory.getLogger(OcspUtils.class);
-
- /**
- * The OID for OCSP responder URLs.
- *
- * http://www.alvestrand.no/objectid/1.3.6.1.5.5.7.48.1.html
- */
- private static final ASN1ObjectIdentifier OCSP_RESPONDER_OID
- = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1").intern();
-
- 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 OcspUtils() {
- }
-
- /**
- * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one.
- */
- public static URI ocspUri(X509Certificate certificate) throws IOException {
- final byte[] value = certificate.getExtensionValue(Extension.authorityInfoAccess.getId());
- if (value == null) {
- return null;
- }
-
- final ASN1Primitive authorityInfoAccess = JcaX509ExtensionUtils.parseExtensionValue(value);
- if (!(authorityInfoAccess instanceof DLSequence)) {
- return null;
- }
-
- final DLSequence aiaSequence = (DLSequence) authorityInfoAccess;
- final DLTaggedObject taggedObject = findObject(aiaSequence, OCSP_RESPONDER_OID, DLTaggedObject.class);
- if (taggedObject == null) {
- return null;
- }
-
- if (taggedObject.getTagNo() != BERTags.OBJECT_IDENTIFIER) {
- return null;
- }
-
- final byte[] encoded = taggedObject.getEncoded();
- int length = (int) encoded[1] & 0xFF;
- final String uri = new String(encoded, 2, length, StandardCharsets.UTF_8);
- return URI.create(uri);
- }
-
- private static T findObject(DLSequence sequence, ASN1ObjectIdentifier oid, Class type) {
- for (final ASN1Encodable element : sequence) {
- if (!(element instanceof DLSequence)) {
- continue;
- }
-
- final DLSequence subSequence = (DLSequence) element;
- if (subSequence.size() != 2) {
- continue;
- }
-
- final ASN1Encodable key = subSequence.getObjectAt(0);
- final ASN1Encodable value = subSequence.getObjectAt(1);
-
- if (key.equals(oid) && type.isInstance(value)) {
- return type.cast(value);
- }
- }
-
- return null;
- }
-
- /**
- * Use OkHttpClient to fetch the OCSP response from the CA's OCSP responder server.
- *
- * @param uri OCSP server URL
- * @param ocspReq OCSP request
- * @param httpClient OkHttpClient instance
- * @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.
- */
- public static OCSPResp request(URI uri, OCSPReq ocspReq, OkHttpClient httpClient) throws IOException {
- final RequestBody requestBody = RequestBody.create(ocspReq.getEncoded(), OCSP_REQUEST_TYPE);
- final Request request = new Request.Builder()
- .url(uri.toURL())
- .post(requestBody)
- .build();
-
- try (final Response response = httpClient.newCall(request).execute()) {
- if (!response.isSuccessful()) {
- throw new IOException("OCSP request was not successful, response: " + response);
- } else {
- LOG.debug("OCSP response: {}", response);
- }
- try (final ResponseBody responseBody = Objects.requireNonNull(response.body())) {
- if (!OCSP_RESPONSE_TYPE.equals(responseBody.contentType())) {
- throw new IOException("OCSP response content type is not " + OCSP_RESPONSE_TYPE);
- }
- return new OCSPResp(responseBody.bytes());
- }
- }
- }
-
-}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java
new file mode 100644
index 00000000..4b8bef36
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp.service;
+
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.webeid.security.exceptions.OCSPCertificateException;
+import org.webeid.security.exceptions.TokenValidationException;
+import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
+
+import java.net.URI;
+import java.security.cert.CertStore;
+import java.security.cert.CertificateException;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.Objects;
+import java.util.Set;
+
+import static org.webeid.security.certificate.CertificateValidator.certificateIsValidOnDate;
+import static org.webeid.security.certificate.CertificateValidator.validateIsSignedByTrustedCA;
+import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateHasSigningExtension;
+import static org.webeid.security.validator.ocsp.OcspUrl.getOcspUri;
+
+/**
+ * An OCSP service that uses the responders from the Certificates' Authority Information Access (AIA) extension.
+ */
+public class AiaOcspService implements OcspService {
+
+ private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
+ private final Set trustedCACertificateAnchors;
+ private final CertStore trustedCACertificateCertStore;
+ private final URI url;
+ private final boolean supportsNonce;
+
+ public AiaOcspService(AiaOcspServiceConfiguration configuration, X509Certificate certificate) throws TokenValidationException {
+ Objects.requireNonNull(configuration);
+ this.trustedCACertificateAnchors = configuration.getTrustedCACertificateAnchors();
+ this.trustedCACertificateCertStore = configuration.getTrustedCACertificateCertStore();
+ this.url = getOcspAiaUrlFromCertificate(Objects.requireNonNull(certificate));
+ this.supportsNonce = !configuration.getNonceDisabledOcspUrls().contains(this.url);
+ }
+
+ @Override
+ public boolean doesSupportNonce() {
+ return supportsNonce;
+ }
+
+ @Override
+ public URI getAccessLocation() {
+ return url;
+ }
+
+ @Override
+ public void validateResponderCertificate(X509CertificateHolder cert, Date producedAt) throws TokenValidationException {
+ try {
+ final X509Certificate certificate = certificateConverter.getCertificate(cert);
+ certificateIsValidOnDate(certificate, producedAt, "AIA OCSP responder");
+ // Trusted certificates' validity has been already verified in validateCertificateExpiry().
+ validateHasSigningExtension(certificate);
+ validateIsSignedByTrustedCA(certificate, trustedCACertificateAnchors, trustedCACertificateCertStore);
+ } catch (CertificateException e) {
+ throw new OCSPCertificateException("Invalid responder certificate", e);
+ }
+ }
+
+ private static URI getOcspAiaUrlFromCertificate(X509Certificate certificate) throws TokenValidationException {
+ final URI uri = getOcspUri(certificate);
+ if (uri == null) {
+ throw new UserCertificateOCSPCheckFailedException("Getting the AIA OCSP responder field from the certificate failed");
+ }
+ return uri;
+ }
+
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java
new file mode 100644
index 00000000..128b405e
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp.service;
+
+import java.net.URI;
+import java.security.cert.CertStore;
+import java.security.cert.TrustAnchor;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+
+public class AiaOcspServiceConfiguration {
+
+ private final Collection nonceDisabledOcspUrls;
+ private final Set trustedCACertificateAnchors;
+ private final CertStore trustedCACertificateCertStore;
+
+ public AiaOcspServiceConfiguration(Collection nonceDisabledOcspUrls, Set trustedCACertificateAnchors, CertStore trustedCACertificateCertStore) {
+ this.nonceDisabledOcspUrls = Objects.requireNonNull(nonceDisabledOcspUrls);
+ this.trustedCACertificateAnchors = Objects.requireNonNull(trustedCACertificateAnchors);
+ this.trustedCACertificateCertStore = Objects.requireNonNull(trustedCACertificateCertStore);
+ }
+
+ public Collection getNonceDisabledOcspUrls() {
+ return nonceDisabledOcspUrls;
+ }
+
+ public Set getTrustedCACertificateAnchors() {
+ return trustedCACertificateAnchors;
+ }
+
+ public CertStore getTrustedCACertificateCertStore() {
+ return trustedCACertificateCertStore;
+ }
+
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java
new file mode 100644
index 00000000..d7220ffb
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp.service;
+
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.webeid.security.exceptions.OCSPCertificateException;
+import org.webeid.security.exceptions.TokenValidationException;
+
+import java.net.URI;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.Objects;
+
+import static org.webeid.security.certificate.CertificateValidator.certificateIsValidOnDate;
+
+/**
+ * An OCSP service that uses a single designated OCSP responder.
+ */
+public class DesignatedOcspService implements OcspService {
+
+ private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
+ private final DesignatedOcspServiceConfiguration configuration;
+
+ public DesignatedOcspService(DesignatedOcspServiceConfiguration configuration) {
+ this.configuration = Objects.requireNonNull(configuration, "configuration");
+ }
+
+ @Override
+ public boolean doesSupportNonce() {
+ return configuration.doesSupportNonce();
+ }
+
+ @Override
+ public URI getAccessLocation() {
+ return configuration.getOcspServiceAccessLocation();
+ }
+
+ @Override
+ public void validateResponderCertificate(X509CertificateHolder cert, Date producedAt) throws TokenValidationException {
+ try {
+ final X509Certificate responderCertificate = certificateConverter.getCertificate(cert);
+ // Certificate pinning is implemented simply by comparing the certificates or their public keys,
+ // see https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning.
+ if (!configuration.getResponderCertificate().equals(responderCertificate)) {
+ throw new OCSPCertificateException("Responder certificate from the OCSP response is not equal to " +
+ "the configured designated OCSP responder certificate");
+ }
+ certificateIsValidOnDate(responderCertificate, producedAt, "Designated OCSP responder");
+ } catch (CertificateException e) {
+ throw new OCSPCertificateException("X509CertificateHolder conversion to X509Certificate failed");
+ }
+ }
+
+ public boolean supportsIssuerOf(X509Certificate certificate) throws CertificateEncodingException {
+ return configuration.supportsIssuerOf(certificate);
+ }
+
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java
new file mode 100644
index 00000000..029905b6
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp.service;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.webeid.security.exceptions.OCSPCertificateException;
+
+import java.net.URI;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateHasSigningExtension;
+
+public class DesignatedOcspServiceConfiguration {
+
+ private final URI ocspServiceAccessLocation;
+ private final X509Certificate responderCertificate;
+ private final boolean doesSupportNonce;
+ private final Collection supportedIssuers;
+
+ /**
+ * Configuration of a designated OCSP service.
+ *
+ * @param ocspServiceAccessLocation the URL where the service is located
+ * @param responderCertificate the service's OCSP responder certificate
+ * @param supportedCertificateIssuers the certificate issuers supported by the service
+ * @param doesSupportNonce true if the service supports the OCSP protocol nonce extension
+ * @throws OCSPCertificateException when an error occurs while extracting issuer names from certificates
+ */
+ public DesignatedOcspServiceConfiguration(URI ocspServiceAccessLocation, X509Certificate responderCertificate, Collection supportedCertificateIssuers, boolean doesSupportNonce) throws OCSPCertificateException {
+ this.ocspServiceAccessLocation = Objects.requireNonNull(ocspServiceAccessLocation, "OCSP service access location");
+ this.responderCertificate = Objects.requireNonNull(responderCertificate, "OCSP responder certificate");
+ this.supportedIssuers = getIssuerX500Names(Objects.requireNonNull(supportedCertificateIssuers, "supported issuers"));
+ validateHasSigningExtension(responderCertificate);
+ this.doesSupportNonce = doesSupportNonce;
+ }
+
+ public URI getOcspServiceAccessLocation() {
+ return ocspServiceAccessLocation;
+ }
+
+ public X509Certificate getResponderCertificate() {
+ return responderCertificate;
+ }
+
+ public boolean doesSupportNonce() {
+ return doesSupportNonce;
+ }
+
+ public boolean supportsIssuerOf(X509Certificate certificate) throws CertificateEncodingException {
+ return supportedIssuers.contains(new JcaX509CertificateHolder(Objects.requireNonNull(certificate)).getIssuer());
+ }
+
+ private Collection getIssuerX500Names(Collection supportedIssuers) throws OCSPCertificateException {
+ try {
+ return supportedIssuers.stream()
+ .map(this::getSubject)
+ .collect(Collectors.toList());
+ } catch (IllegalArgumentException e) {
+ throw new OCSPCertificateException("Supported issuer list contains an invalid certificate", e.getCause());
+ }
+ }
+
+ private X500Name getSubject(X509Certificate certificate) throws IllegalArgumentException {
+ try {
+ return new JcaX509CertificateHolder(certificate).getSubject();
+ } catch (CertificateEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java b/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java
new file mode 100644
index 00000000..d098b440
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.ocsp.service;
+
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.webeid.security.exceptions.TokenValidationException;
+
+import java.net.URI;
+import java.util.Date;
+
+public interface OcspService {
+
+ boolean doesSupportNonce();
+
+ URI getAccessLocation();
+
+ void validateResponderCertificate(X509CertificateHolder cert, Date date) throws TokenValidationException;
+
+}
diff --git a/src/main/java/org/webeid/security/validator/validators/CertificateExpiryValidator.java b/src/main/java/org/webeid/security/validator/validators/CertificateExpiryValidator.java
new file mode 100644
index 00000000..1e3d44f0
--- /dev/null
+++ b/src/main/java/org/webeid/security/validator/validators/CertificateExpiryValidator.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2020-2021 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 org.webeid.security.validator.validators;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.webeid.security.exceptions.TokenValidationException;
+import org.webeid.security.validator.AuthTokenValidatorData;
+
+import java.security.cert.TrustAnchor;
+import java.util.Date;
+import java.util.Set;
+
+import static org.webeid.security.certificate.CertificateValidator.certificateIsValidOnDate;
+import static org.webeid.security.certificate.CertificateValidator.trustedCACertificatesAreValidOnDate;
+
+public final class CertificateExpiryValidator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CertificateExpiryValidator.class);
+
+ private final Set trustedCACertificateAnchors;
+
+ public CertificateExpiryValidator(Set trustedCACertificateAnchors) {
+ this.trustedCACertificateAnchors = trustedCACertificateAnchors;
+ }
+
+ /**
+ * Checks the validity of the user certificate from the authentication token
+ * and the validity of trusted CA certificates.
+ *
+ * @param actualTokenData authentication token data that contains the user certificate
+ * @throws TokenValidationException when a CA certificate or the user certificate is expired or not yet valid
+ */
+ public void validateCertificateExpiry(AuthTokenValidatorData actualTokenData) throws TokenValidationException {
+ // Use JJWT Clock interface so that the date can be mocked in tests.
+ final Date now = SubjectCertificatePurposeValidator.DefaultClock.INSTANCE.now();
+ trustedCACertificatesAreValidOnDate(trustedCACertificateAnchors, now);
+ LOG.debug("CA certificates are valid.");
+ certificateIsValidOnDate(actualTokenData.getSubjectCertificate(), now, "User");
+ LOG.debug("User certificate is valid.");
+ }
+
+}
diff --git a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java
index d72e52ba..aeaf21bd 100644
--- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java
+++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java
@@ -22,89 +22,188 @@
package org.webeid.security.validator.validators;
-import okhttp3.OkHttpClient;
+import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
-import org.bouncycastle.cert.ocsp.*;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.ocsp.BasicOCSPResp;
+import org.bouncycastle.cert.ocsp.CertificateID;
+import org.bouncycastle.cert.ocsp.OCSPException;
+import org.bouncycastle.cert.ocsp.OCSPReq;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+import org.bouncycastle.cert.ocsp.SingleResp;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.DigestCalculator;
+import org.bouncycastle.operator.OperatorCreationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.webeid.security.exceptions.TokenValidationException;
-import org.webeid.security.exceptions.UserCertificateRevocationCheckFailedException;
-import org.webeid.security.exceptions.UserCertificateRevokedException;
+import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
import org.webeid.security.validator.AuthTokenValidatorData;
+import org.webeid.security.validator.ocsp.Digester;
+import org.webeid.security.validator.ocsp.OcspClient;
import org.webeid.security.validator.ocsp.OcspRequestBuilder;
-import org.webeid.security.validator.ocsp.OcspUtils;
+import org.webeid.security.validator.ocsp.OcspServiceProvider;
+import org.webeid.security.validator.ocsp.service.OcspService;
import java.io.IOException;
-import java.net.URI;
+import java.math.BigInteger;
+import java.security.Security;
import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
-import java.util.Collection;
+import java.util.Date;
import java.util.Objects;
+import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateCertificateStatusUpdateTime;
+import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateResponseSignature;
+import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateSubjectCertificateStatus;
+
public final class SubjectCertificateNotRevokedValidator {
private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateNotRevokedValidator.class);
+ private static final DigestCalculator DIGEST_CALCULATOR = Digester.sha1();
private final SubjectCertificateTrustedValidator trustValidator;
- private final OkHttpClient httpClient;
- private final Collection nonceDisabledOcspUrls;
+ private final OcspClient ocspClient;
+ private final OcspServiceProvider ocspServiceProvider;
+
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
- public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator, OkHttpClient httpClient, Collection nonceDisabledOcspUrls) {
+ public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator,
+ OcspClient ocspClient,
+ OcspServiceProvider ocspServiceProvider) {
this.trustValidator = trustValidator;
- this.httpClient = httpClient;
- this.nonceDisabledOcspUrls = nonceDisabledOcspUrls;
+ this.ocspClient = ocspClient;
+ this.ocspServiceProvider = ocspServiceProvider;
}
/**
* Validates that the user certificate from the authentication token is not revoked with OCSP.
*
* @param actualTokenData authentication token data that contains the user certificate.
- * @throws TokenValidationException when user certificate is revoked.
+ * @throws TokenValidationException when user certificate is revoked or revocation check fails.
*/
public void validateCertificateNotRevoked(AuthTokenValidatorData actualTokenData) throws TokenValidationException {
try {
final X509Certificate certificate = actualTokenData.getSubjectCertificate();
- final URI uri = OcspUtils.ocspUri(certificate);
+ OcspService ocspService = ocspServiceProvider.getService(certificate);
- if (uri == null) {
- throw new UserCertificateRevocationCheckFailedException("The CA/certificate doesn't have an OCSP responder");
- }
- final boolean ocspNonceDisabled = nonceDisabledOcspUrls.contains(uri);
- if (ocspNonceDisabled) {
+ if (!ocspService.doesSupportNonce()) {
LOG.debug("Disabling OCSP nonce extension");
}
+ final CertificateID certificateId = getCertificateId(certificate,
+ Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerCertificate()));
+
final OCSPReq request = new OcspRequestBuilder()
- .certificate(certificate)
- .enableOcspNonce(!ocspNonceDisabled)
- .issuer(Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerCertificate()))
+ .withCertificateId(certificateId)
+ .enableOcspNonce(ocspService.doesSupportNonce())
.build();
LOG.debug("Sending OCSP request");
- final OCSPResp response = OcspUtils.request(uri, request, httpClient);
+ final OCSPResp response = Objects.requireNonNull(ocspClient.request(ocspService.getAccessLocation(), request));
if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) {
- throw new UserCertificateRevocationCheckFailedException("Response status: " + response.getStatus());
+ throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus()));
}
final BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject();
- final SingleResp first = basicResponse.getResponses()[0];
-
- final CertificateStatus status = first.getCertStatus();
-
- if (status instanceof RevokedStatus) {
- RevokedStatus revokedStatus = (RevokedStatus) status;
- throw (revokedStatus.hasRevocationReason() ?
- new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason()) :
- new UserCertificateRevokedException());
- } else if (status instanceof UnknownStatus) {
- throw new UserCertificateRevokedException("Unknown status");
- } else if (status == null) {
- LOG.debug("OCSP check result is GOOD");
- } else {
- throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown");
+ verifyOcspResponse(basicResponse, ocspService, certificateId);
+ if (ocspService.doesSupportNonce()) {
+ checkNonce(request, basicResponse);
}
- } catch (CertificateEncodingException | OCSPException | IOException e) {
- throw new UserCertificateRevocationCheckFailedException(e);
+ } catch (OCSPException | CertificateException | OperatorCreationException | IOException e) {
+ throw new UserCertificateOCSPCheckFailedException(e);
}
}
+
+ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId) throws TokenValidationException, OCSPException, CertificateException, OperatorCreationException {
+ // The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt.
+ //
+ // 3.2. Signed Response Acceptance Requirements
+ // Prior to accepting a signed response for a particular certificate as
+ // valid, OCSP clients SHALL confirm that:
+ //
+ // 1. The certificate identified in a received response corresponds to
+ // the certificate that was identified in the corresponding request.
+
+ // As we sent the request for only a single certificate, we expect only a single response.
+ if (basicResponse.getResponses().length != 1) {
+ throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one response, "
+ + "received " + basicResponse.getResponses().length + " responses instead");
+ }
+ final SingleResp certStatusResponse = basicResponse.getResponses()[0];
+ if (!requestCertificateId.equals(certStatusResponse.getCertID())) {
+ throw new UserCertificateOCSPCheckFailedException("OCSP responded with certificate ID that differs from the requested ID");
+ }
+
+ // 2. The signature on the response is valid.
+
+ // We assume that the responder includes its certificate in the certs field of the response
+ // that helps us to verify it. According to RFC 2560 this field is optional, but including it
+ // is standard practice.
+ if (basicResponse.getCerts().length != 1) {
+ throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one responder certificate, "
+ + "received " + basicResponse.getCerts().length + " certificates instead");
+ }
+ final X509CertificateHolder responderCert = basicResponse.getCerts()[0];
+ validateResponseSignature(basicResponse, responderCert);
+
+ // 3. The identity of the signer matches the intended recipient of the
+ // request.
+ //
+ // 4. The signer is currently authorized to provide a response for the
+ // certificate in question.
+
+ final Date producedAt = basicResponse.getProducedAt();
+ ocspService.validateResponderCertificate(responderCert, producedAt);
+
+ // 5. The time at which the status being indicated is known to be
+ // correct (thisUpdate) is sufficiently recent.
+ //
+ // 6. When available, the time at or before which newer information will
+ // be available about the status of the certificate (nextUpdate) is
+ // greater than the current time.
+
+ validateCertificateStatusUpdateTime(certStatusResponse, producedAt);
+
+ // Now we can accept the signed response as valid and validate the certificate status.
+ validateSubjectCertificateStatus(certStatusResponse);
+ LOG.debug("OCSP check result is GOOD");
+ }
+
+ private static void checkNonce(OCSPReq request, BasicOCSPResp response) throws UserCertificateOCSPCheckFailedException {
+ final Extension requestNonce = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
+ final Extension responseNonce = response.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
+ if (!requestNonce.equals(responseNonce)) {
+ throw new UserCertificateOCSPCheckFailedException("OCSP request and response nonces differ, " +
+ "possible replay attack");
+ }
+ }
+
+ private static CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws CertificateEncodingException, IOException, OCSPException {
+ final BigInteger serial = subjectCertificate.getSerialNumber();
+ return new CertificateID(DIGEST_CALCULATOR,
+ new X509CertificateHolder(issuerCertificate.getEncoded()), serial);
+ }
+
+ private static String ocspStatusToString(int status) {
+ switch (status) {
+ case OCSPResp.MALFORMED_REQUEST:
+ return "malformed request";
+ case OCSPResp.INTERNAL_ERROR:
+ return "internal error";
+ case OCSPResp.TRY_LATER:
+ return "service unavailable";
+ case OCSPResp.SIG_REQUIRED:
+ return "request signature missing";
+ case OCSPResp.UNAUTHORIZED:
+ return "unauthorized";
+ default:
+ return "unknown";
+ }
+ }
+
}
diff --git a/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePolicyValidator.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePolicyValidator.java
index 6f9c0334..fd0c78fa 100644
--- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePolicyValidator.java
+++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePolicyValidator.java
@@ -7,7 +7,7 @@
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.webeid.security.exceptions.TokenValidationException;
import org.webeid.security.exceptions.UserCertificateDisallowedPolicyException;
-import org.webeid.security.exceptions.UserCertificateInvalidPolicyException;
+import org.webeid.security.exceptions.UserCertificateParseException;
import org.webeid.security.validator.AuthTokenValidatorData;
import java.io.IOException;
@@ -16,7 +16,7 @@
import java.util.Collection;
import java.util.Optional;
-public class SubjectCertificatePolicyValidator {
+public final class SubjectCertificatePolicyValidator {
private final Collection disallowedSubjectCertificatePolicies;
@@ -29,7 +29,7 @@ public SubjectCertificatePolicyValidator(Collection disall
*
* @param actualTokenData authentication token data that contains the user certificate.
* @throws UserCertificateDisallowedPolicyException when user certificate policy does not match the configured policies.
- * @throws UserCertificateInvalidPolicyException when user certificate policy is invalid.
+ * @throws UserCertificateParseException when user certificate policy is invalid.
*/
public void validateCertificatePolicies(AuthTokenValidatorData actualTokenData) throws TokenValidationException {
final X509Certificate certificate = actualTokenData.getSubjectCertificate();
@@ -46,7 +46,7 @@ public void validateCertificatePolicies(AuthTokenValidatorData actualTokenData)
throw new UserCertificateDisallowedPolicyException();
}
} catch (IOException e) {
- throw new UserCertificateInvalidPolicyException();
+ throw new UserCertificateParseException(e);
}
}
}
diff --git a/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePurposeValidator.java
similarity index 69%
rename from src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java
rename to src/main/java/org/webeid/security/validator/validators/SubjectCertificatePurposeValidator.java
index 03e5a81e..bbf3ea0c 100644
--- a/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java
+++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePurposeValidator.java
@@ -25,38 +25,21 @@
import io.jsonwebtoken.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.webeid.security.exceptions.*;
+import org.webeid.security.exceptions.TokenValidationException;
+import org.webeid.security.exceptions.UserCertificateMissingPurposeException;
+import org.webeid.security.exceptions.UserCertificateParseException;
+import org.webeid.security.exceptions.UserCertificateWrongPurposeException;
import org.webeid.security.validator.AuthTokenValidatorData;
-import java.security.cert.CertificateExpiredException;
-import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertificateParsingException;
import java.util.Date;
import java.util.List;
-public final class FunctionalSubjectCertificateValidators {
+public final class SubjectCertificatePurposeValidator {
- private static final Logger LOG = LoggerFactory.getLogger(FunctionalSubjectCertificateValidators.class);
+ private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificatePurposeValidator.class);
private static final String EXTENDED_KEY_USAGE_CLIENT_AUTHENTICATION = "1.3.6.1.5.5.7.3.2";
- /**
- * Checks the validity of the user certificate from the authentication token.
- *
- * @param actualTokenData authentication token data that contains the user certificate
- * @throws TokenValidationException when the user certificate is expired or not yet valid
- */
- public static void validateCertificateExpiry(AuthTokenValidatorData actualTokenData) throws TokenValidationException {
- try {
- // Use JJWT Clock interface so that the date can be mocked in tests.
- actualTokenData.getSubjectCertificate().checkValidity(DefaultClock.INSTANCE.now());
- LOG.debug("User certificate is valid.");
- } catch (CertificateNotYetValidException e) {
- throw new UserCertificateNotYetValidException(e);
- } catch (CertificateExpiredException e) {
- throw new UserCertificateExpiredException(e);
- }
- }
-
/**
* Validates that the purpose of the user certificate from the authentication token contains client authentication.
*
@@ -78,7 +61,7 @@ public static void validateCertificatePurpose(AuthTokenValidatorData actualToken
}
}
- private FunctionalSubjectCertificateValidators() {
+ private SubjectCertificatePurposeValidator() {
throw new IllegalStateException("Functional class");
}
diff --git a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java
index b6497f52..d2993258 100644
--- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java
+++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java
@@ -24,15 +24,17 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.webeid.security.exceptions.JceException;
-import org.webeid.security.exceptions.UserCertificateNotTrustedException;
+import org.webeid.security.exceptions.CertificateNotTrustedException;
+import org.webeid.security.exceptions.TokenValidationException;
import org.webeid.security.validator.AuthTokenValidatorData;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.*;
+import java.security.cert.CertStore;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
import java.util.Set;
+import static org.webeid.security.certificate.CertificateValidator.validateIsSignedByTrustedCA;
+
public final class SubjectCertificateTrustedValidator {
private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateTrustedValidator.class);
@@ -50,32 +52,12 @@ public SubjectCertificateTrustedValidator(Set trustedCACertificateA
* Validates that the user certificate from the authentication token is signed by a trusted certificate authority.
*
* @param actualTokenData authentication token data that contains the user certificate.
- * @throws UserCertificateNotTrustedException when user certificate is not signed by a trusted CA or is valid after CA certificate.
+ * @throws CertificateNotTrustedException when user certificate is not signed by a trusted CA.
*/
- public void validateCertificateTrusted(AuthTokenValidatorData actualTokenData) throws UserCertificateNotTrustedException, JceException {
-
+ public void validateCertificateTrusted(AuthTokenValidatorData actualTokenData) throws TokenValidationException {
final X509Certificate certificate = actualTokenData.getSubjectCertificate();
-
- final X509CertSelector selector = new X509CertSelector();
- selector.setCertificate(certificate);
-
- try {
- final PKIXBuilderParameters pkixBuilderParameters = new PKIXBuilderParameters(trustedCACertificateAnchors, selector);
- pkixBuilderParameters.setRevocationEnabled(false);
- pkixBuilderParameters.addCertStore(trustedCACertificateCertStore);
-
- // See the comment in AuthTokenValidatorImpl constructor why we use the default JCE provider.
- final CertPathBuilder certPathBuilder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType());
- final PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) certPathBuilder.build(pkixBuilderParameters);
-
- subjectCertificateIssuerCertificate = result.getTrustAnchor().getTrustedCert();
-
- } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
- throw new JceException(e);
- } catch (CertPathBuilderException e) {
- LOG.trace("Error verifying signer's certificate {}: {}", certificate.getSubjectDN(), e);
- throw new UserCertificateNotTrustedException();
- }
+ subjectCertificateIssuerCertificate = validateIsSignedByTrustedCA(certificate, trustedCACertificateAnchors, trustedCACertificateCertStore);
+ LOG.debug("Subject certificate is signed by a trusted CA");
}
public X509Certificate getSubjectCertificateIssuerCertificate() {
diff --git a/src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java b/src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java
index 3f1f06e2..b3a68d7a 100644
--- a/src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java
+++ b/src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java
@@ -26,6 +26,7 @@
import org.webeid.security.exceptions.JceException;
import org.webeid.security.validator.AuthTokenValidator;
+import java.io.IOException;
import java.security.cert.CertificateException;
import static org.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidator;
@@ -40,7 +41,7 @@ public void setup() {
super.setup();
try {
validator = getAuthTokenValidator(cache);
- } catch (CertificateException | JceException e) {
+ } catch (CertificateException | JceException | IOException e) {
throw new RuntimeException(e);
}
}
diff --git a/src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java b/src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java
index d83ad223..e84fb801 100644
--- a/src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java
+++ b/src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java
@@ -26,6 +26,7 @@
import org.webeid.security.exceptions.JceException;
import org.webeid.security.validator.AuthTokenValidator;
+import java.io.IOException;
import java.security.cert.CertificateException;
import static org.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidator;
@@ -40,7 +41,7 @@ protected void setup() {
super.setup();
try {
validator = getAuthTokenValidator(cache);
- } catch (CertificateException | JceException e) {
+ } catch (CertificateException | JceException | IOException e) {
throw new RuntimeException(e);
}
}
diff --git a/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java b/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java
index 3439c5ba..93046e8c 100644
--- a/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java
+++ b/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java
@@ -23,27 +23,36 @@
package org.webeid.security.testutil;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.webeid.security.certificate.CertificateLoader;
import org.webeid.security.exceptions.JceException;
+import org.webeid.security.exceptions.OCSPCertificateException;
+import org.webeid.security.validator.AuthTokenValidationConfiguration;
import org.webeid.security.validator.AuthTokenValidator;
import org.webeid.security.validator.AuthTokenValidatorBuilder;
+import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration;
import javax.cache.Cache;
+import java.io.IOException;
import java.net.URI;
+import java.net.URISyntaxException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.ZonedDateTime;
+import static org.webeid.security.testutil.Certificates.getTestEsteid2018CA;
+import static org.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceConfiguration;
+
public final class AuthTokenValidators {
private static final String TOKEN_ORIGIN_URL = "https://ria.ee";
private static final ASN1ObjectIdentifier EST_IDEMIA_POLICY = new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.2.1");
- public static AuthTokenValidator getAuthTokenValidator(Cache cache) throws CertificateException, JceException {
+ public static AuthTokenValidator getAuthTokenValidator(Cache cache) throws CertificateException, JceException, IOException {
return getAuthTokenValidator(TOKEN_ORIGIN_URL, cache);
}
- public static AuthTokenValidator getAuthTokenValidator(String url, Cache cache) throws CertificateException, JceException {
+ public static AuthTokenValidator getAuthTokenValidator(String url, Cache cache) throws CertificateException, JceException, IOException {
return getAuthTokenValidator(url, cache, getCACertificates());
}
@@ -57,25 +66,33 @@ public static AuthTokenValidator getAuthTokenValidator(String url, Cache cache, String certFingerprint) throws CertificateException, JceException {
+ public static AuthTokenValidator getAuthTokenValidator(Cache cache, String certFingerprint) throws CertificateException, JceException, IOException {
return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates())
.withSiteCertificateSha256Fingerprint(certFingerprint)
.withoutUserCertificateRevocationCheckWithOcsp()
.build();
}
- public static AuthTokenValidator getAuthTokenValidatorWithOcspCheck(Cache cache) throws CertificateException, JceException {
- return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates()).build();
+ public static AuthTokenValidator getAuthTokenValidatorWithOcspCheck(Cache cache) throws CertificateException, JceException, URISyntaxException, IOException {
+ return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates())
+ .build();
+ }
+
+ public static AuthTokenValidator getAuthTokenValidatorWithDesignatedOcspCheck(Cache cache) throws CertificateException, JceException, URISyntaxException, IOException, OCSPCertificateException {
+ return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates())
+ .withDesignatedOcspServiceConfiguration(getDesignatedOcspServiceConfiguration())
+ .build();
}
- public static AuthTokenValidator getAuthTokenValidatorWithWrongTrustedCA(Cache cache) throws CertificateException, JceException {
+ public static AuthTokenValidator getAuthTokenValidatorWithWrongTrustedCA(Cache cache) throws CertificateException, JceException, IOException {
return getAuthTokenValidator(TOKEN_ORIGIN_URL, cache,
CertificateLoader.loadCertificatesFromResources("ESTEID2018.cer"));
}
- public static AuthTokenValidator getAuthTokenValidatorWithDisallowedESTEIDPolicy(Cache cache) throws CertificateException, JceException {
+ public static AuthTokenValidator getAuthTokenValidatorWithDisallowedESTEIDPolicy(Cache cache) throws CertificateException, JceException, IOException {
return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates())
.withDisallowedCertificatePolicies(EST_IDEMIA_POLICY)
+ .withoutUserCertificateRevocationCheckWithOcsp()
.build();
}
@@ -86,7 +103,7 @@ private static AuthTokenValidatorBuilder getAuthTokenValidatorBuilder(String uri
.withTrustedCertificateAuthorities(certificates);
}
- private static X509Certificate[] getCACertificates() throws CertificateException {
+ private static X509Certificate[] getCACertificates() throws CertificateException, IOException {
return CertificateLoader.loadCertificatesFromResources("TEST_of_ESTEID2018.cer");
}
diff --git a/src/test/java/org/webeid/security/testutil/Certificates.java b/src/test/java/org/webeid/security/testutil/Certificates.java
new file mode 100644
index 00000000..1e5cbe48
--- /dev/null
+++ b/src/test/java/org/webeid/security/testutil/Certificates.java
@@ -0,0 +1,64 @@
+package org.webeid.security.testutil;
+
+import org.webeid.security.certificate.CertificateLoader;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import static org.webeid.security.certificate.CertificateLoader.loadCertificatesFromResources;
+
+public class Certificates {
+
+ private static final String JAAK_KRISTJAN_ESTEID2018_CERT = "MIIEAzCCA2WgAwIBAgIQOWkBWXNDJm1byFd3XsWkvjAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE4MTAxODA5NTA0N1oXDTIzMTAxNzIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR5k1lXzvSeI9O/1s1pZvjhEW8nItJoG0EBFxmLEY6S7ki1vF2Q3TEDx6dNztI1Xtx96cs8r4zYTwdiQoDg7k3diUuR9nTWGxQEMO1FDo4Y9fAmiPGWT++GuOVoZQY3XxijggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFOQsvTQJEBVMMSmhyZX5bibYJubAMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgH1UsmMdtLZti51Fq2QR4wUkAwpsnhsBV2HQqUXFYBJ7EXnLCkaXjdZKkHpABfM0QEx7UUhaI4i53jiJ7E1Y7WOAAJBDX4z61pniHJapI1bkMIiJQ/ti7ha8fdJSMSpAds5CyHIyHkQzWlVy86f9mA7Eu3oRO/1q+eFUzDbNN3Vvy7gQWQ=";
+ private static final String MARILIIS_ESTEID2015_CERT = "MIIFwjCCA6qgAwIBAgIQY+LgQ6n0BURZ048wIEiYHjANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2YgRVNURUlELVNLIDIwMTUwHhcNMTcxMDAzMTMyMjU2WhcNMjIxMDAyMjA1OTU5WjCBnjELMAkGA1UEBhMCRUUxDzANBgNVBAoMBkVTVEVJRDEaMBgGA1UECwwRZGlnaXRhbCBzaWduYXR1cmUxJjAkBgNVBAMMHU3DhE5OSUssTUFSSS1MSUlTLDYxNzEwMDMwMTYzMRAwDgYDVQQEDAdNw4ROTklLMRIwEAYDVQQqDAlNQVJJLUxJSVMxFDASBgNVBAUTCzYxNzEwMDMwMTYzMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+nNdtmZ2Ve3XXtjBEGwpvVrDIg7slPfLlyHbCBFMXevfqW5KsXIOy6E2A+Yof+/cqRlY4IhsX2Ka9SsJSo8/EekasFasLFPw9ZBE3MG0nn5zaatg45VSjnPinMmrzFzxo4IB2jCCAdYwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwgYsGA1UdIASBgzCBgDBzBgkrBgEEAc4fAwEwZjAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvcmVwb3NpdG9vcml1bS9DUFMwMwYIKwYBBQUHAgIwJwwlQWludWx0IHRlc3RpbWlzZWtzLiBPbmx5IGZvciB0ZXN0aW5nLjAJBgcEAIvsQAECMB0GA1UdDgQWBBTiw6M0uow+u6sfhgJAWCSvtkB/ejAiBggrBgEFBQcBAwQWMBQwCAYGBACORgEBMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBRJwPJEOWXVm0Y7DThgg7HWLSiGpjCBgwYIKwYBBQUHAQEEdzB1MCwGCCsGAQUFBzABhiBodHRwOi8vYWlhLmRlbW8uc2suZWUvZXN0ZWlkMjAxNTBFBggrBgEFBQcwAoY5aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FU1RFSUQtU0tfMjAxNS5kZXIuY3J0MEEGA1UdHwQ6MDgwNqA0oDKGMGh0dHA6Ly93d3cuc2suZWUvY3Jscy9lc3RlaWQvdGVzdF9lc3RlaWQyMDE1LmNybDANBgkqhkiG9w0BAQsFAAOCAgEAEWBdwmzo/yRncJXKvrE+A1G6yQaBNarKectI5uk18BewYEA4QkhmIwOCwD83jBDB9JF+kuODMHsnvz2mfhwaB/uJIPwfBDQ5JCMBdHPsxLN9nzW/UUzqv2UDMwFkibHCcfV5lTBcmOd7FagUHTUm+8gRlWbDiVl5yPochdJgGYPV+fs/jc5ttHaBvBon0z9LbI4qi0VXdRmV0iogErh8JF5yfGkbfGRaMkWkNYQtQ68i/hPe6MaUxL2/MMt4YTyXtVghmc3ZKZIyp4j0+jlK4vL+d4gaE+TvoQvh6HrmP145FqlMDurATWdB069+hdDLO5fI6AYkc79D5XPKwQ/f1MBufLtBYtOJmtpLT+tdBt/EqOEIO/0FeHcXZlFioNMuxBBeTE/QcDtJ2jxTcg8jNOoepS0wjuxBon9iI1710SR53DLGSWdL52lPoBFacnyPQI1htXVUkJ8icMQKYe3BLt1Ha2cvsA4n4IpjqVROX4mzoPL1hg/aJlD+W2uI2ppYRUNY5FX7C0R+AYzMpOahQ7STQfUxtEnKW98e1I33LWwpjJW9q4htsZeXs4Zatf9ssfUW0VA49tnI28kkN2D8aw1NgWfzVlnJKkEj0qa3ewLZK577j8MexAetT/7leH6mqewr9ewC/tKbYjhufieXx6RPcRC4OZsxtii7ih8TqRg=";
+
+ private static X509Certificate testEsteid2018CA;
+ private static X509Certificate testEsteid2015CA;
+
+ private static X509Certificate jaakKristjanEsteid2018Cert;
+ private static X509Certificate mariliisEsteid2015Cert;
+ private static X509Certificate testSkOcspResponder2020;
+
+ static void loadCertificates() throws CertificateException, IOException {
+ X509Certificate[] certificates = loadCertificatesFromResources("TEST_of_ESTEID-SK_2015.cer", "TEST_of_ESTEID2018.cer", "TEST_of_SK_OCSP_RESPONDER_2020.cer");
+ testEsteid2015CA = certificates[0];
+ testEsteid2018CA = certificates[1];
+ testSkOcspResponder2020 = certificates[2];
+ }
+
+ public static X509Certificate getTestEsteid2018CA() throws CertificateException, IOException {
+ if (testEsteid2018CA == null) {
+ loadCertificates();
+ }
+ return testEsteid2018CA;
+ }
+
+ public static X509Certificate getTestEsteid2015CA() throws CertificateException, IOException {
+ if (testEsteid2015CA == null) {
+ loadCertificates();
+ }
+ return testEsteid2015CA;
+ }
+
+ public static X509Certificate getTestSkOcspResponder2020() throws CertificateException, IOException {
+ if (testSkOcspResponder2020 == null) {
+ loadCertificates();
+ }
+ return testSkOcspResponder2020;
+ }
+
+ public static X509Certificate getJaakKristjanEsteid2018Cert() throws CertificateException, IOException {
+ if (jaakKristjanEsteid2018Cert == null) {
+ jaakKristjanEsteid2018Cert = CertificateLoader.loadCertificateFromBase64String(JAAK_KRISTJAN_ESTEID2018_CERT);
+ }
+ return jaakKristjanEsteid2018Cert;
+ }
+
+ public static X509Certificate getMariliisEsteid2015Cert() throws CertificateException, IOException {
+ if (mariliisEsteid2015Cert == null) {
+ mariliisEsteid2015Cert = CertificateLoader.loadCertificateFromBase64String(MARILIIS_ESTEID2015_CERT);
+ }
+ return mariliisEsteid2015Cert;
+ }
+}
diff --git a/src/test/java/org/webeid/security/testutil/Dates.java b/src/test/java/org/webeid/security/testutil/Dates.java
index 3dc5a378..ed57570c 100644
--- a/src/test/java/org/webeid/security/testutil/Dates.java
+++ b/src/test/java/org/webeid/security/testutil/Dates.java
@@ -25,7 +25,7 @@
import com.fasterxml.jackson.databind.util.StdDateFormat;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.impl.DefaultClock;
-import org.webeid.security.validator.validators.FunctionalSubjectCertificateValidators;
+import org.webeid.security.validator.validators.SubjectCertificatePurposeValidator;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@@ -44,11 +44,11 @@ public static void setMockedDefaultJwtParserDate(Date mockedDate) throws NoSuchF
}
public static void setMockedFunctionalSubjectCertificateValidatorsDate(Date mockedDate) throws NoSuchFieldException, IllegalAccessException {
- setClockField(FunctionalSubjectCertificateValidators.DefaultClock.class, mockedDate);
+ setClockField(SubjectCertificatePurposeValidator.DefaultClock.class, mockedDate);
}
public static void resetMockedFunctionalSubjectCertificateValidatorsDate() throws NoSuchFieldException, IllegalAccessException {
- setClockField(FunctionalSubjectCertificateValidators.DefaultClock.class, new Date());
+ setClockField(SubjectCertificatePurposeValidator.DefaultClock.class, new Date());
}
private static void setClockField(Class extends Clock> cls, Date date) throws NoSuchFieldException, IllegalAccessException {
diff --git a/src/test/java/org/webeid/security/testutil/OcspServiceMaker.java b/src/test/java/org/webeid/security/testutil/OcspServiceMaker.java
new file mode 100644
index 00000000..eefdbaf1
--- /dev/null
+++ b/src/test/java/org/webeid/security/testutil/OcspServiceMaker.java
@@ -0,0 +1,80 @@
+package org.webeid.security.testutil;
+
+import com.google.common.collect.Sets;
+import org.jetbrains.annotations.NotNull;
+import org.webeid.security.exceptions.JceException;
+import org.webeid.security.exceptions.OCSPCertificateException;
+import org.webeid.security.validator.ocsp.OcspServiceProvider;
+import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration;
+import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.webeid.security.certificate.CertificateValidator.buildCertStoreFromCertificates;
+import static org.webeid.security.certificate.CertificateValidator.buildTrustAnchorsFromCertificates;
+import static org.webeid.security.testutil.Certificates.*;
+import static org.webeid.security.validator.ocsp.OcspUrl.AIA_ESTEID_2015;
+
+public class OcspServiceMaker {
+
+ private static final String TEST_OCSP_ACCESS_LOCATION = "http://demo.sk.ee/ocsp";
+ private static final List TRUSTED_CA_CERTIFICATES;
+ private static final URI TEST_ESTEID_2015 = URI.create("http://aia.demo.sk.ee/esteid2015");
+
+ static {
+ try {
+ TRUSTED_CA_CERTIFICATES = Arrays.asList(getTestEsteid2018CA(), getTestEsteid2015CA());
+ } catch (CertificateException | IOException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ @NotNull
+ public static OcspServiceProvider getAiaOcspServiceProvider() throws JceException {
+ return new OcspServiceProvider(null, getAiaOcspServiceConfiguration());
+ }
+
+ @NotNull
+ public static OcspServiceProvider getDesignatedOcspServiceProvider() throws CertificateException, IOException, OCSPCertificateException, JceException {
+ return new OcspServiceProvider(getDesignatedOcspServiceConfiguration(), getAiaOcspServiceConfiguration());
+ }
+
+ @NotNull
+ public static OcspServiceProvider getDesignatedOcspServiceProvider(boolean doesSupportNonce) throws CertificateException, IOException, JceException, OCSPCertificateException {
+ return new OcspServiceProvider(getDesignatedOcspServiceConfiguration(doesSupportNonce), getAiaOcspServiceConfiguration());
+ }
+
+ @NotNull
+ public static OcspServiceProvider getDesignatedOcspServiceProvider(String ocspServiceAccessLocation) throws CertificateException, IOException, OCSPCertificateException, JceException {
+ return new OcspServiceProvider(getDesignatedOcspServiceConfiguration(true, ocspServiceAccessLocation), getAiaOcspServiceConfiguration());
+ }
+
+ private static AiaOcspServiceConfiguration getAiaOcspServiceConfiguration() throws JceException {
+ return new AiaOcspServiceConfiguration(
+ Sets.newHashSet(AIA_ESTEID_2015, TEST_ESTEID_2015),
+ buildTrustAnchorsFromCertificates(TRUSTED_CA_CERTIFICATES),
+ buildCertStoreFromCertificates(TRUSTED_CA_CERTIFICATES));
+ }
+
+ public static DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() throws CertificateException, IOException, OCSPCertificateException {
+ return getDesignatedOcspServiceConfiguration(true, TEST_OCSP_ACCESS_LOCATION);
+ }
+
+ private static DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration(boolean doesSupportNonce) throws CertificateException, IOException, OCSPCertificateException {
+ return getDesignatedOcspServiceConfiguration(doesSupportNonce, TEST_OCSP_ACCESS_LOCATION);
+ }
+
+ private static DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration(boolean doesSupportNonce, String ocspServiceAccessLocation) throws CertificateException, IOException, OCSPCertificateException {
+ return new DesignatedOcspServiceConfiguration(
+ URI.create(ocspServiceAccessLocation),
+ getTestSkOcspResponder2020(),
+ TRUSTED_CA_CERTIFICATES,
+ doesSupportNonce);
+ }
+
+}
diff --git a/src/test/java/org/webeid/security/validator/OcspTest.java b/src/test/java/org/webeid/security/validator/OcspTest.java
index 41e1170c..86ef7803 100644
--- a/src/test/java/org/webeid/security/validator/OcspTest.java
+++ b/src/test/java/org/webeid/security/validator/OcspTest.java
@@ -24,14 +24,20 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.webeid.security.exceptions.*;
+import org.webeid.security.exceptions.JceException;
+import org.webeid.security.exceptions.TokenValidationException;
+import org.webeid.security.exceptions.CertificateExpiredException;
+import org.webeid.security.exceptions.UserCertificateRevokedException;
import org.webeid.security.testutil.AbstractTestWithMockedDateAndCorrectNonce;
import org.webeid.security.testutil.Tokens;
+import java.io.IOException;
+import java.net.URISyntaxException;
import java.security.cert.CertificateException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidatorWithDesignatedOcspCheck;
import static org.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidatorWithOcspCheck;
class OcspTest extends AbstractTestWithMockedDateAndCorrectNonce {
@@ -44,36 +50,42 @@ protected void setup() {
super.setup();
try {
validator = getAuthTokenValidatorWithOcspCheck(cache);
- } catch (CertificateException | JceException e) {
+ } catch (CertificateException | JceException | URISyntaxException | IOException e) {
throw new RuntimeException(e);
}
}
@Test
void detectRevokedUserCertificate() {
- // This test had flaky results as OCSP requests sometimes failed, sometimes passed.
- // Hence the catch which may or may not execute instead of assertThatThrownBy().
try {
validator.validate(Tokens.SIGNED);
} catch (TokenValidationException e) {
- assertThat(e).isInstanceOfAny(
- UserCertificateRevokedException.class,
- UserCertificateRevocationCheckFailedException.class
- );
+ assertThat(e).isInstanceOf(UserCertificateRevokedException.class);
+ }
+ }
+
+ @Test
+ void detectRevokedUserCertificateWithDesignatedOcspService() throws Exception {
+ final AuthTokenValidator validatorWithDesignatedOcspCheck = getAuthTokenValidatorWithDesignatedOcspCheck(cache);
+ try {
+ validatorWithDesignatedOcspCheck.validate(Tokens.SIGNED);
+ } catch (TokenValidationException e) {
+ assertThat(e).isInstanceOf(UserCertificateRevokedException.class);
}
}
@Test
void testTokenCertRsaExpired() {
assertThatThrownBy(() -> validator.validate(Tokens.TOKEN_CERT_RSA_EXIPRED))
- .isInstanceOf(UserCertificateExpiredException.class)
- .hasMessageStartingWith("User certificate has expired:");
+ .isInstanceOf(CertificateExpiredException.class)
+ .hasMessageStartingWith("User certificate has expired");
}
@Test
void testTokenCertEcdsaExpired() {
assertThatThrownBy(() -> validator.validate(Tokens.TOKEN_CERT_ECDSA_EXIPRED))
- .isInstanceOf(UserCertificateExpiredException.class)
- .hasMessageStartingWith("User certificate has expired:");
+ .isInstanceOf(CertificateExpiredException.class)
+ .hasMessageStartingWith("User certificate has expired");
}
+
}
diff --git a/src/test/java/org/webeid/security/validator/ParseTest.java b/src/test/java/org/webeid/security/validator/ParseTest.java
index 31a154e2..4d8efcf2 100644
--- a/src/test/java/org/webeid/security/validator/ParseTest.java
+++ b/src/test/java/org/webeid/security/validator/ParseTest.java
@@ -27,7 +27,7 @@
import org.webeid.security.exceptions.TokenSignatureValidationException;
import org.webeid.security.testutil.AbstractTestWithMockedDateValidatorAndCorrectNonce;
import org.webeid.security.testutil.Tokens;
-import org.webeid.security.util.CertUtil;
+import org.webeid.security.certificate.CertificateData;
import java.security.cert.X509Certificate;
@@ -48,15 +48,15 @@ class ParseTest extends AbstractTestWithMockedDateValidatorAndCorrectNonce {
@Test
void parseSignedToken() throws Exception {
final X509Certificate result = validator.validate(Tokens.SIGNED);
- assertThat(CertUtil.getSubjectCN(result))
+ assertThat(CertificateData.getSubjectCN(result))
.isEqualTo("JÕEORG\\,JAAK-KRISTJAN\\,38001085718");
- assertThat(toTitleCase(CertUtil.getSubjectGivenName(result)))
+ assertThat(toTitleCase(CertificateData.getSubjectGivenName(result)))
.isEqualTo("Jaak-Kristjan");
- assertThat(toTitleCase(CertUtil.getSubjectSurname(result)))
+ assertThat(toTitleCase(CertificateData.getSubjectSurname(result)))
.isEqualTo("Jõeorg");
- assertThat(CertUtil.getSubjectIdCode(result))
+ assertThat(CertificateData.getSubjectIdCode(result))
.isEqualTo("PNOEE-38001085718");
- assertThat(CertUtil.getSubjectCountryCode(result))
+ assertThat(CertificateData.getSubjectCountryCode(result))
.isEqualTo("EE");
}
diff --git a/src/test/java/org/webeid/security/validator/SubjectCertificatePolicyValidatorTest.java b/src/test/java/org/webeid/security/validator/SubjectCertificatePolicyValidatorTest.java
index 0dbea857..3172a512 100644
--- a/src/test/java/org/webeid/security/validator/SubjectCertificatePolicyValidatorTest.java
+++ b/src/test/java/org/webeid/security/validator/SubjectCertificatePolicyValidatorTest.java
@@ -7,6 +7,7 @@
import org.webeid.security.testutil.AbstractTestWithMockedDateAndCorrectNonce;
import org.webeid.security.testutil.Tokens;
+import java.io.IOException;
import java.security.cert.CertificateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -20,7 +21,7 @@ class SubjectCertificatePolicyValidatorTest extends AbstractTestWithMockedDateAn
void setUp() {
try {
validator = getAuthTokenValidatorWithDisallowedESTEIDPolicy(cache);
- } catch (CertificateException | JceException e) {
+ } catch (CertificateException | JceException | IOException e) {
throw new RuntimeException(e);
}
}
diff --git a/src/test/java/org/webeid/security/validator/TrustedCaTest.java b/src/test/java/org/webeid/security/validator/TrustedCaTest.java
index fe0b312b..eac5037f 100644
--- a/src/test/java/org/webeid/security/validator/TrustedCaTest.java
+++ b/src/test/java/org/webeid/security/validator/TrustedCaTest.java
@@ -25,10 +25,11 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.webeid.security.exceptions.JceException;
-import org.webeid.security.exceptions.UserCertificateNotTrustedException;
+import org.webeid.security.exceptions.CertificateNotTrustedException;
import org.webeid.security.testutil.AbstractTestWithMockedDateAndCorrectNonce;
import org.webeid.security.testutil.Tokens;
+import java.io.IOException;
import java.security.cert.CertificateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -44,7 +45,7 @@ protected void setup() {
super.setup();
try {
validator = getAuthTokenValidatorWithWrongTrustedCA(cache);
- } catch (CertificateException | JceException e) {
+ } catch (CertificateException | JceException | IOException e) {
throw new RuntimeException(e);
}
}
@@ -52,7 +53,7 @@ protected void setup() {
@Test
void detectUntrustedUserCertificate() {
assertThatThrownBy(() -> validator.validate(Tokens.SIGNED))
- .isInstanceOf(UserCertificateNotTrustedException.class);
+ .isInstanceOf(CertificateNotTrustedException.class);
}
}
diff --git a/src/test/java/org/webeid/security/validator/ValidateTest.java b/src/test/java/org/webeid/security/validator/ValidateTest.java
index b2743704..912e70e5 100644
--- a/src/test/java/org/webeid/security/validator/ValidateTest.java
+++ b/src/test/java/org/webeid/security/validator/ValidateTest.java
@@ -1,10 +1,10 @@
/*
- * Copyright (c) 2020 The Web eID Project
+ * Copyright (c) 2020-2021 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
+ * to use, copy, modify, mergCertificateExpiryValidatore, 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:
*
@@ -25,8 +25,8 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.webeid.security.exceptions.TokenParseException;
-import org.webeid.security.exceptions.UserCertificateExpiredException;
-import org.webeid.security.exceptions.UserCertificateNotYetValidException;
+import org.webeid.security.exceptions.CertificateExpiredException;
+import org.webeid.security.exceptions.CertificateNotYetValidException;
import org.webeid.security.testutil.AbstractTestWithMockedDateValidatorAndCorrectNonce;
import org.webeid.security.testutil.Dates;
import org.webeid.security.testutil.Tokens;
@@ -50,22 +50,51 @@ public void tearDown() {
}
}
+ // Subject certificate validity:
+ // - not before: Thu Oct 18 12:50:47 EEST 2018
+ // - not after: Wed Oct 18 00:59:59 EEST 2023
+ // Issuer certificate validity:
+ // - not before: Thu Sep 06 12:03:52 EEST 2018
+ // - not after: Tue Aug 30 15:48:28 EEST 2033
+
@Test
- void certificateIsNotValidYet() throws Exception {
+ void userCertificateIsNotYetValid() throws Exception {
final Date certValidFrom = Dates.create("2018-10-17");
setMockedFunctionalSubjectCertificateValidatorsDate(certValidFrom);
assertThatThrownBy(() -> validator.validate(Tokens.SIGNED))
- .isInstanceOf(UserCertificateNotYetValidException.class);
+ .isInstanceOf(CertificateNotYetValidException.class)
+ .hasMessage("User certificate is not yet valid");
+ }
+
+ @Test
+ void trustedCACertificateIsNotYetValid() throws Exception {
+ final Date certValidFrom = Dates.create("2018-08-17");
+ setMockedFunctionalSubjectCertificateValidatorsDate(certValidFrom);
+
+ assertThatThrownBy(() -> validator.validate(Tokens.SIGNED))
+ .isInstanceOf(CertificateNotYetValidException.class)
+ .hasMessage("Trusted CA certificate is not yet valid");
}
@Test
- void certificateIsNoLongerValid() throws Exception {
+ void userCertificateIsNoLongerValid() throws Exception {
final Date certValidFrom = Dates.create("2023-10-19");
setMockedFunctionalSubjectCertificateValidatorsDate(certValidFrom);
assertThatThrownBy(() -> validator.validate(Tokens.SIGNED))
- .isInstanceOf(UserCertificateExpiredException.class);
+ .isInstanceOf(CertificateExpiredException.class)
+ .hasMessage("User certificate has expired");
+ }
+
+ @Test
+ void trustedCACertificateIsNoLongerValid() throws Exception {
+ final Date certValidFrom = Dates.create("2033-10-19");
+ setMockedFunctionalSubjectCertificateValidatorsDate(certValidFrom);
+
+ assertThatThrownBy(() -> validator.validate(Tokens.SIGNED))
+ .isInstanceOf(CertificateExpiredException.class)
+ .hasMessage("Trusted CA certificate has expired");
}
@Test
diff --git a/src/test/java/org/webeid/security/validator/ocsp/OcspResponseValidatorTest.java b/src/test/java/org/webeid/security/validator/ocsp/OcspResponseValidatorTest.java
new file mode 100644
index 00000000..7bdd739d
--- /dev/null
+++ b/src/test/java/org/webeid/security/validator/ocsp/OcspResponseValidatorTest.java
@@ -0,0 +1,70 @@
+package org.webeid.security.validator.ocsp;
+
+import okhttp3.ResponseBody;
+import org.bouncycastle.asn1.ocsp.SingleResponse;
+import org.bouncycastle.cert.ocsp.SingleResp;
+import org.junit.jupiter.api.Test;
+import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
+import org.webeid.security.testutil.Dates;
+import org.webeid.security.validator.validators.SubjectCertificateNotRevokedValidator;
+
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateCertificateStatusUpdateTime;
+
+class OcspResponseValidatorTest {
+
+ @Test
+ void whenThisUpdateDayBeforeProducedAt_thenThrows() throws Exception {
+ final SingleResp mockResponse = mock(SingleResp.class);
+ // yyyy-MM-dd'T'HH:mm:ss.SSSZ
+ when(mockResponse.getThisUpdate()).thenReturn(Dates.create("2021-09-01T00:00:00.000Z"));
+ final Date producedAt = Dates.create("2021-09-02T00:00:00.000Z");
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validateCertificateStatusUpdateTime(mockResponse, producedAt))
+ .withMessage("User certificate revocation check has failed: "
+ + "Certificate status update time check failed: "
+ + "notAllowedBefore: 2021-09-01 23:45:00 UTC, "
+ + "notAllowedAfter: 2021-09-02 00:15:00 UTC, "
+ + "thisUpdate: 2021-09-01 00:00:00 UTC, "
+ + "nextUpdate: null");
+ }
+
+ @Test
+ void whenThisUpdateDayAfterProducedAt_thenThrows() throws Exception {
+ final SingleResp mockResponse = mock(SingleResp.class);
+ when(mockResponse.getThisUpdate()).thenReturn(Dates.create("2021-09-02"));
+ final Date producedAt = Dates.create("2021-09-01");
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validateCertificateStatusUpdateTime(mockResponse, producedAt))
+ .withMessage("User certificate revocation check has failed: "
+ + "Certificate status update time check failed: "
+ + "notAllowedBefore: 2021-08-31 23:45:00 UTC, "
+ + "notAllowedAfter: 2021-09-01 00:15:00 UTC, "
+ + "thisUpdate: 2021-09-02 00:00:00 UTC, "
+ + "nextUpdate: null");
+ }
+
+ @Test
+ void whenNextUpdateDayBeforeProducedAt_thenThrows() throws Exception {
+ final SingleResp mockResponse = mock(SingleResp.class);
+ when(mockResponse.getThisUpdate()).thenReturn(Dates.create("2021-09-02"));
+ when(mockResponse.getNextUpdate()).thenReturn(Dates.create("2021-09-01"));
+ final Date producedAt = Dates.create("2021-09-02");
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validateCertificateStatusUpdateTime(mockResponse, producedAt))
+ .withMessage("User certificate revocation check has failed: "
+ + "Certificate status update time check failed: "
+ + "notAllowedBefore: 2021-09-01 23:45:00 UTC, "
+ + "notAllowedAfter: 2021-09-02 00:15:00 UTC, "
+ + "thisUpdate: 2021-09-02 00:00:00 UTC, "
+ + "nextUpdate: 2021-09-01 00:00:00 UTC");
+ }
+
+}
diff --git a/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java b/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java
new file mode 100644
index 00000000..bf76569a
--- /dev/null
+++ b/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java
@@ -0,0 +1,70 @@
+package org.webeid.security.validator.ocsp;
+
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.junit.jupiter.api.Test;
+import org.webeid.security.exceptions.OCSPCertificateException;
+import org.webeid.security.validator.ocsp.service.OcspService;
+
+import java.net.URI;
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert;
+import static org.webeid.security.testutil.Certificates.getMariliisEsteid2015Cert;
+import static org.webeid.security.testutil.Certificates.getTestEsteid2015CA;
+import static org.webeid.security.testutil.Certificates.getTestEsteid2018CA;
+import static org.webeid.security.testutil.Certificates.getTestSkOcspResponder2020;
+import static org.webeid.security.testutil.OcspServiceMaker.getAiaOcspServiceProvider;
+import static org.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceProvider;
+
+class OcspServiceProviderTest {
+
+ @Test
+ void whenDesignatedOcspServiceConfigurationProvided_thenCreatesDesignatedOcspService() throws Exception {
+ final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider();
+ final OcspService service = ocspServiceProvider.getService(getJaakKristjanEsteid2018Cert());
+ assertThat(service.getAccessLocation()).isEqualTo(new URI("http://demo.sk.ee/ocsp"));
+ assertThat(service.doesSupportNonce()).isTrue();
+ assertThatCode(() ->
+ service.validateResponderCertificate(new X509CertificateHolder(getTestSkOcspResponder2020().getEncoded()), new Date(1630000000000L)))
+ .doesNotThrowAnyException();
+ assertThatCode(() ->
+ service.validateResponderCertificate(new X509CertificateHolder(getTestEsteid2018CA().getEncoded()), new Date(1630000000000L)))
+ .isInstanceOf(OCSPCertificateException.class)
+ .hasMessage("Responder certificate from the OCSP response is not equal to the configured designated OCSP responder certificate");
+ }
+
+ @Test
+ void whenAiaOcspServiceConfigurationProvided_thenCreatesAiaOcspService() throws Exception {
+ final OcspServiceProvider ocspServiceProvider = getAiaOcspServiceProvider();
+ final OcspService service2018 = ocspServiceProvider.getService(getJaakKristjanEsteid2018Cert());
+ assertThat(service2018.getAccessLocation()).isEqualTo(new URI("http://aia.demo.sk.ee/esteid2018"));
+ assertThat(service2018.doesSupportNonce()).isTrue();
+ assertThatCode(() ->
+ // Use the CA certificate instead of responder certificate for convenience.
+ service2018.validateResponderCertificate(new X509CertificateHolder(getTestEsteid2018CA().getEncoded()), new Date(1630000000000L)))
+ .doesNotThrowAnyException();
+
+ final OcspService service2015 = ocspServiceProvider.getService(getMariliisEsteid2015Cert());
+ assertThat(service2015.getAccessLocation()).isEqualTo(new URI("http://aia.demo.sk.ee/esteid2015"));
+ assertThat(service2015.doesSupportNonce()).isFalse();
+ assertThatCode(() ->
+ // Use the CA certificate instead of responder certificate for convenience.
+ service2015.validateResponderCertificate(new X509CertificateHolder(getTestEsteid2015CA().getEncoded()), new Date(1630000000000L)))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void whenAiaOcspServiceConfigurationDoesNotHaveResponderCertTrustedCA_thenThrows() throws Exception {
+ final OcspServiceProvider ocspServiceProvider = getAiaOcspServiceProvider();
+ final OcspService service2018 = ocspServiceProvider.getService(getJaakKristjanEsteid2018Cert());
+ final X509CertificateHolder wrongResponderCert = new X509CertificateHolder(getMariliisEsteid2015Cert().getEncoded());
+ assertThatExceptionOfType(OCSPCertificateException.class)
+ .isThrownBy(() ->
+ service2018.validateResponderCertificate(wrongResponderCert, new Date(1630000000000L)));
+ }
+
+
+}
diff --git a/src/test/java/org/webeid/security/validator/ocsp/OcspUrlTest.java b/src/test/java/org/webeid/security/validator/ocsp/OcspUrlTest.java
new file mode 100644
index 00000000..645825e7
--- /dev/null
+++ b/src/test/java/org/webeid/security/validator/ocsp/OcspUrlTest.java
@@ -0,0 +1,41 @@
+package org.webeid.security.validator.ocsp;
+
+import org.bouncycastle.asn1.x509.Extension;
+import org.junit.jupiter.api.Test;
+
+import java.security.cert.X509Certificate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.webeid.security.validator.ocsp.OcspUrl.getOcspUri;
+
+class OcspUrlTest {
+
+ @Test
+ void whenExtensionValueIsNull_thenReturnsNull() throws Exception {
+ final X509Certificate mockCertificate = mock(X509Certificate.class);
+ when(mockCertificate.getExtensionValue(anyString())).thenReturn(null);
+ assertThat(getOcspUri(mockCertificate)).isNull();
+ }
+
+ @Test
+ void whenExtensionValueIsInvalid_thenReturnsNull() throws Exception {
+ final X509Certificate mockCertificate = mock(X509Certificate.class);
+ when(mockCertificate.getExtensionValue(anyString())).thenReturn(new byte[]{1, 2, 3});
+ assertThat(getOcspUri(mockCertificate)).isNull();
+ }
+
+ @Test
+ void whenExtensionValueIsNotAia_thenReturnsNull() throws Exception {
+ final X509Certificate mockCertificate = mock(X509Certificate.class);
+ when(mockCertificate.getExtensionValue(anyString())).thenReturn(new byte[]{
+ 4, 64, 48, 62, 48, 50, 6, 11, 43, 6, 1, 4, 1, -125, -111, 33, 1, 2, 1, 48,
+ 35, 48, 33, 6, 8, 43, 6, 1, 5, 5, 7, 2, 1, 22, 21, 104, 116, 116, 112, 115,
+ 58, 47, 47, 119, 119, 119, 46, 115, 107, 46, 101, 101, 47, 67, 80, 83, 48,
+ 8, 6, 6, 4, 0, -113, 122, 1, 2});
+ assertThat(getOcspUri(mockCertificate)).isNull();
+ }
+
+}
diff --git a/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java b/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java
new file mode 100644
index 00000000..33298db3
--- /dev/null
+++ b/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (c) 2021 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 org.webeid.security.validator.validators;
+
+import com.google.common.io.ByteStreams;
+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;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.webeid.security.exceptions.CertificateNotTrustedException;
+import org.webeid.security.exceptions.JceException;
+import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
+import org.webeid.security.exceptions.UserCertificateRevokedException;
+import org.webeid.security.validator.AuthTokenValidatorData;
+import org.webeid.security.validator.ocsp.OcspClient;
+import org.webeid.security.validator.ocsp.OcspClientImpl;
+import org.webeid.security.validator.ocsp.OcspServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.net.URISyntaxException;
+import java.security.cert.CertificateException;
+import java.time.Duration;
+import java.util.Objects;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert;
+import static org.webeid.security.testutil.Certificates.getTestEsteid2018CA;
+import static org.webeid.security.testutil.OcspServiceMaker.getAiaOcspServiceProvider;
+import static org.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceProvider;
+
+class SubjectCertificateNotRevokedValidatorTest {
+
+ private static final MediaType OCSP_RESPONSE = MediaType.get("application/ocsp-response");
+
+ private final OcspClient ocspClient = OcspClientImpl.build(Duration.ofSeconds(5));
+ private SubjectCertificateTrustedValidator trustedValidator;
+ private AuthTokenValidatorData authTokenValidatorWithEsteid2018Cert;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ trustedValidator = new SubjectCertificateTrustedValidator(null, null);
+ setSubjectCertificateIssuerCertificate(trustedValidator);
+ authTokenValidatorWithEsteid2018Cert = new AuthTokenValidatorData(getJaakKristjanEsteid2018Cert());
+ }
+
+ @Test
+ void whenValidAiaOcspResponderConfiguration_thenSucceeds() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(ocspClient);
+ assertThatCode(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void whenValidDesignatedOcspResponderConfiguration_thenSucceeds() throws Exception {
+ final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider();
+ final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider);
+ assertThatCode(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void whenValidOcspNonceDisabledConfiguration_thenSucceeds() throws Exception {
+ final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider(false);
+ final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider);
+ assertThatCode(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void whenOcspUrlIsInvalid_thenThrows() throws Exception {
+ final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("http://invalid.invalid");
+ final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider);
+ assertThatCode(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .isInstanceOf(UserCertificateOCSPCheckFailedException.class)
+ .getCause()
+ .isInstanceOf(IOException.class)
+ .hasMessageMatching("invalid.invalid: (Name or service not known|"
+ + "Temporary failure in name resolution)");
+ }
+
+ @Test
+ void whenOcspRequestFails_thenThrows() throws Exception {
+ final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("https://web-eid-test.free.beeceptor.com");
+ final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider);
+ assertThatCode(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .isInstanceOf(UserCertificateOCSPCheckFailedException.class)
+ .getCause()
+ .isInstanceOf(IOException.class)
+ .hasMessageStartingWith("OCSP request was not successful, response: Response{");
+ }
+
+ @Test
+ void whenOcspRequestHasInvalidBody_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create("invalid", OCSP_RESPONSE))
+ .build());
+ assertThatCode(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .isInstanceOf(UserCertificateOCSPCheckFailedException.class)
+ .getCause()
+ .isInstanceOf(IOException.class)
+ .hasMessage("corrupted stream - out of bounds length found: 110 >= 7");
+ }
+
+ @Test
+ void whenOcspResponseIsNotSuccessful_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(buildOcspResponseBodyWithInternalErrorStatus(), OCSP_RESPONSE))
+ .build());
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("User certificate revocation check has failed: Response status: internal error");
+ }
+
+ @Test
+ void whenOcspResponseHasInvalidCertificateId_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(buildOcspResponseBodyWithInvalidCertificateId(), OCSP_RESPONSE))
+ .build());
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("User certificate revocation check has failed: OCSP responded with certificate ID that differs from the requested ID");
+ }
+
+ @Test
+ void whenOcspResponseHasInvalidSignature_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(buildOcspResponseBodyWithInvalidSignature(), OCSP_RESPONSE))
+ .build());
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("User certificate revocation check has failed: OCSP response signature is invalid");
+ }
+
+ @Test
+ void whenOcspResponseHasInvalidResponderCert_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(buildOcspResponseBodyWithInvalidResponderCert(), OCSP_RESPONSE))
+ .build());
+ assertThatCode(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .isInstanceOf(UserCertificateOCSPCheckFailedException.class)
+ .getCause()
+ .isInstanceOf(OCSPException.class)
+ .hasMessage("exception processing sig: java.lang.IllegalArgumentException: invalid info structure in RSA public key");
+ }
+
+ @Test
+ void whenOcspResponseHasInvalidTag_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(buildOcspResponseBodyWithInvalidTag(), OCSP_RESPONSE))
+ .build());
+ assertThatCode(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .isInstanceOf(UserCertificateOCSPCheckFailedException.class)
+ .getCause()
+ .isInstanceOf(OCSPException.class)
+ .hasMessage("problem decoding object: java.io.IOException: unknown tag 23 encountered");
+ }
+
+ @Test
+ void whenOcspResponseHas2CertResponses_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_with_2_responses.der"), OCSP_RESPONSE))
+ .build());
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("User certificate revocation check has failed: OCSP response must contain one response, received 2 responses instead");
+ }
+
+ @Disabled("It is difficult to make Python and Java CertId equal, needs more work")
+ void whenOcspResponseHas2ResponderCerts_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_with_2_responder_certs.der"), OCSP_RESPONSE))
+ .build());
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("User certificate revocation check has failed: OCSP response must contain one responder certificate, received 2 certificates instead");
+ }
+
+ @Test
+ void whenOcspResponseRevoked_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_revoked.der"), OCSP_RESPONSE))
+ .build());
+ assertThatExceptionOfType(UserCertificateRevokedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("User certificate has been revoked: Revocation reason: 0");
+ }
+
+ @Test
+ void whenOcspResponseUnknown_thenThrows() throws Exception {
+ final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("https://web-eid-test.free.beeceptor.com");
+ final Response response = getResponseBuilder()
+ .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_unknown.der"), OCSP_RESPONSE))
+ .build();
+ final OcspClient client = (url, request) -> new OCSPResp(Objects.requireNonNull(response.body()).bytes());
+ final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, client, ocspServiceProvider);
+ assertThatExceptionOfType(UserCertificateRevokedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("User certificate has been revoked: Unknown status");
+ }
+
+ @Test
+ void whenOcspResponseCANotTrusted_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_unknown.der"), OCSP_RESPONSE))
+ .build());
+ assertThatExceptionOfType(CertificateNotTrustedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("Certificate EMAILADDRESS=pki@sk.ee, CN=TEST of SK OCSP RESPONDER 2020, OU=OCSP, O=AS Sertifitseerimiskeskus, C=EE is not trusted");
+ }
+
+ @Test
+ void whenNonceDiffers_thenThrows() throws Exception {
+ final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(
+ getResponseBuilder()
+ .body(ResponseBody.create(getOcspResponseBytesFromResources(), OCSP_RESPONSE))
+ .build());
+ assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class)
+ .isThrownBy(() ->
+ validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert))
+ .withMessage("User certificate revocation check has failed: OCSP request and response nonces differ, possible replay attack");
+ }
+
+ private static byte[] buildOcspResponseBodyWithInternalErrorStatus() throws IOException {
+ final byte[] ocspResponseBytes = getOcspResponseBytesFromResources();
+ final int STATUS_OFFSET = 6;
+ ocspResponseBytes[STATUS_OFFSET] = OCSPResponseStatus.INTERNAL_ERROR;
+ return ocspResponseBytes;
+ }
+
+ private static byte[] buildOcspResponseBodyWithInvalidCertificateId() throws IOException {
+ final byte[] ocspResponseBytes = getOcspResponseBytesFromResources();
+ final int CERTIFICATE_ID_OFFSET = 234;
+ ocspResponseBytes[CERTIFICATE_ID_OFFSET + 3] = 0x42;
+ return ocspResponseBytes;
+ }
+
+ private byte[] buildOcspResponseBodyWithInvalidSignature() throws IOException {
+ final byte[] ocspResponseBytes = getOcspResponseBytesFromResources();
+ final int SIGNATURE_OFFSET = 349;
+ ocspResponseBytes[SIGNATURE_OFFSET + 5] = 0x01;
+ return ocspResponseBytes;
+ }
+
+ private byte[] buildOcspResponseBodyWithInvalidResponderCert() throws IOException {
+ final byte[] ocspResponseBytes = getOcspResponseBytesFromResources();
+ final int CERTIFICATE_OFFSET = 935;
+ ocspResponseBytes[CERTIFICATE_OFFSET + 3] = 0x42;
+ return ocspResponseBytes;
+ }
+
+ private byte[] buildOcspResponseBodyWithInvalidTag() throws IOException {
+ final byte[] ocspResponseBytes = getOcspResponseBytesFromResources();
+ final int TAG_OFFSET = 352;
+ ocspResponseBytes[TAG_OFFSET] = 0x42;
+ return ocspResponseBytes;
+ }
+
+ // Either write the bytes of a real OCSP response to a file or use Python and asn1crypto.ocsp
+ // to create a mock response, see OCSPBuilder in https://github.com/wbond/ocspbuilder/blob/master/ocspbuilder/__init__.py
+ // and https://gist.github.com/mrts/bb0dcf93a2b9d2458eab1f9642ee97b2.
+ private static byte[] getOcspResponseBytesFromResources() throws IOException {
+ return getOcspResponseBytesFromResources("ocsp_response.der");
+ }
+
+ private static byte[] getOcspResponseBytesFromResources(String resource) throws IOException {
+ try (final InputStream resourceAsStream = ClassLoader.getSystemResourceAsStream(resource)) {
+ return ByteStreams.toByteArray(Objects.requireNonNull(resourceAsStream));
+ }
+ }
+
+ @NotNull
+ private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidatorWithAiaOcsp(Response response) throws JceException, URISyntaxException, CertificateException, IOException {
+ final OcspClient client = (url, request) -> new OCSPResp(Objects.requireNonNull(response.body()).bytes());
+ return getSubjectCertificateNotRevokedValidatorWithAiaOcsp(client);
+ }
+
+ @NotNull
+ private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidatorWithAiaOcsp(OcspClient client) throws JceException, URISyntaxException, CertificateException, IOException {
+ return new SubjectCertificateNotRevokedValidator(trustedValidator, client, getAiaOcspServiceProvider());
+ }
+
+ private static void setSubjectCertificateIssuerCertificate(SubjectCertificateTrustedValidator trustedValidator) throws NoSuchFieldException, IllegalAccessException, CertificateException, IOException {
+ final Field field = trustedValidator.getClass().getDeclaredField("subjectCertificateIssuerCertificate");
+ field.setAccessible(true);
+ field.set(trustedValidator, getTestEsteid2018CA());
+ }
+
+ @NotNull
+ private static Response.Builder getResponseBuilder() {
+ return new Response.Builder()
+ .request(new Request.Builder().url("http://testing").build())
+ .message("testing")
+ .protocol(Protocol.HTTP_1_1)
+ .code(200);
+ }
+
+}
diff --git a/src/test/resources/TEST_of_ESTEID-SK_2015.cer b/src/test/resources/TEST_of_ESTEID-SK_2015.cer
new file mode 100644
index 00000000..7749286c
Binary files /dev/null and b/src/test/resources/TEST_of_ESTEID-SK_2015.cer differ
diff --git a/src/test/resources/TEST_of_SK_OCSP_RESPONDER_2020.cer b/src/test/resources/TEST_of_SK_OCSP_RESPONDER_2020.cer
new file mode 100644
index 00000000..f2c4bd5e
Binary files /dev/null and b/src/test/resources/TEST_of_SK_OCSP_RESPONDER_2020.cer differ
diff --git a/src/test/resources/ocsp_response.der b/src/test/resources/ocsp_response.der
new file mode 100644
index 00000000..9bf94116
Binary files /dev/null and b/src/test/resources/ocsp_response.der differ
diff --git a/src/test/resources/ocsp_response_revoked.der b/src/test/resources/ocsp_response_revoked.der
new file mode 100644
index 00000000..551a99df
Binary files /dev/null and b/src/test/resources/ocsp_response_revoked.der differ
diff --git a/src/test/resources/ocsp_response_unknown.der b/src/test/resources/ocsp_response_unknown.der
new file mode 100644
index 00000000..0c1e0187
Binary files /dev/null and b/src/test/resources/ocsp_response_unknown.der differ
diff --git a/src/test/resources/ocsp_response_with_2_responder_certs.der b/src/test/resources/ocsp_response_with_2_responder_certs.der
new file mode 100644
index 00000000..b8520125
Binary files /dev/null and b/src/test/resources/ocsp_response_with_2_responder_certs.der differ
diff --git a/src/test/resources/ocsp_response_with_2_responses.der b/src/test/resources/ocsp_response_with_2_responses.der
new file mode 100644
index 00000000..387b8aab
Binary files /dev/null and b/src/test/resources/ocsp_response_with_2_responses.der differ