From c360a3c19da99c5c3b42e048a8ca67e260f94466 Mon Sep 17 00:00:00 2001 From: Mart Somermaa Date: Fri, 27 Aug 2021 23:09:14 +0300 Subject: [PATCH 1/3] feat(OCSP): implement configurable designated OCSP service, verify OCSP responder certificate and response signature WE2-432 Signed-off-by: Mart Somermaa --- README.md | 18 + pom.xml | 10 +- .../CertificateData.java} | 6 +- .../certificate}/CertificateLoader.java | 23 +- .../certificate/CertificateValidator.java | 96 ++++++ ...iaOcspResponderConfigurationException.java | 7 + ...va => CertificateNotTrustedException.java} | 14 +- .../exceptions/OCSPCertificateException.java | 13 + .../exceptions/OriginMismatchException.java | 2 +- .../exceptions/TokenExpiredException.java | 2 +- .../exceptions/TokenParseException.java | 2 +- .../TokenSignatureValidationException.java | 2 +- .../UserCertificateExpiredException.java | 2 +- .../UserCertificateNotYetValidException.java | 2 +- ...rCertificateOCSPCheckFailedException.java} | 8 +- .../UserCertificateParseException.java | 2 +- .../org/webeid/security/util/OcspUrls.java | 12 - .../AuthTokenValidationConfiguration.java | 47 ++- .../validator/AuthTokenValidator.java | 4 +- .../validator/AuthTokenValidatorBuilder.java | 28 +- .../validator/AuthTokenValidatorData.java | 2 +- .../validator/AuthTokenValidatorImpl.java | 48 ++- .../security/validator/ocsp/OcspClient.java | 13 + .../validator/ocsp/OcspClientImpl.java | 74 ++++ .../validator/ocsp/OcspRequestBuilder.java | 52 +-- .../validator/ocsp/OcspServiceProvider.java | 33 ++ .../security/validator/ocsp/OcspUtils.java | 87 +++-- .../AiaOcspResponderConfiguration.java | 46 +++ .../ocsp/service/AiaOcspService.java | 70 ++++ .../service/AiaOcspServiceConfiguration.java | 26 ++ .../ocsp/service/DesignatedOcspService.java | 53 +++ .../DesignatedOcspServiceConfiguration.java | 39 +++ .../validator/ocsp/service/OcspService.java | 17 + ...unctionalSubjectCertificateValidators.java | 21 +- ...SubjectCertificateNotRevokedValidator.java | 174 +++++++--- .../SubjectCertificateTrustedValidator.java | 42 +-- ...ithMockedDateValidatorAndCorrectNonce.java | 3 +- .../testutil/AbstractTestWithValidator.java | 3 +- .../testutil/AuthTokenValidators.java | 28 +- .../security/testutil/Certificates.java | 64 ++++ .../webeid/security/validator/OcspTest.java | 10 +- .../webeid/security/validator/ParseTest.java | 12 +- ...SubjectCertificatePolicyValidatorTest.java | 3 +- .../security/validator/TrustedCaTest.java | 7 +- .../ocsp/OcspServiceProviderTest.java | 75 ++++ ...ectCertificateNotRevokedValidatorTest.java | 319 ++++++++++++++++++ src/test/resources/TEST_of_ESTEID-SK_2015.cer | Bin 0 -> 1671 bytes .../TEST_of_SK_OCSP_RESPONDER_2020.cer | Bin 0 -> 1234 bytes src/test/resources/ocsp_response.der | Bin 0 -> 1579 bytes src/test/resources/ocsp_response_revoked.der | Bin 0 -> 1601 bytes src/test/resources/ocsp_response_unknown.der | Bin 0 -> 1833 bytes .../ocsp_response_with_2_responder_certs.der | Bin 0 -> 2931 bytes .../ocsp_response_with_2_responses.der | Bin 0 -> 267 bytes 53 files changed, 1327 insertions(+), 294 deletions(-) rename src/main/java/org/webeid/security/{util/CertUtil.java => certificate/CertificateData.java} (96%) rename src/{test/java/org/webeid/security/testutil => main/java/org/webeid/security/certificate}/CertificateLoader.java (69%) create mode 100644 src/main/java/org/webeid/security/certificate/CertificateValidator.java create mode 100644 src/main/java/org/webeid/security/exceptions/AiaOcspResponderConfigurationException.java rename src/main/java/org/webeid/security/exceptions/{UserCertificateNotTrustedException.java => CertificateNotTrustedException.java} (76%) create mode 100644 src/main/java/org/webeid/security/exceptions/OCSPCertificateException.java rename src/main/java/org/webeid/security/exceptions/{UserCertificateRevocationCheckFailedException.java => UserCertificateOCSPCheckFailedException.java} (80%) delete mode 100644 src/main/java/org/webeid/security/util/OcspUrls.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/OcspClient.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspResponderConfiguration.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java create mode 100644 src/test/java/org/webeid/security/testutil/Certificates.java create mode 100644 src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java create mode 100644 src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java create mode 100644 src/test/resources/TEST_of_ESTEID-SK_2015.cer create mode 100644 src/test/resources/TEST_of_SK_OCSP_RESPONDER_2020.cer create mode 100644 src/test/resources/ocsp_response.der create mode 100644 src/test/resources/ocsp_response_revoked.der create mode 100644 src/test/resources/ocsp_response_unknown.der create mode 100644 src/test/resources/ocsp_response_with_2_responder_certs.der create mode 100644 src/test/resources/ocsp_response_with_2_responses.der diff --git a/README.md b/README.md index baf3a28f..a06e1536 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,11 @@ import java.security.cert.X509Certificate; ... ``` +## 5. Add trusted OCSP responder certificates + +- AIA +- Designated + ## 5. Configure the authentication token validator Once the prerequisites have been met, the authentication token validator itself can be configured. @@ -235,6 +240,8 @@ try { - [Nonce generation](#nonce-generation) - [Basic usage](#basic-usage-1) - [Extended configuration](#extended-configuration-1) +- [Frequently asked questions](#frequently-asked-questions) + - [How can I find the AIA OCSP service URLs?](#how-can-i-find-the-aia-ocsp-service-urls) # Introduction @@ -356,3 +363,14 @@ NonceGenerator generator = new NonceGeneratorBuilder() .withSecureRandom(customSecureRandom) .build(); ``` + +## Frequently asked questions + +### How can I find the AIA OCSP service URLs? + +You can find the AIA OCSP service URLs from the electronic ID certificate profile documents, in the section that describes certificate extensions. +The AIA OCSP extension OID is 1.3.6.1.5.5.7.48.1. + +For example, the EstEID AIA URLs are specified in the documents +[*Certificate, CRL and OCSP Profile for identification documents of the Republic of Estonia*](https://www.skidsolutions.eu/upload/files/SK-CPR-ESTEID-EN-v8_4-20200630.pdf) and +[*Certificate, CRL and OCSP Profile for ID-1 Format Identity Documents Issued by the Republic of Estonia*](https://www.skidsolutions.eu/upload/files/SK-CPR-ESTEID2018-EN-v1_2_20200630.pdf). diff --git a/pom.xml b/pom.xml index a3339adf..bc746e9b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 authtoken-validation org.webeid.security - 1.1.0 + 2.0.0 jar authtoken-validation Web eID authentication token validation library for Java @@ -16,6 +16,7 @@ 1.8 0.11.2 1.7.30 + 1.65 2.8.5 5.6.2 3.17.2 @@ -67,10 +68,15 @@ guava 30.1-jre + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + org.bouncycastle bcpkix-jdk15on - 1.65 + ${bouncycastle.version} com.squareup.okhttp3 diff --git a/src/main/java/org/webeid/security/util/CertUtil.java b/src/main/java/org/webeid/security/certificate/CertificateData.java similarity index 96% rename from src/main/java/org/webeid/security/util/CertUtil.java rename to src/main/java/org/webeid/security/certificate/CertificateData.java index 9c1031c9..87f01256 100644 --- a/src/main/java/org/webeid/security/util/CertUtil.java +++ b/src/main/java/org/webeid/security/certificate/CertificateData.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package org.webeid.security.util; +package org.webeid.security.certificate; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.x500.RDN; @@ -34,7 +34,7 @@ import java.util.Arrays; import java.util.stream.Collectors; -public final class CertUtil { +public final class CertificateData { public static String getSubjectCN(X509Certificate certificate) throws CertificateEncodingException { return getField(certificate, BCStyle.CN); @@ -65,7 +65,7 @@ private static String getField(X509Certificate certificate, ASN1ObjectIdentifier .collect(Collectors.joining(", ")); } - private CertUtil() { + private CertificateData() { throw new IllegalStateException("Utility class"); } diff --git a/src/test/java/org/webeid/security/testutil/CertificateLoader.java b/src/main/java/org/webeid/security/certificate/CertificateLoader.java similarity index 69% rename from src/test/java/org/webeid/security/testutil/CertificateLoader.java rename to src/main/java/org/webeid/security/certificate/CertificateLoader.java index 5b67d04e..af760ab2 100644 --- a/src/test/java/org/webeid/security/testutil/CertificateLoader.java +++ b/src/main/java/org/webeid/security/certificate/CertificateLoader.java @@ -20,32 +20,43 @@ * SOFTWARE. */ -package org.webeid.security.testutil; +package org.webeid.security.certificate; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Base64; import java.util.List; public final class CertificateLoader { - public static X509Certificate[] loadCertificatesFromResources(String... resourceNames) throws CertificateException { - List caCertificates = new ArrayList<>(); - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + public static X509Certificate[] loadCertificatesFromResources(String... resourceNames) throws CertificateException, IOException { + final List caCertificates = new ArrayList<>(); + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); for (final String resourceName : resourceNames) { try (final InputStream resourceAsStream = ClassLoader.getSystemResourceAsStream(resourceName)) { X509Certificate caCertificate = (X509Certificate) certFactory.generateCertificate(resourceAsStream); caCertificates.add(caCertificate); - } catch (IOException e) { - throw new RuntimeException("Error loading certificate resource.", e); } } return caCertificates.toArray(new X509Certificate[0]); } + public static X509Certificate loadCertificateFromBase64String(String certificate) throws CertificateException, IOException { + try (final InputStream targetStream = new ByteArrayInputStream(Base64.getDecoder().decode(certificate))) { + return (X509Certificate) CertificateFactory + .getInstance("X509") + .generateCertificate(targetStream); + } + } + + private CertificateLoader() { + throw new IllegalStateException("Utility class"); + } } diff --git a/src/main/java/org/webeid/security/certificate/CertificateValidator.java b/src/main/java/org/webeid/security/certificate/CertificateValidator.java new file mode 100644 index 00000000..e26c99d5 --- /dev/null +++ b/src/main/java/org/webeid/security/certificate/CertificateValidator.java @@ -0,0 +1,96 @@ +package org.webeid.security.certificate; + +import org.webeid.security.exceptions.CertificateNotTrustedException; +import org.webeid.security.exceptions.JceException; +import org.webeid.security.exceptions.UserCertificateExpiredException; +import org.webeid.security.exceptions.UserCertificateNotYetValidException; + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.TrustAnchor; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Set; +import java.util.stream.Collectors; + +public final class CertificateValidator { + + /** + * Checks whether the certificate was valid on the given date. + */ + public static void certificateIsValidOnDate(X509Certificate cert, Date date) throws UserCertificateNotYetValidException, UserCertificateExpiredException { + try { + cert.checkValidity(date); + } catch (CertificateNotYetValidException e) { + throw new UserCertificateNotYetValidException(e); + } catch (CertificateExpiredException e) { + throw new UserCertificateExpiredException(e); + } + } + + public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certificate, + Set trustedCACertificateAnchors, + CertStore trustedCACertificateCertStore) throws CertificateNotTrustedException, JceException { + 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 buildCertStoreFromCertificates() below why we use the default JCE provider. + final CertPathBuilder certPathBuilder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType()); + final PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) certPathBuilder.build(pkixBuilderParameters); + + return result.getTrustAnchor().getTrustedCert(); + + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + throw new JceException(e); + } catch (CertPathBuilderException e) { + throw new CertificateNotTrustedException(certificate, e); + } + } + + public static Set buildTrustAnchorsFromCertificates(Collection certificates) { + return certificates.stream() + .map(cert -> new TrustAnchor(cert, null)) + .collect(Collectors.toSet()); + } + + public static Set buildTrustAnchorsFromCertificate(X509Certificate certificate) { + return buildTrustAnchorsFromCertificates(Collections.singleton(certificate)); + } + + public static CertStore buildCertStoreFromCertificates(Collection certificates) throws JceException { + // 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. + try { + return CertStore.getInstance("Collection", new CollectionCertStoreParameters(certificates)); + } catch (GeneralSecurityException e) { + throw new JceException(e); + } + } + + public static CertStore buildCertStoreFromCertificate(X509Certificate certificate) throws JceException { + return buildCertStoreFromCertificates(Collections.singleton(certificate)); + } + + private CertificateValidator() { + throw new IllegalStateException("Utility class"); + } + +} diff --git a/src/main/java/org/webeid/security/exceptions/AiaOcspResponderConfigurationException.java b/src/main/java/org/webeid/security/exceptions/AiaOcspResponderConfigurationException.java new file mode 100644 index 00000000..c81636a9 --- /dev/null +++ b/src/main/java/org/webeid/security/exceptions/AiaOcspResponderConfigurationException.java @@ -0,0 +1,7 @@ +package org.webeid.security.exceptions; + +public class AiaOcspResponderConfigurationException extends TokenValidationException { + public AiaOcspResponderConfigurationException(String message) { + super(message); + } +} diff --git a/src/main/java/org/webeid/security/exceptions/UserCertificateNotTrustedException.java b/src/main/java/org/webeid/security/exceptions/CertificateNotTrustedException.java similarity index 76% rename from src/main/java/org/webeid/security/exceptions/UserCertificateNotTrustedException.java rename to src/main/java/org/webeid/security/exceptions/CertificateNotTrustedException.java index f9c41969..f44dee43 100644 --- a/src/main/java/org/webeid/security/exceptions/UserCertificateNotTrustedException.java +++ b/src/main/java/org/webeid/security/exceptions/CertificateNotTrustedException.java @@ -22,15 +22,15 @@ package org.webeid.security.exceptions; +import java.security.cert.X509Certificate; + /** - * Thrown when the user certificate is not trusted. + * Thrown when the given certificate is not signed by a trusted CA. */ -public class UserCertificateNotTrustedException extends TokenValidationException { - public UserCertificateNotTrustedException() { - super("User certificate is not trusted"); - } +public class CertificateNotTrustedException extends TokenValidationException { - public UserCertificateNotTrustedException(String msg) { - super("User certificate is not trusted: " + msg); + public CertificateNotTrustedException(X509Certificate certificate, Throwable e) { + super("Certificate " + certificate.getSubjectDN() + " is not trusted", e); } + } diff --git a/src/main/java/org/webeid/security/exceptions/OCSPCertificateException.java b/src/main/java/org/webeid/security/exceptions/OCSPCertificateException.java new file mode 100644 index 00000000..80ca6894 --- /dev/null +++ b/src/main/java/org/webeid/security/exceptions/OCSPCertificateException.java @@ -0,0 +1,13 @@ +package org.webeid.security.exceptions; + +public class OCSPCertificateException extends TokenValidationException { + + public OCSPCertificateException(String message) { + super(message); + } + + public OCSPCertificateException(String message, Throwable exception) { + super(message, exception); + } + +} diff --git a/src/main/java/org/webeid/security/exceptions/OriginMismatchException.java b/src/main/java/org/webeid/security/exceptions/OriginMismatchException.java index ac23787c..2c666d4a 100644 --- a/src/main/java/org/webeid/security/exceptions/OriginMismatchException.java +++ b/src/main/java/org/webeid/security/exceptions/OriginMismatchException.java @@ -35,6 +35,6 @@ public OriginMismatchException() { } public OriginMismatchException(Throwable cause) { - super(MESSAGE + ":", cause); + super(MESSAGE, cause); } } diff --git a/src/main/java/org/webeid/security/exceptions/TokenExpiredException.java b/src/main/java/org/webeid/security/exceptions/TokenExpiredException.java index 548b1d31..20ea7898 100644 --- a/src/main/java/org/webeid/security/exceptions/TokenExpiredException.java +++ b/src/main/java/org/webeid/security/exceptions/TokenExpiredException.java @@ -28,6 +28,6 @@ public class TokenExpiredException extends TokenValidationException { public TokenExpiredException(Throwable cause) { - super("Token has expired:", cause); + super("Token has expired", cause); } } diff --git a/src/main/java/org/webeid/security/exceptions/TokenParseException.java b/src/main/java/org/webeid/security/exceptions/TokenParseException.java index 8c36dad1..5a95b899 100644 --- a/src/main/java/org/webeid/security/exceptions/TokenParseException.java +++ b/src/main/java/org/webeid/security/exceptions/TokenParseException.java @@ -27,7 +27,7 @@ */ public class TokenParseException extends TokenValidationException { public TokenParseException(Throwable cause) { - super("Error parsing token:", cause); + super("Error parsing token", cause); } public TokenParseException(String message) { diff --git a/src/main/java/org/webeid/security/exceptions/TokenSignatureValidationException.java b/src/main/java/org/webeid/security/exceptions/TokenSignatureValidationException.java index 8ef02087..d4af2111 100644 --- a/src/main/java/org/webeid/security/exceptions/TokenSignatureValidationException.java +++ b/src/main/java/org/webeid/security/exceptions/TokenSignatureValidationException.java @@ -27,6 +27,6 @@ */ public class TokenSignatureValidationException extends TokenValidationException { public TokenSignatureValidationException(Throwable cause) { - super("Token signature validation has failed:", cause); + super("Token signature validation has failed", cause); } } diff --git a/src/main/java/org/webeid/security/exceptions/UserCertificateExpiredException.java b/src/main/java/org/webeid/security/exceptions/UserCertificateExpiredException.java index 64ab5395..7cfa8d42 100644 --- a/src/main/java/org/webeid/security/exceptions/UserCertificateExpiredException.java +++ b/src/main/java/org/webeid/security/exceptions/UserCertificateExpiredException.java @@ -27,6 +27,6 @@ */ public class UserCertificateExpiredException extends TokenValidationException { public UserCertificateExpiredException(Throwable cause) { - super("User certificate has expired:", cause); + super("User certificate has expired", cause); } } diff --git a/src/main/java/org/webeid/security/exceptions/UserCertificateNotYetValidException.java b/src/main/java/org/webeid/security/exceptions/UserCertificateNotYetValidException.java index 3d677b88..ed298f3b 100644 --- a/src/main/java/org/webeid/security/exceptions/UserCertificateNotYetValidException.java +++ b/src/main/java/org/webeid/security/exceptions/UserCertificateNotYetValidException.java @@ -27,6 +27,6 @@ */ public class UserCertificateNotYetValidException extends TokenValidationException { public UserCertificateNotYetValidException(Throwable cause) { - super("User certificate is not yet valid:", cause); + super("User certificate is not yet valid", cause); } } diff --git a/src/main/java/org/webeid/security/exceptions/UserCertificateRevocationCheckFailedException.java b/src/main/java/org/webeid/security/exceptions/UserCertificateOCSPCheckFailedException.java similarity index 80% rename from src/main/java/org/webeid/security/exceptions/UserCertificateRevocationCheckFailedException.java rename to src/main/java/org/webeid/security/exceptions/UserCertificateOCSPCheckFailedException.java index 21fbe8de..63ae1a6c 100644 --- a/src/main/java/org/webeid/security/exceptions/UserCertificateRevocationCheckFailedException.java +++ b/src/main/java/org/webeid/security/exceptions/UserCertificateOCSPCheckFailedException.java @@ -25,12 +25,12 @@ /** * Thrown when user certificate revocation check with OCSP fails. */ -public class UserCertificateRevocationCheckFailedException extends TokenValidationException { - public UserCertificateRevocationCheckFailedException(Throwable cause) { - super("User certificate revocation check has failed:", cause); +public class UserCertificateOCSPCheckFailedException extends TokenValidationException { + public UserCertificateOCSPCheckFailedException(Throwable cause) { + super("User certificate revocation check has failed", cause); } - public UserCertificateRevocationCheckFailedException(String message) { + public UserCertificateOCSPCheckFailedException(String message) { super("User certificate revocation check has failed: " + message); } } diff --git a/src/main/java/org/webeid/security/exceptions/UserCertificateParseException.java b/src/main/java/org/webeid/security/exceptions/UserCertificateParseException.java index 9eb42495..891b410f 100644 --- a/src/main/java/org/webeid/security/exceptions/UserCertificateParseException.java +++ b/src/main/java/org/webeid/security/exceptions/UserCertificateParseException.java @@ -27,6 +27,6 @@ */ public class UserCertificateParseException extends TokenValidationException { public UserCertificateParseException(Throwable cause) { - super("Error parsing certificate:", cause); + super("Error parsing certificate", cause); } } diff --git a/src/main/java/org/webeid/security/util/OcspUrls.java b/src/main/java/org/webeid/security/util/OcspUrls.java deleted file mode 100644 index 67c6e984..00000000 --- a/src/main/java/org/webeid/security/util/OcspUrls.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.webeid.security.util; - -import java.net.URI; - -public class OcspUrls { - public static final URI ESTEID_2015 = URI.create("http://aia.sk.ee/esteid2015"); - - private OcspUrls() { - throw new IllegalStateException("Constants class"); - } - -} diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java b/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java index a16b97ef..7e248e2c 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java @@ -24,6 +24,8 @@ import com.google.common.collect.Sets; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; +import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; import org.webeid.security.validator.validators.OriginValidator; import javax.cache.Cache; @@ -36,8 +38,10 @@ import java.util.Objects; import static org.webeid.security.nonce.NonceGeneratorBuilder.requirePositiveDuration; -import static org.webeid.security.util.OcspUrls.ESTEID_2015; -import static org.webeid.security.util.SubjectCertificatePolicies.*; +import static org.webeid.security.util.SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY; +import static org.webeid.security.util.SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1; +import static org.webeid.security.util.SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V2; +import static org.webeid.security.util.SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V3; /** * Stores configuration parameters for {@link AuthTokenValidatorImpl}. @@ -50,6 +54,8 @@ final class AuthTokenValidationConfiguration { private boolean isUserCertificateRevocationCheckWithOcspEnabled = true; private Duration ocspRequestTimeout = Duration.ofSeconds(5); private Duration allowedClientClockSkew = Duration.ofMinutes(3); + private AiaOcspServiceConfiguration aiaOcspServiceConfiguration; + private DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration; private boolean isSiteCertificateFingerprintValidationEnabled = false; private String siteCertificateSha256Fingerprint; // Don't allow Estonian Mobile-ID policy by default. @@ -59,8 +65,6 @@ final class AuthTokenValidationConfiguration { ESTEID_SK_2015_MOBILE_ID_POLICY_V3, ESTEID_SK_2015_MOBILE_ID_POLICY ); - // Disable OCSP nonce extension for EstEID 2015 cards by default. - private Collection nonceDisabledOcspUrls = Sets.newHashSet(ESTEID_2015); AuthTokenValidationConfiguration() { } @@ -72,10 +76,11 @@ private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) this.isUserCertificateRevocationCheckWithOcspEnabled = other.isUserCertificateRevocationCheckWithOcspEnabled; this.ocspRequestTimeout = other.ocspRequestTimeout; this.allowedClientClockSkew = other.allowedClientClockSkew; + this.aiaOcspServiceConfiguration = other.aiaOcspServiceConfiguration; + this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration; this.isSiteCertificateFingerprintValidationEnabled = other.isSiteCertificateFingerprintValidationEnabled; this.siteCertificateSha256Fingerprint = other.siteCertificateSha256Fingerprint; this.disallowedSubjectCertificatePolicies = new HashSet<>(other.disallowedSubjectCertificatePolicies); - this.nonceDisabledOcspUrls = new HashSet<>(other.nonceDisabledOcspUrls); } void setSiteOrigin(URI siteOrigin) { @@ -122,6 +127,22 @@ Duration getAllowedClientClockSkew() { return allowedClientClockSkew; } + public AiaOcspServiceConfiguration getAiaOcspServiceConfiguration() { + return aiaOcspServiceConfiguration; + } + + public void setAiaOcspServiceConfiguration(AiaOcspServiceConfiguration aiaOcspServiceConfiguration) { + this.aiaOcspServiceConfiguration = aiaOcspServiceConfiguration; + } + + public DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() { + return designatedOcspServiceConfiguration; + } + + public void setDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration) { + this.designatedOcspServiceConfiguration = designatedOcspServiceConfiguration; + } + boolean isSiteCertificateFingerprintValidationEnabled() { return isSiteCertificateFingerprintValidationEnabled; } @@ -139,10 +160,6 @@ public Collection getDisallowedSubjectCertificatePolicies( return disallowedSubjectCertificatePolicies; } - public Collection getNonceDisabledOcspUrls() { - return nonceDisabledOcspUrls; - } - /** * Checks that the configuration parameters are valid. * @@ -156,6 +173,18 @@ void validate() { if (trustedCACertificates.isEmpty()) { throw new IllegalArgumentException("At least one trusted certificate authority must be provided"); } + if (isUserCertificateRevocationCheckWithOcspEnabled) { + if (aiaOcspServiceConfiguration == null && designatedOcspServiceConfiguration == null) { + throw new IllegalArgumentException("Either AIA or designated OCSP service configuration must be provided"); + } + if (aiaOcspServiceConfiguration != null && designatedOcspServiceConfiguration != null) { + throw new IllegalArgumentException("AIA and designated OCSP service configuration cannot provided together, " + + "please provide either one or the other"); + } + } else if (aiaOcspServiceConfiguration != null || designatedOcspServiceConfiguration != null) { + throw new IllegalArgumentException("When user certificate OCSP check is disabled, " + + "AIA or designated OCSP service configuration should not be provided"); + } requirePositiveDuration(ocspRequestTimeout, "OCSP request timeout"); requirePositiveDuration(allowedClientClockSkew, "Allowed client clock skew"); if (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..8e3561ea 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java @@ -26,6 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.webeid.security.exceptions.JceException; +import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; +import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; import javax.cache.Cache; import java.net.URI; @@ -84,7 +86,10 @@ 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)); + } return this; } @@ -125,20 +130,19 @@ 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; } - /** - * Adds the given URLs to the list of OCSP URLs for which the nonce protocol extension will be disabled. - * The OCSP URL is extracted from the user certificate and some OCSP services don't support the nonce extension. - * - * @param urls OCSP URLs for which the nonce protocol extension will be disabled - * @return the builder instance for method chaining - */ - public AuthTokenValidatorBuilder withNonceDisabledOcspUrls(URI... urls) { - Collections.addAll(configuration.getNonceDisabledOcspUrls(), urls); - LOG.debug("OCSP URLs for which the nonce protocol extension is disabled set to {}", configuration.getNonceDisabledOcspUrls()); + public AuthTokenValidatorBuilder withAiaOcspServiceConfiguration(AiaOcspServiceConfiguration serviceConfiguration) { + configuration.setAiaOcspServiceConfiguration(serviceConfiguration); + LOG.debug("Using AIA OCSP service configuration"); + return this; + } + + public AuthTokenValidatorBuilder withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration) { + configuration.setDesignatedOcspServiceConfiguration(serviceConfiguration); + LOG.debug("Using designated OCSP service configuration"); return this; } 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..363e1b18 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java @@ -23,22 +23,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.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 +52,15 @@ 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; + // 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 ocspClientSupplier; private final ValidatorBatch simpleSubjectCertificateValidators; private final ValidatorBatch tokenBodyValidators; private final Set trustedCACertificateAnchors; private final CertStore trustedCACertificateCertStore; + private OcspServiceProvider ocspServiceProvider; /** * @param configuration configuration parameters for the token validator @@ -71,11 +72,7 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { // 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() - ); + this.ocspClientSupplier = Suppliers.memoize(() -> OcspClientImpl.build(configuration.getOcspRequestTimeout())); simpleSubjectCertificateValidators = ValidatorBatch.createFrom( FunctionalSubjectCertificateValidators::validateCertificateExpiry, @@ -90,18 +87,13 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { ); // 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); + trustedCACertificateAnchors = buildTrustAnchorsFromCertificates(configuration.getTrustedCACertificates()); + trustedCACertificateCertStore = buildCertStoreFromCertificates(configuration.getTrustedCACertificates()); + + if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) { + ocspServiceProvider = configuration.getAiaOcspServiceConfiguration() != null ? + new OcspServiceProvider(configuration.getAiaOcspServiceConfiguration()) : + new OcspServiceProvider(configuration.getDesignatedOcspServiceConfiguration()); } } @@ -153,7 +145,7 @@ private ValidatorBatch getCertTrustValidators() { return ValidatorBatch.createFrom( certTrustedValidator::validateCertificateTrusted ).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(), - new SubjectCertificateNotRevokedValidator(certTrustedValidator, httpClientSupplier.get(), configuration.getNonceDisabledOcspUrls())::validateCertificateNotRevoked + new SubjectCertificateNotRevokedValidator(certTrustedValidator, ocspClientSupplier.get(), ocspServiceProvider)::validateCertificateNotRevoked ); } } 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..c78853c9 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java @@ -0,0 +1,13 @@ +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..b1f6d826 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java @@ -0,0 +1,74 @@ +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 CA's OCSP responder server. + * + * @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..f6958b0f 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java @@ -43,18 +43,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 +60,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 +74,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 +89,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/OcspServiceProvider.java b/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java new file mode 100644 index 00000000..93bf9c98 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java @@ -0,0 +1,33 @@ +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.X509Certificate; + +public class OcspServiceProvider { + + private final AiaOcspServiceConfiguration aiaOcspServiceConfiguration; + private final DesignatedOcspService designatedOcspService; + + public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration) { + this.designatedOcspService = new DesignatedOcspService(designatedOcspServiceConfiguration); + this.aiaOcspServiceConfiguration = null; + } + + public OcspServiceProvider(AiaOcspServiceConfiguration aiaOcspServiceConfiguration) { + this.aiaOcspServiceConfiguration = aiaOcspServiceConfiguration; + this.designatedOcspService = null; + } + + public OcspService getService(X509Certificate certificate) throws TokenValidationException { + return designatedOcspService != null ? + designatedOcspService : + new AiaOcspService(aiaOcspServiceConfiguration, certificate); + } + +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java b/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java index 168cf369..cacdf30c 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java @@ -39,49 +39,64 @@ package org.webeid.security.validator.ocsp; -import okhttp3.*; -import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.BERTags; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.DLTaggedObject; 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 org.webeid.security.exceptions.OCSPCertificateException; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; -import java.util.Objects; public final class OcspUtils { - private static final Logger LOG = LoggerFactory.getLogger(OcspUtils.class); - + /** + * 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"; /** * The OID for OCSP responder URLs. *

- * http://www.alvestrand.no/objectid/1.3.6.1.5.5.7.48.1.html + * https://oidref.com/1.3.6.1.5.5.7.48.1 */ 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() { + public static void validateHasSigningExtension(X509Certificate cert) throws OCSPCertificateException { + try { + if (cert.getExtendedKeyUsage() == null || !cert.getExtendedKeyUsage().contains(OID_OCSP_SIGNING)) { + throw new OCSPCertificateException("Certificate " + cert.getSubjectDN() + + " does not contain the key usage extension for OCSP response signing"); + } + } catch (CertificateParsingException e) { + throw new OCSPCertificateException("Certificate parsing failed:", e); + } } /** * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one. */ - public static URI ocspUri(X509Certificate certificate) throws IOException { + public static URI ocspUri(X509Certificate certificate) { final byte[] value = certificate.getExtensionValue(Extension.authorityInfoAccess.getId()); if (value == null) { return null; } - final ASN1Primitive authorityInfoAccess = JcaX509ExtensionUtils.parseExtensionValue(value); + final ASN1Primitive authorityInfoAccess; + try { + authorityInfoAccess = JcaX509ExtensionUtils.parseExtensionValue(value); + } catch (IOException e) { + return null; + } if (!(authorityInfoAccess instanceof DLSequence)) { return null; } @@ -96,7 +111,12 @@ public static URI ocspUri(X509Certificate certificate) throws IOException { return null; } - final byte[] encoded = taggedObject.getEncoded(); + final byte[] encoded; + try { + encoded = taggedObject.getEncoded(); + } catch (IOException e) { + return null; + } int length = (int) encoded[1] & 0xFF; final String uri = new String(encoded, 2, length, StandardCharsets.UTF_8); return URI.create(uri); @@ -124,36 +144,7 @@ private static T findObject(DLSequence sequence, ASN1ObjectIdentifier oid, C 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()); - } - } + private OcspUtils() { + throw new IllegalStateException("Utility class"); } - } diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspResponderConfiguration.java b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspResponderConfiguration.java new file mode 100644 index 00000000..a6e4db5e --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspResponderConfiguration.java @@ -0,0 +1,46 @@ +package org.webeid.security.validator.ocsp.service; + +import org.webeid.security.exceptions.JceException; + +import java.net.URI; +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.*; + +public class AiaOcspResponderConfiguration { + + private final URI responderUrl; + private final boolean doesSupportNonce; + private final Set trustedCACertificateAnchors; + private final CertStore trustedCACertificateCertStore; + + public AiaOcspResponderConfiguration(URI responderUrl, X509Certificate responderTrustedCACertificate, boolean doesSupportNonce) throws JceException { + this.responderUrl = responderUrl; + this.trustedCACertificateAnchors = buildTrustAnchorsFromCertificate(responderTrustedCACertificate); + this.trustedCACertificateCertStore = buildCertStoreFromCertificate(responderTrustedCACertificate); + this.doesSupportNonce = doesSupportNonce; + } + + public AiaOcspResponderConfiguration(URI responderUrl, X509Certificate responderTrustedCACertificate) throws JceException { + this(responderUrl, responderTrustedCACertificate, true); + } + + public boolean doesSupportNonce() { + return doesSupportNonce; + } + + public URI getResponderUrl() { + return responderUrl; + } + + public Set getTrustedCACertificateAnchors() { + return trustedCACertificateAnchors; + } + + public CertStore getTrustedCACertificateCertStore() { + return trustedCACertificateCertStore; + } +} 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..4cc1e539 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java @@ -0,0 +1,70 @@ +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 org.webeid.security.validator.ocsp.OcspUtils; + +import java.net.URI; +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; +import static org.webeid.security.certificate.CertificateValidator.validateIsSignedByTrustedCA; +import static org.webeid.security.validator.ocsp.OcspUtils.validateHasSigningExtension; + +/** + * 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 URI url; + private final AiaOcspResponderConfiguration configuration; + + public AiaOcspService(AiaOcspServiceConfiguration aiaOcspServiceConfiguration, X509Certificate certificate) throws TokenValidationException { + this.url = getOcspAiaUrlFromCertificate(certificate); + Objects.requireNonNull(aiaOcspServiceConfiguration, "aiaOcspServiceConfiguration"); + this.configuration = aiaOcspServiceConfiguration.getResponderConfigurationForUrl(this.url); + } + + @Override + public boolean doesSupportNonce() { + return configuration.doesSupportNonce(); + } + + @Override + public URI getUrl() { + return url; + } + + @Override + public void validateResponderCertificate(X509CertificateHolder cert, Date producedAt) throws TokenValidationException { + try { + final X509Certificate certificate = certificateConverter.getCertificate(cert); + certificateIsValidOnDate(certificate, producedAt); + validateHasSigningExtension(certificate); + validateIsSignedByTrustedCA( + certificate, + configuration.getTrustedCACertificateAnchors(), + configuration.getTrustedCACertificateCertStore() + ); + } catch (CertificateException e) { + throw new OCSPCertificateException("Invalid certificate", e); + } + } + + private static URI getOcspAiaUrlFromCertificate(X509Certificate certificate) throws TokenValidationException { + final URI uri = OcspUtils.ocspUri(Objects.requireNonNull(certificate, "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..2357998d --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java @@ -0,0 +1,26 @@ +package org.webeid.security.validator.ocsp.service; + +import org.webeid.security.exceptions.AiaOcspResponderConfigurationException; + +import java.net.URI; +import java.util.HashMap; + +public class AiaOcspServiceConfiguration { + + private final HashMap configs = new HashMap<>(); + + public AiaOcspServiceConfiguration(AiaOcspResponderConfiguration... aiaOcspResponderConfiguration) { + for (AiaOcspResponderConfiguration conf : aiaOcspResponderConfiguration) { + configs.put(conf.getResponderUrl(), conf); + } + } + + public AiaOcspResponderConfiguration getResponderConfigurationForUrl(URI url) throws AiaOcspResponderConfigurationException { + final AiaOcspResponderConfiguration configuration = configs.get(url); + if (configuration == null) { + throw new AiaOcspResponderConfigurationException("No AIA OCSP responder configuration exists for URL " + url); + } + return configuration; + } + +} 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..badd2c38 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java @@ -0,0 +1,53 @@ +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.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 DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration; + private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + + public DesignatedOcspService(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration) { + this.designatedOcspServiceConfiguration = Objects.requireNonNull(designatedOcspServiceConfiguration, "designatedOcspServiceConfiguration"); + } + + @Override + public boolean doesSupportNonce() { + return designatedOcspServiceConfiguration.doesSupportNonce(); + } + + @Override + public URI getUrl() { + return designatedOcspServiceConfiguration.getOcspServiceUrl(); + } + + @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 (!designatedOcspServiceConfiguration.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); + } catch (CertificateException e) { + throw new OCSPCertificateException("X509CertificateHolder conversion to X509Certificate failed"); + } + } +} 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..fba97cb2 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java @@ -0,0 +1,39 @@ +package org.webeid.security.validator.ocsp.service; + +import org.webeid.security.exceptions.OCSPCertificateException; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.Objects; + +import static org.webeid.security.validator.ocsp.OcspUtils.validateHasSigningExtension; + +public class DesignatedOcspServiceConfiguration { + + private final URI ocspServiceUrl; + private final X509Certificate responderCertificate; + private final boolean doesSupportNonce; + + public DesignatedOcspServiceConfiguration(URI ocspServiceUrl, X509Certificate responderCertificate, boolean doesSupportNonce) throws OCSPCertificateException { + this.ocspServiceUrl = Objects.requireNonNull(ocspServiceUrl, "OCSP service URL"); + this.responderCertificate = Objects.requireNonNull(responderCertificate, "OCSP responder certificate"); + validateHasSigningExtension(responderCertificate); + this.doesSupportNonce = doesSupportNonce; + } + + public DesignatedOcspServiceConfiguration(URI ocspServiceUrl, X509Certificate responderCertificate) throws OCSPCertificateException { + this(ocspServiceUrl, responderCertificate, true); + } + + public URI getOcspServiceUrl() { + return ocspServiceUrl; + } + + public X509Certificate getResponderCertificate() { + return responderCertificate; + } + + public boolean doesSupportNonce() { + return doesSupportNonce; + } +} 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..5bb032c3 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java @@ -0,0 +1,17 @@ +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 getUrl(); + + void validateResponderCertificate(X509CertificateHolder cert, Date date) throws TokenValidationException; + +} diff --git a/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java b/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java index 03e5a81e..959009c6 100644 --- a/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java +++ b/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java @@ -25,15 +25,18 @@ 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; +import static org.webeid.security.certificate.CertificateValidator.certificateIsValidOnDate; + public final class FunctionalSubjectCertificateValidators { private static final Logger LOG = LoggerFactory.getLogger(FunctionalSubjectCertificateValidators.class); @@ -46,15 +49,9 @@ public final class FunctionalSubjectCertificateValidators { * @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); - } + // Use JJWT Clock interface so that the date can be mocked in tests. + certificateIsValidOnDate(actualTokenData.getSubjectCertificate(), DefaultClock.INSTANCE.now()); + 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..6faca5c3 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,183 @@ 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.CertificateStatus; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.RevokedStatus; +import org.bouncycastle.cert.ocsp.SingleResp; +import org.bouncycastle.cert.ocsp.UnknownStatus; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentVerifierProvider; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; 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.UserCertificateOCSPCheckFailedException; import org.webeid.security.exceptions.UserCertificateRevokedException; 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.Objects; 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; - public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator, OkHttpClient httpClient, Collection nonceDisabledOcspUrls) { + static { + Security.addProvider(new BouncyCastleProvider()); + } + + 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.getUrl(), 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 { + // 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"); + } + // We require the responder to include a single certificate in the certs field of the response + // that helps us to verify the responder's signature. + if (basicResponse.getCerts().length != 1) { + throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one responder certificate, " + + "received " + basicResponse.getCerts().length + " certificates 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"); + } + + final X509CertificateHolder responderCert = basicResponse.getCerts()[0]; + + ocspService.validateResponderCertificate(responderCert, basicResponse.getProducedAt()); + validateResponseSignature(basicResponse, responderCert); + validateSubjectCertificateStatus(certStatusResponse); + } + + private 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"); + } + } + + private void validateSubjectCertificateStatus(SingleResp certStatusResponse) throws UserCertificateRevokedException { + final CertificateStatus status = certStatusResponse.getCertStatus(); + if (status == null) { + LOG.debug("OCSP check result is GOOD"); + } else 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 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 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 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/SubjectCertificateTrustedValidator.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java index b6497f52..1ff7b557 100644 --- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java +++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java @@ -22,20 +22,19 @@ package org.webeid.security.validator.validators; -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.TokenValidationException; +import org.webeid.security.exceptions.CertificateNotTrustedException; 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); private final Set trustedCACertificateAnchors; private final CertStore trustedCACertificateCertStore; @@ -50,32 +49,11 @@ 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); } 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..b2aeeccb 100644 --- a/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java +++ b/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java @@ -23,27 +23,34 @@ 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.validator.AuthTokenValidator; import org.webeid.security.validator.AuthTokenValidatorBuilder; +import org.webeid.security.validator.ocsp.service.AiaOcspResponderConfiguration; +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; + 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()); } @@ -52,30 +59,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()) + .withAiaOcspServiceConfiguration(new AiaOcspServiceConfiguration( + new AiaOcspResponderConfiguration(new URI("http://aia.demo.sk.ee/esteid2018"), getTestEsteid2018CA()))) + .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 +96,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/validator/OcspTest.java b/src/test/java/org/webeid/security/validator/OcspTest.java index 41e1170c..7827e598 100644 --- a/src/test/java/org/webeid/security/validator/OcspTest.java +++ b/src/test/java/org/webeid/security/validator/OcspTest.java @@ -28,6 +28,8 @@ 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; @@ -44,7 +46,7 @@ protected void setup() { super.setup(); try { validator = getAuthTokenValidatorWithOcspCheck(cache); - } catch (CertificateException | JceException e) { + } catch (CertificateException | JceException | URISyntaxException | IOException e) { throw new RuntimeException(e); } } @@ -58,7 +60,7 @@ void detectRevokedUserCertificate() { } catch (TokenValidationException e) { assertThat(e).isInstanceOfAny( UserCertificateRevokedException.class, - UserCertificateRevocationCheckFailedException.class + UserCertificateOCSPCheckFailedException.class ); } } @@ -67,13 +69,13 @@ void detectRevokedUserCertificate() { void testTokenCertRsaExpired() { assertThatThrownBy(() -> validator.validate(Tokens.TOKEN_CERT_RSA_EXIPRED)) .isInstanceOf(UserCertificateExpiredException.class) - .hasMessageStartingWith("User certificate has expired:"); + .hasMessageStartingWith("User certificate has expired"); } @Test void testTokenCertEcdsaExpired() { assertThatThrownBy(() -> validator.validate(Tokens.TOKEN_CERT_ECDSA_EXIPRED)) .isInstanceOf(UserCertificateExpiredException.class) - .hasMessageStartingWith("User certificate has expired:"); + .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/ocsp/OcspServiceProviderTest.java b/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java new file mode 100644 index 00000000..f4fb1cea --- /dev/null +++ b/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java @@ -0,0 +1,75 @@ +package org.webeid.security.validator.ocsp; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.webeid.security.exceptions.JceException; +import org.webeid.security.exceptions.OCSPCertificateException; +import org.webeid.security.validator.ocsp.service.AiaOcspResponderConfiguration; +import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; +import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; +import org.webeid.security.validator.ocsp.service.OcspService; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.cert.CertificateException; +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.*; + +class OcspServiceProviderTest { + + @Test + void whenDesignatedOcspServiceConfigurationProvided_thenCreatesDesignatedOcspService() throws Exception { + final OcspServiceProvider ocspServiceProvider = + new OcspServiceProvider(new DesignatedOcspServiceConfiguration(new URI("http://demo.sk.ee/ocsp"), getTestSkOcspResponder2020())); + final OcspService service = ocspServiceProvider.getService(null); + assertThat(service.getUrl()).isEqualTo(new URI("http://demo.sk.ee/ocsp")); + assertThat(service.doesSupportNonce()).isTrue(); + assertThatCode(() -> + service.validateResponderCertificate(new X509CertificateHolder(getTestSkOcspResponder2020().getEncoded()), new Date(1630000000000L))) + .doesNotThrowAnyException(); + } + + @Test + void whenAiaOcspServiceConfigurationProvided_thenCreatesAiaOcspService() throws Exception { + final OcspServiceProvider ocspServiceProvider = getAiaOcspServiceProvider(); + final OcspService service2018 = ocspServiceProvider.getService(getJaakKristjanEsteid2018Cert()); + assertThat(service2018.getUrl()).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.getUrl()).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))); + } + + @NotNull + private OcspServiceProvider getAiaOcspServiceProvider() throws URISyntaxException, CertificateException, JceException, IOException { + return new OcspServiceProvider(new AiaOcspServiceConfiguration( + new AiaOcspResponderConfiguration(new URI("http://aia.demo.sk.ee/esteid2018"), getTestEsteid2018CA()), + new AiaOcspResponderConfiguration(new URI("http://aia.demo.sk.ee/esteid2015"), getTestEsteid2015CA(), false) + )); + } +} 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..7b670df2 --- /dev/null +++ b/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java @@ -0,0 +1,319 @@ +package org.webeid.security.validator.validators; + +import com.google.common.io.ByteStreams; +import okhttp3.*; +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.Test; +import org.webeid.security.exceptions.*; +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 org.webeid.security.validator.ocsp.service.AiaOcspResponderConfiguration; +import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; +import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +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.*; + +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 = new OcspServiceProvider( + new DesignatedOcspServiceConfiguration(new URI("http://demo.sk.ee/ocsp"), getTestSkOcspResponder2020())); + final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .doesNotThrowAnyException(); + } + + @Test + void whenValidOcspNonceDisabledConfiguration_thenSucceeds() throws Exception { + final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider( + new DesignatedOcspServiceConfiguration(new URI("http://demo.sk.ee/ocsp"), getTestSkOcspResponder2020(), false)); + final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .doesNotThrowAnyException(); + } + + @Test + void whenOcspUrlIsInvalid_thenThrows() throws Exception { + final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider( + new DesignatedOcspServiceConfiguration(new URI("http://invalid.invalid-tld"), getTestSkOcspResponder2020())); + final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .isInstanceOf(UserCertificateOCSPCheckFailedException.class) + .getCause() + .isInstanceOf(IOException.class) + .hasMessage("invalid.invalid-tld: Name or service not known"); + } + + @Test + void whenOcspRequestFails_thenThrows() throws Exception { + final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider( + new DesignatedOcspServiceConfiguration(new URI("https://web-eid-test.free.beeceptor.com"), getTestSkOcspResponder2020())); + 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(OCSPCertificateException.class) + .getCause() + .isInstanceOf(CertificateParsingException.class) + .hasMessage("java.io.IOException: subject key, java.security.InvalidKeyException: Invalid 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"); + } + + @Test + 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 = new OcspServiceProvider( + new DesignatedOcspServiceConfiguration(new URI("http://demo.sk.ee/ocsp"), getTestSkOcspResponder2020())); + 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"); + } + + 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. + 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 { + final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider(new AiaOcspServiceConfiguration( + new AiaOcspResponderConfiguration(new URI("http://aia.demo.sk.ee/esteid2018"), getTestEsteid2018CA()))); + return new SubjectCertificateNotRevokedValidator(trustedValidator, client, ocspServiceProvider); + } + + 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 0000000000000000000000000000000000000000..7749286c895084bf2d7bacb98b742a01cd684122 GIT binary patch literal 1671 zcmb7EX;4#F6wZ5ji4YT@LXe$^l>i-*`vL)3WD$Z;z=c%QO6{Uz|Nti5Ys|*EzrHLU@nPI%pFormh!Q(M}UdIaK zh5^P>8p=gE=t|&>IygG=jdVG{ z0pJd2jsw?)>%s>~|DWJeK-UnAqw1^;3*qtXQQ++83{VHu5}3bcZn(faG>jMEYabfO z-~f~h6tLCX4T4Anq5{?>;C>3MfFXkFHX*9@YOKZPhws_e%qlnOxqQNRwIF&n`~1KE|CGi>^}+HLlswpy~$=6&4-s8xoQu zPWZNaI-1?K_1m)=i%?y>INNxazUcC3=NFXg{Lh`Ad45@PS*LrIYTEetNUfHrv#Vuz z!^?qN&#WMG!<^jJDzbBy1YKXU#6!Pztk|T2UvZ`;T2i5xe=9g>AlSFJ&0{qDxBce} zGxzCtI8=KbySVY7swKF3L{S$NR�?itslb-bPA?nh!_1G3$MPD2FPu$I{g%r!&R2 z7Wd~~5Wl%Zb&m2WF~aUx^f0H!IT=uI@RRV8ox@z?+~Vy|8D@qT+J=qy&-q8w#2zEW z2o9}C(Yo!lu)kE2_u_8K=yLn6HRI5`n zsa6&tfKJg0aY#En=1H<22%?{K4E76 z{^(l5NYEX2cK46HW>pDOFcBg^RSKAfPlPtG#D~dzIf763E`#%ql=4$-y(^>3bedFk z*cwyfw%J$HX0C%6A_f7?lfEw4%`!hX+RE7Mx;}dwWi`;zyw`=GIRwF%l?)O!n1Kku zM*t;LJ|IH~)L}`4Ag~-PBWvN9L|OzxhV;#HxkTp5W~Zg4v0kka+h=_!puY74eg_fX zK?qo)$)m5ys|CpTg%39H*7z(CSFrN96F~4iumY4^Ey9}vta2Ii_J7@gsrh0B!T=WY zWQp1f7QA2~VE5j606|D5*a=q5V*n|QHPzw8C15GyH`NK_1Y)5~0zwwTV5Q%iuzz|W z#1%-!)-1L_oG9S&1n)=^jx>@a&`6TN$D5UaNm)Xv9QbHRTO#fNue{Q!(oxCzcXy?j zL@X1@#nN=PP?{`b%Q2Z;fMG(RR2YYUlKi%0;F?NQKoFtDWzSej1FN!l*U`=!Cn?sn zJBhYMN6%(Y5V8znN<+$f>|Emwh@a_k>IXiqZaCYfpj)CtH)li_+}E{7H<1W@=XS#^ zk5pEZ4tXR;>CsZo?QFTW^+PAsUsa#xxl6cvJp#_n^fag0JjuBf6Th2TRy19ex5>-q zfckldT}3N)JvnlfKYgd2=Q$TXbg#>X!ff?V+wV{=G&xpz=_SnV;64Ak!`EPhIYw8ESfeZIIag&l~psrr8d>b^~V literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f2c4bd5e3a96329c12fd3a4e084fba1ec8ac4745 GIT binary patch literal 1234 zcmXqLVmW8f#Jp_*GZP~dlR)+!i;gTMh1c=xYUQPW#9TMvW#iOp^Jx3d%gD&e%3x4y z$Zf#M#vIDRCd}mOYN%wO0OD}*NIC{91g932WTs`76sM*ZW#(oUXQvitmlhiu80dl2 zF!QK~xCVzPgT*W&#Yz!Tw+Y0 zwRC;ap8eBK?Dx96KJY)gu7SwWHBsJ@bD2(T-2XKA;k}(o%*lyDo864}PTj#E`<8Dr ztJUs2rOY+K5;^C6xeBFg7r%dTQQ+_XSsPoXt?FOjzy7~`%A{Ryvlp$3*tR!1&+FRc z{5dasczx7v@3lCmJo#gkeqfZR-M5%8pO+l%%N3dd@8Od-t0Q_F;mI&aeah+d^r@eil~|IRTacQgZy*nHp)w0Fl{AQK zHQ}1ORA1oeb49*=M>eLqOEZ`KGjIkekY}+murjb%V79=d%?M;%F^YLbsRjAPnI-u} zmHNp=ImP-Vsl_Gnsj11yMahYJKoR6*2F#woWX8y#DZgeDYcP)`+w|Amhwc0hF7OF> zd}QOP-@FH=&$67Dq2JYOzGs5=^mNWM^1FTNJ)2L&ZVObZU!1wcAav{GGmqtCR$u?y z*(9Co`eWw|?H2~c;UVWQ`3LNOwd>Q&Wem5s6e`(nx$3gPm#h3~_rpLZN$b+vr~hp^ zSyr@t50m#EhdFgSga2~RbNP9A!fEb92grw|H`^HG`HCBR_;lUdc?g0R5AF)sX literal 0 HcmV?d00001 diff --git a/src/test/resources/ocsp_response.der b/src/test/resources/ocsp_response.der new file mode 100644 index 0000000000000000000000000000000000000000..9bf94116a227ca66fc8a779caef5cb3f0807199b GIT binary patch literal 1579 zcmXqLVpHd0WLVI|reM&-Cd0<5&Bn;e%5K2O$kN2d3l!osXkye@SYl9QXl`K2#vIDR z%%kVx>g%tNpQhj%9OCNfq8se3U}Ruuq2TE0sNnA$9H0>78XVy7=i(X!7B}QI;09^r z=3xQJ8cG{Tg1B5fLO_k4E(*cs#9B7%T zdBktNHIvD=nGdGK^X%X&`POicrJE!r}gZ{YeSIay|oIHcqWJkGAi;jEvl@49rc8 zj0_VQ7X%;wdQx`EVKys$mK#Tz^t$H1W;o8c?Ab=fNv~KKzD}3pEq4nzT(l|p*>az( zguI_q#ksttRMd?xGC%zJkt$R3nWqYuZ#yTGbEC7aVNsPY&+-!o%XQbCYmQZG zej;|dv9m8j@?M+K`(KPF_x$<6x!>Js&1$C_le11yoo|k0Uz*~WW_&a++WLoiSyj8< zABH>0Oghh%H<_5oByPfsG{ieB48xT~3|RsT$1 zR+{)^`wn(-JE?@T3!0dZ8Z?*^9u=bb=5UAFgGzbFf@kgV&)MF0VY&LdId+P zoH(zcg`t75AuxdfL6kVJv9W=LktLKn5Xl{w(Ug!A5FZM6l zCMVyYKIVV(;KgrJZAz6d;E57xh;?b0MB0-1xZglNjdUWk)y)PmQ)uk=WZ}IJ1 ze~mr(I%lcMkMN$hr&nAo=43yR&q)q4xPE)>tH&?;ef~*oW$$i^*jMC}XngM2kMd1( z_vVFt5ofo#d2rMFXXf3`Z&Qt0&Y#)Zzh>&h)01~AhGdlOWt?X6=gOV`3ufl3+FyB* zwIrSC?M6-7d`H%(2y2GSBTl`^H1*ynFXdev=r{__6ZK z<C%u7dPHDXuM^>2aI!Ben!UsEG*1S>T zOfyPK3W}}t^^3FhQd9Ly3v%)kQ}oj^b5e`-K}jw?KP?_oM1qo>UP@|_UUE^10T0Md zVUSaq3>Xaf5d{e=3ll2?az0>g>}D`%Y-e&}Z22|i($wV#TEmS`Wq$wUbv??Q$;~nJ ziFNE!x$b%Qy}suy*u1J{2}`ASvO>MJjPwz;=^@_$+`MgGn1oCo54Mo z1#kJj2W2oQsC<7}C?V)#`oA)(ZBL1<#XkxTmSYl9QXl`K2#vIDR z%%kVx>g%tNpQhj%9OCNfq8se3U}Ruuq2TE0sNnA$9H0>78XVy7=i(X!7B}QI;09^r z=3xQJ8cG{Tg1B5fLO_k4E(*c)cJ`r9XcXY|Vg<>c+7#NyFEno%)28t-i z-i1mA3I=kBuwi2n1gQbK%f!Icz|zFj$S`W5q=C4BC_)Vr3x}W2|KBeJ-YqrYW#iOp z^Jx3d%gD&h%D~*j$jG2#Q{Xp|Z;1p?&8mPS{>PHa4LjX+9o=QBY}UVf+N@;O^uBe^ z#EGxPN<8xQ(%zSzFX^w2UG{0-FP^kd&-)ASTSUiqnJoJ$8hS=#Nu&?I{d(D%`!BY5 zR~<^~G=J_<_#z^^Qe)R@F7e8*F%wr>ek(ZkVok}%(^~B&!lw6^?^<>uBCetD-_O0p z+%3s_7(*muwT^$@!IL)kL4&ptu00qeLD)zmOnTS6xE`a}#r5DuU@^ z<`D`3CSOD<21loyIIp3Fp@Fd>Fxde?lsK=kv4Mq=C6qf5nE;sml#mk;BRB!^GXTZ8 zm_R9Mr)ZPvrAbvLC*Pkw=700x#cxt=N|i6;&UQ(EUXsUXez!Mlq5W&_%%nBj1D7u~ zl1zN)TKIW|NME`@Gn=7$Wa;eI1nuRY-ZPX*o;TAgzV)Eu(Uf;0L5KQobnRSvbnR!o zFCq)or7g^F@$FoHjXn4}XQ|4M@Se7(S6nRSWIvG4Ne(i&etYe!$1nPQ{z+_Q?{13N zSLBmueD2tf@=bI1=7oI`XSca|aMSx|=H1S3Q;k~ApV`{KX6nS#lXokIWR&e?oM!Up z%ANlUX6CBeUwM(WCe%uQ^MVQ4sU}H&f82U=+skiPmtMHtF3jT0vDH{I&+R|^#zX16 zd-qI!lNizXvGU91&%2qJ85tNCH{La9yk)=#jB{CjM#ldvEX+*o4F+N$zAA{%14>Dd zvW=P3Ko%s<$0Eie5`J9a@44Tz-@0)8=*X)U+jRVX=NAKckhC(3gn?KC)}q895M~yz zm~=I8HgJUT4VcX^wTnPQj7ILNiIG=Egn*Ef|8tG zN@|f_a#4u^56DhokW-lq7!3Fk1qmw)6DtF9K45O_W-w@MXL4d}`8DOz)a3_S!;Md6 ze*ffkJ<6QP%`xN6)M2n(U z6jTI}f}-s}1eCa=(Fzs8VbO|CskEThtym*2&a#hD45jgnw$7t0sMvSev2 zp@3zquwwYasK|(g{1_G&Z~644Xa)uXBJm#|r?C zC)XnpB!eWd2#`rcS2F@)gIaRS0!k{=o-d(wUvt_PyQ#0Yp}roqJinoQc5B4fa}oC! z<%874kjZS{HHf*l7*0?G&d6NazC@MWR(nnZ{lmY;8cYLI{~im1noNn0J6v&^VGro< z?+DX~5J3aOkWGm`4XaeOIyUE*t$JMW^K0ib^ydbFqfLXp^49FWA1{1+^vRfpel2{> zN_e8|GUa-Jd40|1wIdhi)`nYM#(ZmGLttNh|HYnpo=)8TT|F_D=H(T67I7na35Cxe zJuhfI^Ymel*$j_Z{qouaY>`h+VDW?QS!-^%6!{Z>5NI_5mnVNz`K1TjG5I3R&_y(6 ze{A+I+7Yt|GVwP*&;0yW_F~%*>pL9@CxU_oRGAaGIaF6}V8q<5!E(c5v}0$TVf;%1 zV<6c*qZOdmZ7{;uElGX4@?ee(KZ;G*vt0DTK2qpx%({F zWWs(yol|^awFd128uTh2T|h%PUQIw?1TmMicx_tczn=!irJ-r%ze5G=rqOZeI2;~_%j5Ap5^?Dc zJaNf+BmW=huEOaR2OH%r`rYmN&|8hsugRYR%QkJIz0!zKy7=Q~mr6tXN@bM~w$j69^q=+Wax66@vZE72d|xCz8>wvzcV_mM9v;4i zjFxR2ZT>>HqPS>*$K!!{Rm}Tyu$cbyj7o)x%NM+k5HwfQ*Sa&CH^QYzp?aSLZ z_R49t0*(+#gg;$0cgYWyn=l0ov1dZkc0 zD=~#K1;bKP z<*6b!Jo6!D`15?T*=94fM~OmHZ&KMK+C|@p3)Nv!w_A^N{7FAwR^h#4C42K%b6U2$ zmZej!Fwcao4O!p*+3{$HwFksH5O=Kf%57#+{f+S=jcq1>^wb{L`#`ZIw)<*il>XuA zr{#yBeqFZ1eBJehP2p74-7SOB0oHSK`a36dmviMOS`a~tUuD55;W({o;n>CPowN&# z+@1ZWlx6wpX3MTyG%R5`Gds-!g7UZB0!l0k1u3>c*g|#F;b-Zh zNh=IyF==fnR;{+`mexj_)+a^qfz+f1OR$Q?qz0p{F*RxEgE5B2gz6m@$c{;U>f=7l zo%7vu?%z4*o(qy*)FCP=={rHvx3k(@7O7NfKnP3v2GX?y4vw)HbG%!~NaKO$ppf(M zZZ`;m%Pn{Wo@<6iXaEanSygTlLvK=1G4ohU)A;)V>On)J84uE&p@EV=(6d@+WP4=E zh>(g=QSwHjlI{zTbmJhY8bQJnQHDk|o!sa9`(kRS=J2kwd*|=(ZMX|MRzrqfE5Qtg zW=>Cb$TVQqM0>rQ1u|vO8B+W;wyH{-Ukvtz_KEv@ZDoEg9n7+_)sR$MU+4Aaao`a= zz_~dym}CWGFpyrKc%Y(;2d)4%ryEII$OHL!zMwhXNc>X>TZyYUa5qEK!&ut{8)Iwh zy#9J`RprXcgQp;wRj(Y9M#TSvm~t?K5elQ}f=uyl^<2r&Utjmvil3G)4^-^G!M~UN z)W^f!r<-T2-dlI(+9k{Mgt7gG@X@KJ%G=dv)NcmQdCrTfp)ID7XVFo|a^6ohEnVY| z{%cmB8D;xb&_w1(Ia-288~>m_(X|6q!vjYk(y`8c`sjZ{%JZme3UfmqEj_ zm^$_{qDk1*m^!>zN<^O3lLW_T=(rsr1P-vXnpnouD9zj%3U&1s<>!aPVOQ^qu0SBa z;u${}j<*ol*5@V2iXRj*P_H|*Cl#g!tlbHOs3rpEs(RufFf2o zswAkb`&w6n8VKZdhzHtQS8h=Lx+xuTteUp18pdC8S9_qx)z%Y&jI{xX!3NO9^6cm~ zk_sU2I1A^%V~`znMD3BR|C_I^Cm3I#n870y48BH}Q$J5`E3nSa2kSt*xgi zBmB7Ug0|ALW%je~qR{0Xryspp*IA~agp((RMSImB;`1XDqceAIkGvtg%9S|ja^$$c zyk2Pkq{As(EnZNU`yW|6AGBYYE*fa*&6>RVOXeQGjXiSpohJ@ndh5xBKi@a){u~>a mIbVILcDDHb)ULkg5~Vd!S`(#}JQ9i0y81vSO6x-^t^Wc*ld3%c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..387b8aabda6b4befb8d3194ab1112cf9563d39c5 GIT binary patch literal 267 zcmXqLVr1uHWLVI|$Y9X;myJ`Kjggg=-GGsirSZK%;~Rs Date: Wed, 22 Sep 2021 00:44:01 +0300 Subject: [PATCH 2/3] refactor(OCSP): simplify AIA OCSP configuration, use single truststore for subject and OCSP responder certificates, add trusted CA certifcates validation WE2-432 Signed-off-by: Mart Somermaa --- README.md | 30 +--- pom.xml | 11 +- .../security/certificate/CertificateData.java | 20 ++- .../certificate/CertificateValidator.java | 56 +++++--- ...iaOcspResponderConfigurationException.java | 7 - ....java => CertificateExpiredException.java} | 10 +- ...a => CertificateNotYetValidException.java} | 10 +- .../exceptions/OCSPCertificateException.java | 22 +++ ...UserCertificateInvalidPolicyException.java | 10 -- .../AuthTokenValidationConfiguration.java | 38 ++--- .../validator/AuthTokenValidatorBuilder.java | 37 +++-- .../validator/AuthTokenValidatorImpl.java | 40 +++--- .../security/validator/ocsp/Digester.java | 22 --- .../security/validator/ocsp/OcspClient.java | 22 +++ .../validator/ocsp/OcspClientImpl.java | 24 +++- .../validator/ocsp/OcspRequestBuilder.java | 24 +--- .../validator/ocsp/OcspResponseValidator.java | 133 ++++++++++++++++++ .../validator/ocsp/OcspServiceProvider.java | 57 ++++++-- .../ocsp/{OcspUtils.java => OcspUrl.java} | 59 ++------ .../AiaOcspResponderConfiguration.java | 46 ------ .../ocsp/service/AiaOcspService.java | 61 +++++--- .../service/AiaOcspServiceConfiguration.java | 58 ++++++-- .../ocsp/service/DesignatedOcspService.java | 44 ++++-- .../DesignatedOcspServiceConfiguration.java | 76 ++++++++-- .../validator/ocsp/service/OcspService.java | 24 +++- .../CertificateExpiryValidator.java | 63 +++++++++ ...SubjectCertificateNotRevokedValidator.java | 89 ++++++------ .../SubjectCertificatePolicyValidator.java | 8 +- ...> SubjectCertificatePurposeValidator.java} | 20 +-- .../SubjectCertificateTrustedValidator.java | 6 +- .../testutil/AuthTokenValidators.java | 13 +- .../org/webeid/security/testutil/Dates.java | 6 +- .../security/testutil/OcspServiceMaker.java | 80 +++++++++++ .../webeid/security/validator/OcspTest.java | 28 ++-- .../security/validator/ValidateTest.java | 45 ++++-- .../ocsp/OcspResponseValidatorTest.java | 70 +++++++++ .../ocsp/OcspServiceProviderTest.java | 39 +++-- .../security/validator/ocsp/OcspUrlTest.java | 41 ++++++ ...ectCertificateNotRevokedValidatorTest.java | 89 ++++++++---- .../ocsp_response_with_2_responder_certs.der | Bin 2931 -> 2980 bytes 40 files changed, 1060 insertions(+), 478 deletions(-) delete mode 100644 src/main/java/org/webeid/security/exceptions/AiaOcspResponderConfigurationException.java rename src/main/java/org/webeid/security/exceptions/{UserCertificateNotYetValidException.java => CertificateExpiredException.java} (76%) rename src/main/java/org/webeid/security/exceptions/{UserCertificateExpiredException.java => CertificateNotYetValidException.java} (77%) delete mode 100644 src/main/java/org/webeid/security/exceptions/UserCertificateInvalidPolicyException.java create mode 100644 src/main/java/org/webeid/security/validator/ocsp/OcspResponseValidator.java rename src/main/java/org/webeid/security/validator/ocsp/{OcspUtils.java => OcspUrl.java} (58%) delete mode 100644 src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspResponderConfiguration.java create mode 100644 src/main/java/org/webeid/security/validator/validators/CertificateExpiryValidator.java rename src/main/java/org/webeid/security/validator/validators/{FunctionalSubjectCertificateValidators.java => SubjectCertificatePurposeValidator.java} (76%) create mode 100644 src/test/java/org/webeid/security/testutil/OcspServiceMaker.java create mode 100644 src/test/java/org/webeid/security/validator/ocsp/OcspResponseValidatorTest.java create mode 100644 src/test/java/org/webeid/security/validator/ocsp/OcspUrlTest.java diff --git a/README.md b/README.md index a06e1536..3a0e32d2 100644 --- a/README.md +++ b/README.md @@ -143,11 +143,6 @@ import java.security.cert.X509Certificate; ... ``` -## 5. Add trusted OCSP responder certificates - -- AIA -- Designated - ## 5. Configure the authentication token validator Once the prerequisites have been met, the authentication token validator itself can be configured. @@ -240,8 +235,6 @@ try { - [Nonce generation](#nonce-generation) - [Basic usage](#basic-usage-1) - [Extended configuration](#extended-configuration-1) -- [Frequently asked questions](#frequently-asked-questions) - - [How can I find the AIA OCSP service URLs?](#how-can-i-find-the-aia-ocsp-service-urls) # Introduction @@ -294,12 +287,13 @@ toTitleCase(CertUtil.getSubjectSurname(userCertificate)); // "Jõeorg" The following additional configuration options are available in `AuthTokenValidatorBuilder`: -- `withSiteCertificateSha256Fingerprint(String siteCertificateFingerprint)` – turns on origin website certificate fingerprint validation. The validator checks that the site certificate fingerprint from the authentication token matches with the provided site certificate SHA-256 fingerprint. This disables powerful man-in-the-middle attacks where attackers are able to issue falsified certificates for the origin, but also disables TLS proxy usage. Due to the technical limitations of web browsers, certificate fingerprint validation currently works only with Firefox. The provided certificate SHA-256 fingerprint should have the prefix `urn:cert:sha-256:` followed by the hexadecimal encoding of the hash value octets as specified in [URN Namespace for Certificates](https://tools.ietf.org/id/draft-seantek-certspec-01.html). Certificate fingerprint validation is disabled by default. -- `withoutUserCertificateRevocationCheckWithOcsp()` – turns off user certificate revocation check with OCSP. The OCSP URL is extracted from the user certificate AIA extension. OCSP check is enabled by default. +- `withSiteCertificateSha256Fingerprint(String siteCertificateFingerprint)` – turns on origin website certificate fingerprint validation. The validator checks that the site certificate fingerprint from the authentication token matches with the provided site certificate SHA-256 fingerprint. This disables powerful man-in-the-middle attacks where attackers are able to issue falsified certificates for the origin, but also disables TLS proxy usage. Due to the technical limitations of web browsers, certificate fingerprint validation is an experimental feature that currently works only with Firefox. The provided certificate SHA-256 fingerprint should have the prefix `urn:cert:sha-256:` followed by the hexadecimal encoding of the hash value octets as specified in [URN Namespace for Certificates](https://tools.ietf.org/id/draft-seantek-certspec-01.html). Certificate fingerprint validation is disabled by default. +- `withoutUserCertificateRevocationCheckWithOcsp()` – turns off user certificate revocation check with OCSP. OCSP check is enabled by default and the OCSP responder access location URL is extracted from the user certificate AIA extension unless a designated OCSP service is activated. +- `withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration)` – activates the provided designated OCSP responder service configuration for user certificate revocation check with OCSP. The designated service is only used for checking the status of the certificates whose issuers are supported by the service, for other certificates the default AIA extension service access location will be used. See configuration examples in `testutil.OcspServiceMaker.getDesignatedOcspServiceConfiguration()`. - `withOcspRequestTimeout(Duration ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds. - `withAllowedClientClockSkew(Duration allowedClockSkew)` – sets the tolerated clock skew of the client computer when verifying the token expiration. Default value is 3 minutes. - `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` – adds the given policies to the list of disallowed user certificate policies. In order for the user certificate to be considered valid, it must not contain any policies present in this list. Contains the Estonian Mobile-ID policies by default as it must not be possible to authenticate with a Mobile-ID certificate when an eID smart card is expected. -- `withNonceDisabledOcspUrls(URI... urls)` – adds the given URLs to the list of OCSP URLs for which the nonce protocol extension will be disabled. Some OCSP services don't support the nonce extension. Contains the ESTEID-2015 OCSP URL by default. +- `withNonceDisabledOcspUrls(URI... urls)` – adds the given URLs to the list of OCSP responder access location URLs for which the nonce protocol extension will be disabled. Some OCSP responders don't support the nonce extension. Contains the ESTEID-2015 OCSP responder URL by default. Extended configuration example: @@ -318,9 +312,9 @@ AuthTokenValidator validator = new AuthTokenValidatorBuilder() ### Certificates' *Authority Information Access* (AIA) extension -It is assumed that the AIA extension that contains the certificates’ OCSP service location, is part of both the user and CA certificates. The AIA OCSP URL will be used to check the certificate revocation status with OCSP. +Unless a designated OCSP responder service is in use, it is required that the AIA extension that contains the certificate’s OCSP responder access location is present in the user certificate. The AIA OCSP URL will be used to check the certificate revocation status with OCSP. -**Note that there may be legal limitations to using AIA URLs during signing** as the services behind these URLs provide different security and SLA guarantees than dedicated OCSP services. For digital signing, OCSP responder certificate validation is additionally needed. Using AIA URLs during authentication is sufficient, however. +**Note that there may be legal limitations to using AIA URLs during signing** as the services behind these URLs provide different security and SLA guarantees than dedicated OCSP responder services. Using AIA URLs during authentication is sufficient, however. ## Possible validation errors @@ -350,6 +344,7 @@ String nonce = nonceGenerator.generateAndStoreNonce(); The `generateAndStoreNonce()` method both generates the nonce and stores it in the cache. ## Extended configuration + The following additional configuration options are available in `NonceGeneratorBuilder`: - `withNonceTtl(Duration duration)` – overrides the default nonce time-to-live duration. When the time-to-live passes, the nonce is considered to be expired. Default nonce time-to-live is 5 minutes. @@ -363,14 +358,3 @@ NonceGenerator generator = new NonceGeneratorBuilder() .withSecureRandom(customSecureRandom) .build(); ``` - -## Frequently asked questions - -### How can I find the AIA OCSP service URLs? - -You can find the AIA OCSP service URLs from the electronic ID certificate profile documents, in the section that describes certificate extensions. -The AIA OCSP extension OID is 1.3.6.1.5.5.7.48.1. - -For example, the EstEID AIA URLs are specified in the documents -[*Certificate, CRL and OCSP Profile for identification documents of the Republic of Estonia*](https://www.skidsolutions.eu/upload/files/SK-CPR-ESTEID-EN-v8_4-20200630.pdf) and -[*Certificate, CRL and OCSP Profile for ID-1 Format Identity Documents Issued by the Republic of Estonia*](https://www.skidsolutions.eu/upload/files/SK-CPR-ESTEID2018-EN-v1_2_20200630.pdf). diff --git a/pom.xml b/pom.xml index bc746e9b..d57f98bb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 authtoken-validation org.webeid.security - 2.0.0 + 1.2.0 jar authtoken-validation Web eID authentication token validation library for Java @@ -16,10 +16,11 @@ 1.8 0.11.2 1.7.30 - 1.65 + 1.69 2.8.5 5.6.2 3.17.2 + 3.12.4 0.8.5 ${project.basedir}/../jacoco-coverage-report/target/site/jacoco-aggregate/jacoco.xml @@ -96,6 +97,12 @@ ${assertj.version} test + + org.mockito + mockito-core + ${mockito.version} + test + org.slf4j slf4j-simple diff --git a/src/main/java/org/webeid/security/certificate/CertificateData.java b/src/main/java/org/webeid/security/certificate/CertificateData.java index 87f01256..13a11802 100644 --- a/src/main/java/org/webeid/security/certificate/CertificateData.java +++ b/src/main/java/org/webeid/security/certificate/CertificateData.java @@ -37,29 +37,35 @@ public final class CertificateData { public static String getSubjectCN(X509Certificate certificate) throws CertificateEncodingException { - return getField(certificate, BCStyle.CN); + return getSubjectField(certificate, BCStyle.CN); } public static String getSubjectSurname(X509Certificate certificate) throws CertificateEncodingException { - return getField(certificate, BCStyle.SURNAME); + return getSubjectField(certificate, BCStyle.SURNAME); } public static String getSubjectGivenName(X509Certificate certificate) throws CertificateEncodingException { - return getField(certificate, BCStyle.GIVENNAME); + return getSubjectField(certificate, BCStyle.GIVENNAME); } public static String getSubjectIdCode(X509Certificate certificate) throws CertificateEncodingException { - return getField(certificate, BCStyle.SERIALNUMBER); + return getSubjectField(certificate, BCStyle.SERIALNUMBER); } public static String getSubjectCountryCode(X509Certificate certificate) throws CertificateEncodingException { - return getField(certificate, BCStyle.C); + return getSubjectField(certificate, BCStyle.C); } - private static String getField(X509Certificate certificate, ASN1ObjectIdentifier fieldId) throws CertificateEncodingException { - final X500Name x500Name = new JcaX509CertificateHolder(certificate).getSubject(); + private static String getSubjectField(X509Certificate certificate, ASN1ObjectIdentifier fieldId) throws CertificateEncodingException { + return getField(new JcaX509CertificateHolder(certificate).getSubject(), fieldId); + } + + private static String getField(X500Name x500Name, ASN1ObjectIdentifier fieldId) throws CertificateEncodingException { // Example value: [C=EE, CN=JÕEORG\,JAAK-KRISTJAN\,38001085718, 2.5.4.4=#0c074ac395454f5247, 2.5.4.42=#0c0d4a41414b2d4b524953544a414e, 2.5.4.5=#1311504e4f45452d3338303031303835373138] final RDN[] rdns = x500Name.getRDNs(fieldId); + if (rdns.length == 0 || rdns[0].getFirst() == null) { + throw new CertificateEncodingException("X500 name RDNs empty or first element is null"); + } return Arrays.stream(rdns) .map(rdn -> IETFUtils.valueToString(rdn.getFirst().getValue())) .collect(Collectors.joining(", ")); diff --git a/src/main/java/org/webeid/security/certificate/CertificateValidator.java b/src/main/java/org/webeid/security/certificate/CertificateValidator.java index e26c99d5..773b5bd9 100644 --- a/src/main/java/org/webeid/security/certificate/CertificateValidator.java +++ b/src/main/java/org/webeid/security/certificate/CertificateValidator.java @@ -1,9 +1,31 @@ +/* + * 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.certificate; import org.webeid.security.exceptions.CertificateNotTrustedException; import org.webeid.security.exceptions.JceException; -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 java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; @@ -11,8 +33,6 @@ import java.security.cert.CertPathBuilder; import java.security.cert.CertPathBuilderException; import java.security.cert.CertStore; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.PKIXBuilderParameters; import java.security.cert.PKIXCertPathBuilderResult; @@ -20,23 +40,25 @@ import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.Set; import java.util.stream.Collectors; public final class CertificateValidator { - /** - * Checks whether the certificate was valid on the given date. - */ - public static void certificateIsValidOnDate(X509Certificate cert, Date date) throws UserCertificateNotYetValidException, UserCertificateExpiredException { + public static void certificateIsValidOnDate(X509Certificate cert, Date date, String subject) throws CertificateNotYetValidException, CertificateExpiredException { try { cert.checkValidity(date); - } catch (CertificateNotYetValidException e) { - throw new UserCertificateNotYetValidException(e); - } catch (CertificateExpiredException e) { - throw new UserCertificateExpiredException(e); + } catch (java.security.cert.CertificateNotYetValidException e) { + throw new CertificateNotYetValidException(subject, e); + } catch (java.security.cert.CertificateExpiredException e) { + throw new CertificateExpiredException(subject, e); + } + } + + public static void trustedCACertificatesAreValidOnDate(Set trustedCACertificateAnchors, Date date) throws CertificateNotYetValidException, CertificateExpiredException { + for (TrustAnchor cert : trustedCACertificateAnchors) { + certificateIsValidOnDate(cert.getTrustedCert(), date, "Trusted CA"); } } @@ -70,10 +92,6 @@ public static Set buildTrustAnchorsFromCertificates(Collection buildTrustAnchorsFromCertificate(X509Certificate certificate) { - return buildTrustAnchorsFromCertificates(Collections.singleton(certificate)); - } - public static CertStore buildCertStoreFromCertificates(Collection certificates) throws JceException { // 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 @@ -85,10 +103,6 @@ public static CertStore buildCertStoreFromCertificates(Collection nonceCache; @@ -54,7 +51,6 @@ final class AuthTokenValidationConfiguration { private boolean isUserCertificateRevocationCheckWithOcspEnabled = true; private Duration ocspRequestTimeout = Duration.ofSeconds(5); private Duration allowedClientClockSkew = Duration.ofMinutes(3); - private AiaOcspServiceConfiguration aiaOcspServiceConfiguration; private DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration; private boolean isSiteCertificateFingerprintValidationEnabled = false; private String siteCertificateSha256Fingerprint; @@ -65,6 +61,8 @@ final class AuthTokenValidationConfiguration { ESTEID_SK_2015_MOBILE_ID_POLICY_V3, ESTEID_SK_2015_MOBILE_ID_POLICY ); + // Disable OCSP nonce extension for EstEID 2015 cards by default. + private Collection nonceDisabledOcspUrls = Sets.newHashSet(AIA_ESTEID_2015); AuthTokenValidationConfiguration() { } @@ -76,11 +74,11 @@ private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) this.isUserCertificateRevocationCheckWithOcspEnabled = other.isUserCertificateRevocationCheckWithOcspEnabled; this.ocspRequestTimeout = other.ocspRequestTimeout; this.allowedClientClockSkew = other.allowedClientClockSkew; - this.aiaOcspServiceConfiguration = other.aiaOcspServiceConfiguration; this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration; this.isSiteCertificateFingerprintValidationEnabled = other.isSiteCertificateFingerprintValidationEnabled; this.siteCertificateSha256Fingerprint = other.siteCertificateSha256Fingerprint; this.disallowedSubjectCertificatePolicies = new HashSet<>(other.disallowedSubjectCertificatePolicies); + this.nonceDisabledOcspUrls = new HashSet<>(other.nonceDisabledOcspUrls); } void setSiteOrigin(URI siteOrigin) { @@ -127,14 +125,6 @@ Duration getAllowedClientClockSkew() { return allowedClientClockSkew; } - public AiaOcspServiceConfiguration getAiaOcspServiceConfiguration() { - return aiaOcspServiceConfiguration; - } - - public void setAiaOcspServiceConfiguration(AiaOcspServiceConfiguration aiaOcspServiceConfiguration) { - this.aiaOcspServiceConfiguration = aiaOcspServiceConfiguration; - } - public DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() { return designatedOcspServiceConfiguration; } @@ -160,6 +150,10 @@ public Collection getDisallowedSubjectCertificatePolicies( return disallowedSubjectCertificatePolicies; } + public Collection getNonceDisabledOcspUrls() { + return nonceDisabledOcspUrls; + } + /** * Checks that the configuration parameters are valid. * @@ -173,18 +167,6 @@ void validate() { if (trustedCACertificates.isEmpty()) { throw new IllegalArgumentException("At least one trusted certificate authority must be provided"); } - if (isUserCertificateRevocationCheckWithOcspEnabled) { - if (aiaOcspServiceConfiguration == null && designatedOcspServiceConfiguration == null) { - throw new IllegalArgumentException("Either AIA or designated OCSP service configuration must be provided"); - } - if (aiaOcspServiceConfiguration != null && designatedOcspServiceConfiguration != null) { - throw new IllegalArgumentException("AIA and designated OCSP service configuration cannot provided together, " + - "please provide either one or the other"); - } - } else if (aiaOcspServiceConfiguration != null || designatedOcspServiceConfiguration != null) { - throw new IllegalArgumentException("When user certificate OCSP check is disabled, " + - "AIA or designated OCSP service configuration should not be provided"); - } requirePositiveDuration(ocspRequestTimeout, "OCSP request timeout"); requirePositiveDuration(allowedClientClockSkew, "Allowed client clock skew"); if (isSiteCertificateFingerprintValidationEnabled) { diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java b/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java index 8e3561ea..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,7 +26,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.webeid.security.exceptions.JceException; -import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; import javax.cache.Cache; @@ -35,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. @@ -75,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. * @@ -88,7 +89,9 @@ public AuthTokenValidatorBuilder withTrustedCertificateAuthorities(X509Certifica Collections.addAll(configuration.getTrustedCACertificates(), certificates); if (LOG.isDebugEnabled()) { LOG.debug("Trusted intermediate certificate authorities set to {}", - configuration.getTrustedCACertificates().stream().map(X509Certificate::getSubjectDN)); + configuration.getTrustedCACertificates().stream() + .map(X509Certificate::getSubjectDN) + .collect(Collectors.toList())); } return this; } @@ -134,12 +137,28 @@ public AuthTokenValidatorBuilder withOcspRequestTimeout(Duration ocspRequestTime return this; } - public AuthTokenValidatorBuilder withAiaOcspServiceConfiguration(AiaOcspServiceConfiguration serviceConfiguration) { - configuration.setAiaOcspServiceConfiguration(serviceConfiguration); - LOG.debug("Using AIA OCSP service configuration"); + /** + * Adds the given URLs to the list of OCSP URLs for which the nonce protocol extension will be disabled. + * The OCSP URL is extracted from the user certificate and some OCSP services don't support the nonce extension. + * + * @param urls OCSP URLs for which the nonce protocol extension will be disabled + * @return the builder instance for method chaining + */ + public AuthTokenValidatorBuilder withNonceDisabledOcspUrls(URI... urls) { + Collections.addAll(configuration.getNonceDisabledOcspUrls(), urls); + LOG.debug("OCSP URLs for which the nonce protocol extension is disabled set to {}", configuration.getNonceDisabledOcspUrls()); 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"); diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java b/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java index 363e1b18..917e1d2c 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java @@ -22,7 +22,6 @@ package org.webeid.security.validator; -import com.google.common.base.Suppliers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.webeid.security.exceptions.JceException; @@ -31,13 +30,13 @@ 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.cert.CertStore; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.Set; -import java.util.function.Supplier; import static org.webeid.security.certificate.CertificateValidator.buildCertStoreFromCertificates; import static org.webeid.security.certificate.CertificateValidator.buildTrustAnchorsFromCertificates; @@ -52,14 +51,15 @@ 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 ocspClientSupplier; 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; /** @@ -68,15 +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.ocspClientSupplier = Suppliers.memoize(() -> OcspClientImpl.build(configuration.getOcspRequestTimeout())); + + // 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( @@ -86,14 +85,13 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { new SiteCertificateFingerprintValidator(configuration.getSiteCertificateSha256Fingerprint())::validateSiteCertificateFingerprint ); - // Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator. - trustedCACertificateAnchors = buildTrustAnchorsFromCertificates(configuration.getTrustedCACertificates()); - trustedCACertificateCertStore = buildCertStoreFromCertificates(configuration.getTrustedCACertificates()); - if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) { - ocspServiceProvider = configuration.getAiaOcspServiceConfiguration() != null ? - new OcspServiceProvider(configuration.getAiaOcspServiceConfiguration()) : - new OcspServiceProvider(configuration.getDesignatedOcspServiceConfiguration()); + ocspClient = OcspClientImpl.build(configuration.getOcspRequestTimeout()); + ocspServiceProvider = new OcspServiceProvider( + configuration.getDesignatedOcspServiceConfiguration(), + new AiaOcspServiceConfiguration(configuration.getNonceDisabledOcspUrls(), + trustedCACertificateAnchors, + trustedCACertificateCertStore)); } } @@ -145,7 +143,7 @@ private ValidatorBatch getCertTrustValidators() { return ValidatorBatch.createFrom( certTrustedValidator::validateCertificateTrusted ).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(), - new SubjectCertificateNotRevokedValidator(certTrustedValidator, ocspClientSupplier.get(), ocspServiceProvider)::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 index c78853c9..7b5ad6d6 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java @@ -1,3 +1,25 @@ +/* + * 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; diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java b/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java index b1f6d826..cb63cba4 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java @@ -1,3 +1,25 @@ +/* + * 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; @@ -34,7 +56,7 @@ public static OcspClient build(Duration ocspRequestTimeout) { } /** - * Use OkHttpClient to fetch the OCSP response from the CA's OCSP responder server. + * Use OkHttpClient to fetch the OCSP response from the OCSP responder service. * * @param uri OCSP server URL * @param ocspReq OCSP request 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 f6958b0f..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 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 index 93bf9c98..b85831a6 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java @@ -1,3 +1,25 @@ +/* + * 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; @@ -7,27 +29,36 @@ 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 AiaOcspServiceConfiguration aiaOcspServiceConfiguration; private final DesignatedOcspService designatedOcspService; + private final AiaOcspServiceConfiguration aiaOcspServiceConfiguration; - public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration) { - this.designatedOcspService = new DesignatedOcspService(designatedOcspServiceConfiguration); - this.aiaOcspServiceConfiguration = null; - } - - public OcspServiceProvider(AiaOcspServiceConfiguration aiaOcspServiceConfiguration) { - this.aiaOcspServiceConfiguration = aiaOcspServiceConfiguration; - this.designatedOcspService = null; + public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration, AiaOcspServiceConfiguration aiaOcspServiceConfiguration) { + designatedOcspService = designatedOcspServiceConfiguration != null ? + new DesignatedOcspService(designatedOcspServiceConfiguration) + : null; + this.aiaOcspServiceConfiguration = Objects.requireNonNull(aiaOcspServiceConfiguration, "aiaOcspServiceConfiguration"); } - public OcspService getService(X509Certificate certificate) throws TokenValidationException { - return designatedOcspService != null ? - designatedOcspService : - new AiaOcspService(aiaOcspServiceConfiguration, certificate); + /** + * 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/OcspUtils.java b/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java similarity index 58% rename from src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java rename to src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java index cacdf30c..0bc67cd7 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.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) 2017 The Netty 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 @@ -47,22 +25,17 @@ import org.bouncycastle.asn1.DLTaggedObject; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; -import org.webeid.security.exceptions.OCSPCertificateException; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; +import java.util.Objects; -public final class OcspUtils { +public final class OcspUrl { + + public static final URI AIA_ESTEID_2015 = URI.create("http://aia.sk.ee/esteid2015"); - /** - * 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"; /** * The OID for OCSP responder URLs. *

@@ -71,21 +44,11 @@ public final class OcspUtils { private static final ASN1ObjectIdentifier OCSP_RESPONDER_OID = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1").intern(); - public static void validateHasSigningExtension(X509Certificate cert) throws OCSPCertificateException { - try { - if (cert.getExtendedKeyUsage() == null || !cert.getExtendedKeyUsage().contains(OID_OCSP_SIGNING)) { - throw new OCSPCertificateException("Certificate " + cert.getSubjectDN() + - " does not contain the key usage extension for OCSP response signing"); - } - } catch (CertificateParsingException e) { - throw new OCSPCertificateException("Certificate parsing failed:", e); - } - } - /** * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one. */ - public static URI ocspUri(X509Certificate certificate) { + public static URI getOcspUri(X509Certificate certificate) { + Objects.requireNonNull(certificate, "certificate"); final byte[] value = certificate.getExtensionValue(Extension.authorityInfoAccess.getId()); if (value == null) { return null; @@ -94,7 +57,7 @@ public static URI ocspUri(X509Certificate certificate) { final ASN1Primitive authorityInfoAccess; try { authorityInfoAccess = JcaX509ExtensionUtils.parseExtensionValue(value); - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { return null; } if (!(authorityInfoAccess instanceof DLSequence)) { @@ -117,7 +80,7 @@ public static URI ocspUri(X509Certificate certificate) { } catch (IOException e) { return null; } - int length = (int) encoded[1] & 0xFF; + int length = encoded[1] & 0xFF; final String uri = new String(encoded, 2, length, StandardCharsets.UTF_8); return URI.create(uri); } @@ -144,7 +107,7 @@ private static T findObject(DLSequence sequence, ASN1ObjectIdentifier oid, C return null; } - private OcspUtils() { + private OcspUrl() { throw new IllegalStateException("Utility class"); } } diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspResponderConfiguration.java b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspResponderConfiguration.java deleted file mode 100644 index a6e4db5e..00000000 --- a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspResponderConfiguration.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.webeid.security.validator.ocsp.service; - -import org.webeid.security.exceptions.JceException; - -import java.net.URI; -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.*; - -public class AiaOcspResponderConfiguration { - - private final URI responderUrl; - private final boolean doesSupportNonce; - private final Set trustedCACertificateAnchors; - private final CertStore trustedCACertificateCertStore; - - public AiaOcspResponderConfiguration(URI responderUrl, X509Certificate responderTrustedCACertificate, boolean doesSupportNonce) throws JceException { - this.responderUrl = responderUrl; - this.trustedCACertificateAnchors = buildTrustAnchorsFromCertificate(responderTrustedCACertificate); - this.trustedCACertificateCertStore = buildCertStoreFromCertificate(responderTrustedCACertificate); - this.doesSupportNonce = doesSupportNonce; - } - - public AiaOcspResponderConfiguration(URI responderUrl, X509Certificate responderTrustedCACertificate) throws JceException { - this(responderUrl, responderTrustedCACertificate, true); - } - - public boolean doesSupportNonce() { - return doesSupportNonce; - } - - public URI getResponderUrl() { - return responderUrl; - } - - public Set getTrustedCACertificateAnchors() { - return trustedCACertificateAnchors; - } - - public CertStore getTrustedCACertificateCertStore() { - return trustedCACertificateCertStore; - } -} 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 index 4cc1e539..4b8bef36 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java +++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java @@ -1,3 +1,25 @@ +/* + * 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; @@ -5,17 +27,20 @@ import org.webeid.security.exceptions.OCSPCertificateException; import org.webeid.security.exceptions.TokenValidationException; import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; -import org.webeid.security.validator.ocsp.OcspUtils; 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.OcspUtils.validateHasSigningExtension; +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. @@ -23,23 +48,26 @@ 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 AiaOcspResponderConfiguration configuration; + private final boolean supportsNonce; - public AiaOcspService(AiaOcspServiceConfiguration aiaOcspServiceConfiguration, X509Certificate certificate) throws TokenValidationException { - this.url = getOcspAiaUrlFromCertificate(certificate); - Objects.requireNonNull(aiaOcspServiceConfiguration, "aiaOcspServiceConfiguration"); - this.configuration = aiaOcspServiceConfiguration.getResponderConfigurationForUrl(this.url); + 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 configuration.doesSupportNonce(); + return supportsNonce; } @Override - public URI getUrl() { + public URI getAccessLocation() { return url; } @@ -47,20 +75,17 @@ public URI getUrl() { public void validateResponderCertificate(X509CertificateHolder cert, Date producedAt) throws TokenValidationException { try { final X509Certificate certificate = certificateConverter.getCertificate(cert); - certificateIsValidOnDate(certificate, producedAt); + certificateIsValidOnDate(certificate, producedAt, "AIA OCSP responder"); + // Trusted certificates' validity has been already verified in validateCertificateExpiry(). validateHasSigningExtension(certificate); - validateIsSignedByTrustedCA( - certificate, - configuration.getTrustedCACertificateAnchors(), - configuration.getTrustedCACertificateCertStore() - ); + validateIsSignedByTrustedCA(certificate, trustedCACertificateAnchors, trustedCACertificateCertStore); } catch (CertificateException e) { - throw new OCSPCertificateException("Invalid certificate", e); + throw new OCSPCertificateException("Invalid responder certificate", e); } } private static URI getOcspAiaUrlFromCertificate(X509Certificate certificate) throws TokenValidationException { - final URI uri = OcspUtils.ocspUri(Objects.requireNonNull(certificate, "certificate")); + final URI uri = getOcspUri(certificate); if (uri == null) { throw new UserCertificateOCSPCheckFailedException("Getting the AIA OCSP responder field from the certificate failed"); } 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 index 2357998d..128b405e 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java +++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java @@ -1,26 +1,56 @@ -package org.webeid.security.validator.ocsp.service; +/* + * 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. + */ -import org.webeid.security.exceptions.AiaOcspResponderConfigurationException; +package org.webeid.security.validator.ocsp.service; import java.net.URI; -import java.util.HashMap; +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 HashMap configs = new HashMap<>(); + 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 AiaOcspServiceConfiguration(AiaOcspResponderConfiguration... aiaOcspResponderConfiguration) { - for (AiaOcspResponderConfiguration conf : aiaOcspResponderConfiguration) { - configs.put(conf.getResponderUrl(), conf); - } + public Set getTrustedCACertificateAnchors() { + return trustedCACertificateAnchors; } - public AiaOcspResponderConfiguration getResponderConfigurationForUrl(URI url) throws AiaOcspResponderConfigurationException { - final AiaOcspResponderConfiguration configuration = configs.get(url); - if (configuration == null) { - throw new AiaOcspResponderConfigurationException("No AIA OCSP responder configuration exists for URL " + url); - } - return configuration; + 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 index badd2c38..d7220ffb 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java +++ b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java @@ -1,3 +1,25 @@ +/* + * 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; @@ -6,6 +28,7 @@ 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; @@ -18,21 +41,21 @@ */ public class DesignatedOcspService implements OcspService { - private final DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration; private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + private final DesignatedOcspServiceConfiguration configuration; - public DesignatedOcspService(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration) { - this.designatedOcspServiceConfiguration = Objects.requireNonNull(designatedOcspServiceConfiguration, "designatedOcspServiceConfiguration"); + public DesignatedOcspService(DesignatedOcspServiceConfiguration configuration) { + this.configuration = Objects.requireNonNull(configuration, "configuration"); } @Override public boolean doesSupportNonce() { - return designatedOcspServiceConfiguration.doesSupportNonce(); + return configuration.doesSupportNonce(); } @Override - public URI getUrl() { - return designatedOcspServiceConfiguration.getOcspServiceUrl(); + public URI getAccessLocation() { + return configuration.getOcspServiceAccessLocation(); } @Override @@ -41,13 +64,18 @@ public void validateResponderCertificate(X509CertificateHolder cert, Date produc 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 (!designatedOcspServiceConfiguration.getResponderCertificate().equals(responderCertificate)) { + 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); + 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 index fba97cb2..029905b6 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java +++ b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java @@ -1,32 +1,66 @@ +/* + * 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.OcspUtils.validateHasSigningExtension; +import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateHasSigningExtension; public class DesignatedOcspServiceConfiguration { - private final URI ocspServiceUrl; + private final URI ocspServiceAccessLocation; private final X509Certificate responderCertificate; private final boolean doesSupportNonce; + private final Collection supportedIssuers; - public DesignatedOcspServiceConfiguration(URI ocspServiceUrl, X509Certificate responderCertificate, boolean doesSupportNonce) throws OCSPCertificateException { - this.ocspServiceUrl = Objects.requireNonNull(ocspServiceUrl, "OCSP service URL"); + /** + * 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 DesignatedOcspServiceConfiguration(URI ocspServiceUrl, X509Certificate responderCertificate) throws OCSPCertificateException { - this(ocspServiceUrl, responderCertificate, true); - } - - public URI getOcspServiceUrl() { - return ocspServiceUrl; + public URI getOcspServiceAccessLocation() { + return ocspServiceAccessLocation; } public X509Certificate getResponderCertificate() { @@ -36,4 +70,26 @@ public X509Certificate getResponderCertificate() { 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 index 5bb032c3..d098b440 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java +++ b/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java @@ -1,3 +1,25 @@ +/* + * 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; @@ -10,7 +32,7 @@ public interface OcspService { boolean doesSupportNonce(); - URI getUrl(); + 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 6faca5c3..aeaf21bd 100644 --- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java +++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java @@ -28,23 +28,17 @@ import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.ocsp.BasicOCSPResp; import org.bouncycastle.cert.ocsp.CertificateID; -import org.bouncycastle.cert.ocsp.CertificateStatus; import org.bouncycastle.cert.ocsp.OCSPException; import org.bouncycastle.cert.ocsp.OCSPReq; import org.bouncycastle.cert.ocsp.OCSPResp; -import org.bouncycastle.cert.ocsp.RevokedStatus; import org.bouncycastle.cert.ocsp.SingleResp; -import org.bouncycastle.cert.ocsp.UnknownStatus; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentVerifierProvider; import org.bouncycastle.operator.DigestCalculator; import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.webeid.security.exceptions.TokenValidationException; import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; -import org.webeid.security.exceptions.UserCertificateRevokedException; import org.webeid.security.validator.AuthTokenValidatorData; import org.webeid.security.validator.ocsp.Digester; import org.webeid.security.validator.ocsp.OcspClient; @@ -58,8 +52,13 @@ 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.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); @@ -105,7 +104,7 @@ public void validateCertificateNotRevoked(AuthTokenValidatorData actualTokenData .build(); LOG.debug("Sending OCSP request"); - final OCSPResp response = Objects.requireNonNull(ocspClient.request(ocspService.getUrl(), request)); + final OCSPResp response = Objects.requireNonNull(ocspClient.request(ocspService.getAccessLocation(), request)); if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) { throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus())); } @@ -121,55 +120,61 @@ public void validateCertificateNotRevoked(AuthTokenValidatorData actualTokenData } 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"); } - // We require the responder to include a single certificate in the certs field of the response - // that helps us to verify the responder's signature. - if (basicResponse.getCerts().length != 1) { - throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one responder certificate, " - + "received " + basicResponse.getCerts().length + " certificates 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"); } - final X509CertificateHolder responderCert = basicResponse.getCerts()[0]; + // 2. The signature on the response is valid. - ocspService.validateResponderCertificate(responderCert, basicResponse.getProducedAt()); + // 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); - validateSubjectCertificateStatus(certStatusResponse); - } - private 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"); - } - } + // 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. - private void validateSubjectCertificateStatus(SingleResp certStatusResponse) throws UserCertificateRevokedException { - final CertificateStatus status = certStatusResponse.getCertStatus(); - if (status == null) { - LOG.debug("OCSP check result is GOOD"); - } else 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"); - } + 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 void checkNonce(OCSPReq request, BasicOCSPResp response) throws UserCertificateOCSPCheckFailedException { + 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)) { @@ -178,13 +183,13 @@ private void checkNonce(OCSPReq request, BasicOCSPResp response) throws UserCert } } - private CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws CertificateEncodingException, IOException, OCSPException { + 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 String ocspStatusToString(int status) { + private static String ocspStatusToString(int status) { switch (status) { case OCSPResp.MALFORMED_REQUEST: return "malformed request"; 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 76% 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 959009c6..bbf3ea0c 100644 --- a/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java +++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePurposeValidator.java @@ -35,25 +35,11 @@ import java.util.Date; import java.util.List; -import static org.webeid.security.certificate.CertificateValidator.certificateIsValidOnDate; +public final class SubjectCertificatePurposeValidator { -public final class FunctionalSubjectCertificateValidators { - - 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 { - // Use JJWT Clock interface so that the date can be mocked in tests. - certificateIsValidOnDate(actualTokenData.getSubjectCertificate(), DefaultClock.INSTANCE.now()); - LOG.debug("User certificate is valid."); - } - /** * Validates that the purpose of the user certificate from the authentication token contains client authentication. * @@ -75,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 1ff7b557..d2993258 100644 --- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java +++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java @@ -22,8 +22,10 @@ package org.webeid.security.validator.validators; -import org.webeid.security.exceptions.TokenValidationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.webeid.security.exceptions.CertificateNotTrustedException; +import org.webeid.security.exceptions.TokenValidationException; import org.webeid.security.validator.AuthTokenValidatorData; import java.security.cert.CertStore; @@ -35,6 +37,7 @@ public final class SubjectCertificateTrustedValidator { + private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateTrustedValidator.class); private final Set trustedCACertificateAnchors; private final CertStore trustedCACertificateCertStore; @@ -54,6 +57,7 @@ public SubjectCertificateTrustedValidator(Set trustedCACertificateA public void validateCertificateTrusted(AuthTokenValidatorData actualTokenData) throws TokenValidationException { final X509Certificate certificate = actualTokenData.getSubjectCertificate(); 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/AuthTokenValidators.java b/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java index b2aeeccb..93046e8c 100644 --- a/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java +++ b/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java @@ -25,9 +25,10 @@ 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.AiaOcspResponderConfiguration; import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; import javax.cache.Cache; @@ -40,6 +41,7 @@ import java.time.ZonedDateTime; import static org.webeid.security.testutil.Certificates.getTestEsteid2018CA; +import static org.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceConfiguration; public final class AuthTokenValidators { @@ -59,6 +61,7 @@ public static AuthTokenValidator getAuthTokenValidator(String url, Cache cache) throws CertificateException, JceException, URISyntaxException, IOException { return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates()) - .withAiaOcspServiceConfiguration(new AiaOcspServiceConfiguration( - new AiaOcspResponderConfiguration(new URI("http://aia.demo.sk.ee/esteid2018"), getTestEsteid2018CA()))) + .build(); + } + + public static AuthTokenValidator getAuthTokenValidatorWithDesignatedOcspCheck(Cache cache) throws CertificateException, JceException, URISyntaxException, IOException, OCSPCertificateException { + return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates()) + .withDesignatedOcspServiceConfiguration(getDesignatedOcspServiceConfiguration()) .build(); } 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 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 7827e598..86ef7803 100644 --- a/src/test/java/org/webeid/security/validator/OcspTest.java +++ b/src/test/java/org/webeid/security/validator/OcspTest.java @@ -24,7 +24,10 @@ 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; @@ -34,6 +37,7 @@ 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 { @@ -53,29 +57,35 @@ protected void setup() { @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, - UserCertificateOCSPCheckFailedException.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) + .isInstanceOf(CertificateExpiredException.class) .hasMessageStartingWith("User certificate has expired"); } @Test void testTokenCertEcdsaExpired() { assertThatThrownBy(() -> validator.validate(Tokens.TOKEN_CERT_ECDSA_EXIPRED)) - .isInstanceOf(UserCertificateExpiredException.class) + .isInstanceOf(CertificateExpiredException.class) .hasMessageStartingWith("User certificate has expired"); } + } 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 index f4fb1cea..bf76569a 100644 --- a/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java +++ b/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java @@ -1,45 +1,46 @@ package org.webeid.security.validator.ocsp; import org.bouncycastle.cert.X509CertificateHolder; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; -import org.webeid.security.exceptions.JceException; import org.webeid.security.exceptions.OCSPCertificateException; -import org.webeid.security.validator.ocsp.service.AiaOcspResponderConfiguration; -import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; -import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; import org.webeid.security.validator.ocsp.service.OcspService; -import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; -import java.security.cert.CertificateException; 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.*; +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 = - new OcspServiceProvider(new DesignatedOcspServiceConfiguration(new URI("http://demo.sk.ee/ocsp"), getTestSkOcspResponder2020())); - final OcspService service = ocspServiceProvider.getService(null); - assertThat(service.getUrl()).isEqualTo(new URI("http://demo.sk.ee/ocsp")); + 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.getUrl()).isEqualTo(new URI("http://aia.demo.sk.ee/esteid2018")); + 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. @@ -47,7 +48,7 @@ void whenAiaOcspServiceConfigurationProvided_thenCreatesAiaOcspService() throws .doesNotThrowAnyException(); final OcspService service2015 = ocspServiceProvider.getService(getMariliisEsteid2015Cert()); - assertThat(service2015.getUrl()).isEqualTo(new URI("http://aia.demo.sk.ee/esteid2015")); + 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. @@ -65,11 +66,5 @@ void whenAiaOcspServiceConfigurationDoesNotHaveResponderCertTrustedCA_thenThrows service2018.validateResponderCertificate(wrongResponderCert, new Date(1630000000000L))); } - @NotNull - private OcspServiceProvider getAiaOcspServiceProvider() throws URISyntaxException, CertificateException, JceException, IOException { - return new OcspServiceProvider(new AiaOcspServiceConfiguration( - new AiaOcspResponderConfiguration(new URI("http://aia.demo.sk.ee/esteid2018"), getTestEsteid2018CA()), - new AiaOcspResponderConfiguration(new URI("http://aia.demo.sk.ee/esteid2015"), getTestEsteid2015CA(), false) - )); - } + } 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 index 7b670df2..33298db3 100644 --- a/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java +++ b/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java @@ -1,35 +1,63 @@ +/* + * 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.*; +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.*; +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 org.webeid.security.validator.ocsp.service.AiaOcspResponderConfiguration; -import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; -import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; -import java.net.URI; import java.net.URISyntaxException; import java.security.cert.CertificateException; -import java.security.cert.CertificateParsingException; 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.*; +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 { @@ -56,8 +84,7 @@ void whenValidAiaOcspResponderConfiguration_thenSucceeds() throws Exception { @Test void whenValidDesignatedOcspResponderConfiguration_thenSucceeds() throws Exception { - final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider( - new DesignatedOcspServiceConfiguration(new URI("http://demo.sk.ee/ocsp"), getTestSkOcspResponder2020())); + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider(); final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); assertThatCode(() -> validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) @@ -66,8 +93,7 @@ void whenValidDesignatedOcspResponderConfiguration_thenSucceeds() throws Excepti @Test void whenValidOcspNonceDisabledConfiguration_thenSucceeds() throws Exception { - final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider( - new DesignatedOcspServiceConfiguration(new URI("http://demo.sk.ee/ocsp"), getTestSkOcspResponder2020(), false)); + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider(false); final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); assertThatCode(() -> validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) @@ -76,21 +102,20 @@ void whenValidOcspNonceDisabledConfiguration_thenSucceeds() throws Exception { @Test void whenOcspUrlIsInvalid_thenThrows() throws Exception { - final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider( - new DesignatedOcspServiceConfiguration(new URI("http://invalid.invalid-tld"), getTestSkOcspResponder2020())); + 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) - .hasMessage("invalid.invalid-tld: Name or service not known"); + .hasMessageMatching("invalid.invalid: (Name or service not known|" + + "Temporary failure in name resolution)"); } @Test void whenOcspRequestFails_thenThrows() throws Exception { - final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider( - new DesignatedOcspServiceConfiguration(new URI("https://web-eid-test.free.beeceptor.com"), getTestSkOcspResponder2020())); + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("https://web-eid-test.free.beeceptor.com"); final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); assertThatCode(() -> validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) @@ -158,10 +183,10 @@ void whenOcspResponseHasInvalidResponderCert_thenThrows() throws Exception { .build()); assertThatCode(() -> validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) - .isInstanceOf(OCSPCertificateException.class) + .isInstanceOf(UserCertificateOCSPCheckFailedException.class) .getCause() - .isInstanceOf(CertificateParsingException.class) - .hasMessage("java.io.IOException: subject key, java.security.InvalidKeyException: Invalid RSA public key"); + .isInstanceOf(OCSPException.class) + .hasMessage("exception processing sig: java.lang.IllegalArgumentException: invalid info structure in RSA public key"); } @Test @@ -190,7 +215,7 @@ void whenOcspResponseHas2CertResponses_thenThrows() throws Exception { .withMessage("User certificate revocation check has failed: OCSP response must contain one response, received 2 responses instead"); } - @Test + @Disabled("It is difficult to make Python and Java CertId equal, needs more work") void whenOcspResponseHas2ResponderCerts_thenThrows() throws Exception { final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( getResponseBuilder() @@ -216,8 +241,7 @@ void whenOcspResponseRevoked_thenThrows() throws Exception { @Test void whenOcspResponseUnknown_thenThrows() throws Exception { - final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider( - new DesignatedOcspServiceConfiguration(new URI("http://demo.sk.ee/ocsp"), getTestSkOcspResponder2020())); + 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(); @@ -241,6 +265,18 @@ void whenOcspResponseCANotTrusted_thenThrows() throws Exception { .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; @@ -277,7 +313,8 @@ private byte[] buildOcspResponseBodyWithInvalidTag() throws IOException { } // 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. + // 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"); } @@ -296,9 +333,7 @@ private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedVal @NotNull private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidatorWithAiaOcsp(OcspClient client) throws JceException, URISyntaxException, CertificateException, IOException { - final OcspServiceProvider ocspServiceProvider = new OcspServiceProvider(new AiaOcspServiceConfiguration( - new AiaOcspResponderConfiguration(new URI("http://aia.demo.sk.ee/esteid2018"), getTestEsteid2018CA()))); - return new SubjectCertificateNotRevokedValidator(trustedValidator, client, ocspServiceProvider); + return new SubjectCertificateNotRevokedValidator(trustedValidator, client, getAiaOcspServiceProvider()); } private static void setSubjectCertificateIssuerCertificate(SubjectCertificateTrustedValidator trustedValidator) throws NoSuchFieldException, IllegalAccessException, CertificateException, IOException { diff --git a/src/test/resources/ocsp_response_with_2_responder_certs.der b/src/test/resources/ocsp_response_with_2_responder_certs.der index efc65e5ad1ad4c460fbedb55ead610ec96127dc1..b8520125c410d815d9f6cd0926c98cbe5fbe4b17 100644 GIT binary patch delta 211 zcmew?wnV(%pox0{7bC-hChnO)=2SLLZ8k<$R(1nMMwTY-HlR?GLF3v*Y%DB>M#d%* zVnzl=h6a{K#)js`hQ^k9mKK(lmL^dKjco>v%?6$ZoNTPxe9TNztPCt7M$?@p%}M(j z&VBx}N?%CFwTtUkuVxWB&@xl=h~Io`CX;V7A996xF>q+wRWk`#W->+=JFDeJp9n9H VJGx|F0|PF*7D(Wb**N1i7XbFVJ9Gd5 delta 162 zcmZ1?{#mTvpou%5i;-bL6L$uXnZm}Y&Bn;e%5K2O$kN0e3KR-7s9nUy!eVG-Y$73M zWME`yU}

WNd6;VxnhkVs2t&7-bM{5NaT8z{$p{&Bx3n#mWFt4k4MCSS~d%;L<9& MKmv!<#*??X08zCduK)l5 From 459b4b6f05cad92b0004e2f92c1e56853a8eb0f5 Mon Sep 17 00:00:00 2001 From: Mart Somermaa Date: Thu, 30 Sep 2021 17:25:21 +0300 Subject: [PATCH 3/3] feat(OCSP): make collections immutable in AuthtokeValidationConfiguration.copy() and CertificateValidator.buildTrustAnchorsFromCertificates(), reimplement AIA OCSP access location retriving from certificate with higher level AuthorityInformationAccess API --- README.md | 6 +- .../certificate/CertificateValidator.java | 5 +- .../AuthTokenValidationConfiguration.java | 7 +- .../security/validator/ocsp/OcspUrl.java | 114 ++++++------------ 4 files changed, 45 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 3a0e32d2..7051ad86 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ import org.webeid.security.nonce.NonceGeneratorBuilder; ## 4. Add trusted certificate authority certificates -You must explicitly specify which **intermediate** certificate authorities (CAs) are trusted to issue the eID authentication certificates. CA certificates can be loaded from either the truststore file, resources or any stream source. We use the [`CertificateLoader`](https://github.com/web-eid/web-eid-authtoken-validation-java/blob/main/src/test/java/org/webeid/security/testutil/CertificateLoader.java) helper class from [`testutil`](https://github.com/web-eid/web-eid-authtoken-validation-java/tree/main/src/test/java/org/webeid/security/testutil) to load CA certificates from resources here, but consider using [the truststore file](https://github.com/web-eid/web-eid-spring-boot-example/blob/main/src/main/java/org/webeid/example/config/ValidationConfiguration.java#L104-L122) instead. +You must explicitly specify which **intermediate** certificate authorities (CAs) are trusted to issue the eID authentication and OCSP responder certificates. CA certificates can be loaded from either the truststore file, resources or any stream source. We use the [`CertificateLoader`](https://github.com/web-eid/web-eid-authtoken-validation-java/blob/main/src/test/java/org/webeid/security/testutil/CertificateLoader.java) helper class from [`testutil`](https://github.com/web-eid/web-eid-authtoken-validation-java/tree/main/src/test/java/org/webeid/security/testutil) to load CA certificates from resources here, but consider using [the truststore file](https://github.com/web-eid/web-eid-spring-boot-example/blob/main/src/main/java/org/webeid/example/config/ValidationConfiguration.java#L104-L122) instead. First, copy the trusted certificates, for example `ESTEID-SK_2015.cer` and `ESTEID2018.cer`, to `resources/cacerts/`, then load the certificates as follows: @@ -260,7 +260,7 @@ As described in section *[5. Configure the authentication token validator](#5-co The **nonce cache** instance is used to look up nonce expiry time using its unique value as key. The values in the cache are populated by the nonce generator as described in section *[Nonce generation](#nonce-generation)* below. Consider using [Caffeine](https://github.com/ben-manes/caffeine) or [Ehcache](https://www.ehcache.org/) as the caching provider if your application does not run in a cluster, or [Hazelcast](https://hazelcast.com/), [Infinispan](https://infinispan.org/) or non-Java distributed cahces like [Memcached](https://memcached.org/) or [Redis](https://redis.io/) if it does. Cache configuration is described in more detail in section *[2. Add cache support](#2-add-cache-support)*. -The **trusted certificate authority certificates** are used to validate that the user certificate from the authentication token is signed by a trusted certificate authority. Intermediate CA certificates must be used instead of the root CA certificates so that revoked CA certificates can be detected. Trusted certificate authority certificates configuration is described in more detail in section *[4. Add trusted certificate authority certificates](#4-add-trusted-certificate-authority-certificates)*. +The **trusted certificate authority certificates** are used to validate that the user certificate from the authentication token and the OCSP responder certificate is signed by a trusted certificate authority. Intermediate CA certificates must be used instead of the root CA certificates so that revoked CA certificates can be removed. Trusted certificate authority certificates configuration is described in more detail in section *[4. Add trusted certificate authority certificates](#4-add-trusted-certificate-authority-certificates)*. The authentication token validator configuration and construction is described in more detail in section *[5. Configure the authentication token validator](#5-configure-the-authentication-token-validator)*. Once the validator object has been constructed, it can be used for validating authentication tokens as follows: @@ -287,13 +287,13 @@ toTitleCase(CertUtil.getSubjectSurname(userCertificate)); // "Jõeorg" The following additional configuration options are available in `AuthTokenValidatorBuilder`: -- `withSiteCertificateSha256Fingerprint(String siteCertificateFingerprint)` – turns on origin website certificate fingerprint validation. The validator checks that the site certificate fingerprint from the authentication token matches with the provided site certificate SHA-256 fingerprint. This disables powerful man-in-the-middle attacks where attackers are able to issue falsified certificates for the origin, but also disables TLS proxy usage. Due to the technical limitations of web browsers, certificate fingerprint validation is an experimental feature that currently works only with Firefox. The provided certificate SHA-256 fingerprint should have the prefix `urn:cert:sha-256:` followed by the hexadecimal encoding of the hash value octets as specified in [URN Namespace for Certificates](https://tools.ietf.org/id/draft-seantek-certspec-01.html). Certificate fingerprint validation is disabled by default. - `withoutUserCertificateRevocationCheckWithOcsp()` – turns off user certificate revocation check with OCSP. OCSP check is enabled by default and the OCSP responder access location URL is extracted from the user certificate AIA extension unless a designated OCSP service is activated. - `withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration)` – activates the provided designated OCSP responder service configuration for user certificate revocation check with OCSP. The designated service is only used for checking the status of the certificates whose issuers are supported by the service, for other certificates the default AIA extension service access location will be used. See configuration examples in `testutil.OcspServiceMaker.getDesignatedOcspServiceConfiguration()`. - `withOcspRequestTimeout(Duration ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds. - `withAllowedClientClockSkew(Duration allowedClockSkew)` – sets the tolerated clock skew of the client computer when verifying the token expiration. Default value is 3 minutes. - `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` – adds the given policies to the list of disallowed user certificate policies. In order for the user certificate to be considered valid, it must not contain any policies present in this list. Contains the Estonian Mobile-ID policies by default as it must not be possible to authenticate with a Mobile-ID certificate when an eID smart card is expected. - `withNonceDisabledOcspUrls(URI... urls)` – adds the given URLs to the list of OCSP responder access location URLs for which the nonce protocol extension will be disabled. Some OCSP responders don't support the nonce extension. Contains the ESTEID-2015 OCSP responder URL by default. +- `withSiteCertificateSha256Fingerprint(String siteCertificateFingerprint)` – turns on origin website certificate fingerprint validation. The validator checks that the site certificate fingerprint from the authentication token matches with the provided site certificate SHA-256 fingerprint. This disables powerful man-in-the-middle attacks where attackers are able to issue falsified certificates for the origin, but also disables TLS proxy usage. Due to the technical limitations of web browsers, certificate fingerprint validation is an experimental feature that currently works only with Firefox. The provided certificate SHA-256 fingerprint should have the prefix `urn:cert:sha-256:` followed by the hexadecimal encoding of the hash value octets as specified in [URN Namespace for Certificates](https://tools.ietf.org/id/draft-seantek-certspec-01.html). Certificate fingerprint validation is disabled by default. Extended configuration example: diff --git a/src/main/java/org/webeid/security/certificate/CertificateValidator.java b/src/main/java/org/webeid/security/certificate/CertificateValidator.java index 773b5bd9..4a2a66d5 100644 --- a/src/main/java/org/webeid/security/certificate/CertificateValidator.java +++ b/src/main/java/org/webeid/security/certificate/CertificateValidator.java @@ -40,6 +40,7 @@ import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.Set; import java.util.stream.Collectors; @@ -87,9 +88,9 @@ public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certif } public static Set buildTrustAnchorsFromCertificates(Collection certificates) { - return certificates.stream() + return Collections.unmodifiableSet(certificates.stream() .map(cert -> new TrustAnchor(cert, null)) - .collect(Collectors.toSet()); + .collect(Collectors.toSet())); } public static CertStore buildCertStoreFromCertificates(Collection certificates) throws JceException { diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java b/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java index e2954f86..b6433ce6 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java @@ -33,6 +33,7 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Objects; @@ -70,15 +71,15 @@ public 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) { diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java b/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java index 0bc67cd7..ca8fcc83 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java @@ -1,34 +1,36 @@ /* - * Copyright (c) 2017 The Netty 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 - * in compliance with the License. You may obtain a copy of the License at: + * 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: * - * http://www.apache.org/licenses/LICENSE-2.0 + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. * - * 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. + * 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.ASN1Encodable; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.ASN1Primitive; -import org.bouncycastle.asn1.BERTags; -import org.bouncycastle.asn1.DLSequence; -import org.bouncycastle.asn1.DLTaggedObject; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +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.nio.charset.StandardCharsets; +import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.Objects; @@ -36,74 +38,28 @@ public final class OcspUrl { public static final URI AIA_ESTEID_2015 = URI.create("http://aia.sk.ee/esteid2015"); - /** - * The OID for OCSP responder URLs. - *

- * https://oidref.com/1.3.6.1.5.5.7.48.1 - */ - private static final ASN1ObjectIdentifier OCSP_RESPONDER_OID - = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1").intern(); - /** * 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 byte[] value = certificate.getExtensionValue(Extension.authorityInfoAccess.getId()); - if (value == null) { - return null; - } - - final ASN1Primitive authorityInfoAccess; + final X509CertificateHolder certificateHolder; try { - authorityInfoAccess = JcaX509ExtensionUtils.parseExtensionValue(value); - } catch (IOException | IllegalArgumentException e) { - return null; - } - 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; - try { - encoded = taggedObject.getEncoded(); - } catch (IOException e) { - return null; - } - int length = 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); + 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; }