diff --git a/README.md b/README.md index baf3a28f..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,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. +- `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. +- `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: @@ -311,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 @@ -343,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. diff --git a/pom.xml b/pom.xml index a3339adf..d57f98bb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 authtoken-validation org.webeid.security - 1.1.0 + 1.2.0 jar authtoken-validation Web eID authentication token validation library for Java @@ -16,9 +16,11 @@ 1.8 0.11.2 1.7.30 + 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 @@ -67,10 +69,15 @@ guava 30.1-jre + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + org.bouncycastle bcpkix-jdk15on - 1.65 + ${bouncycastle.version} com.squareup.okhttp3 @@ -90,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/util/CertUtil.java b/src/main/java/org/webeid/security/certificate/CertificateData.java similarity index 73% rename from src/main/java/org/webeid/security/util/CertUtil.java rename to src/main/java/org/webeid/security/certificate/CertificateData.java index 9c1031c9..13a11802 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,38 +34,44 @@ 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); + 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(", ")); } - 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..4a2a66d5 --- /dev/null +++ b/src/main/java/org/webeid/security/certificate/CertificateValidator.java @@ -0,0 +1,111 @@ +/* + * 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.CertificateExpiredException; +import org.webeid.security.exceptions.CertificateNotYetValidException; + +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.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 { + + public static void certificateIsValidOnDate(X509Certificate cert, Date date, String subject) throws CertificateNotYetValidException, CertificateExpiredException { + try { + cert.checkValidity(date); + } 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"); + } + } + + 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 Collections.unmodifiableSet(certificates.stream() + .map(cert -> new TrustAnchor(cert, null)) + .collect(Collectors.toSet())); + } + + 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); + } + } + + private CertificateValidator() { + throw new IllegalStateException("Utility class"); + } + +} diff --git a/src/main/java/org/webeid/security/exceptions/UserCertificateNotYetValidException.java b/src/main/java/org/webeid/security/exceptions/CertificateExpiredException.java similarity index 76% rename from src/main/java/org/webeid/security/exceptions/UserCertificateNotYetValidException.java rename to src/main/java/org/webeid/security/exceptions/CertificateExpiredException.java index 3d677b88..ae879719 100644 --- a/src/main/java/org/webeid/security/exceptions/UserCertificateNotYetValidException.java +++ b/src/main/java/org/webeid/security/exceptions/CertificateExpiredException.java @@ -1,5 +1,5 @@ /* - * 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 @@ -23,10 +23,10 @@ package org.webeid.security.exceptions; /** - * Thrown when the user certificate valid from date is in the future. + * Thrown when the certificate's valid until date is in the past. */ -public class UserCertificateNotYetValidException extends TokenValidationException { - public UserCertificateNotYetValidException(Throwable cause) { - super("User certificate is not yet valid:", cause); +public class CertificateExpiredException extends TokenValidationException { + public CertificateExpiredException(String subject, Throwable cause) { + super(subject + " certificate has expired", cause); } } 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/UserCertificateExpiredException.java b/src/main/java/org/webeid/security/exceptions/CertificateNotYetValidException.java similarity index 77% rename from src/main/java/org/webeid/security/exceptions/UserCertificateExpiredException.java rename to src/main/java/org/webeid/security/exceptions/CertificateNotYetValidException.java index 64ab5395..44c4a540 100644 --- a/src/main/java/org/webeid/security/exceptions/UserCertificateExpiredException.java +++ b/src/main/java/org/webeid/security/exceptions/CertificateNotYetValidException.java @@ -1,5 +1,5 @@ /* - * 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 @@ -23,10 +23,10 @@ package org.webeid.security.exceptions; /** - * Thrown when the user certificate valid until date is in the past. + * Thrown when the certificate's valid from date is in the future. */ -public class UserCertificateExpiredException extends TokenValidationException { - public UserCertificateExpiredException(Throwable cause) { - super("User certificate has expired:", cause); +public class CertificateNotYetValidException extends TokenValidationException { + public CertificateNotYetValidException(String subject, Throwable cause) { + super(subject + " certificate is not yet valid", cause); } } 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..4b706f8b --- /dev/null +++ b/src/main/java/org/webeid/security/exceptions/OCSPCertificateException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, 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: + * + * 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.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/UserCertificateInvalidPolicyException.java b/src/main/java/org/webeid/security/exceptions/UserCertificateInvalidPolicyException.java deleted file mode 100644 index fb2143ff..00000000 --- a/src/main/java/org/webeid/security/exceptions/UserCertificateInvalidPolicyException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.webeid.security.exceptions; - -/** - * Thrown when the user certificate policy is invalid. - */ -public class UserCertificateInvalidPolicyException extends TokenValidationException { - public UserCertificateInvalidPolicyException() { - super("User certificate policy is invalid"); - } -} 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..b6433ce6 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java @@ -24,6 +24,7 @@ import com.google.common.collect.Sets; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; import org.webeid.security.validator.validators.OriginValidator; import javax.cache.Cache; @@ -32,17 +33,18 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; 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.validator.ocsp.OcspUrl.AIA_ESTEID_2015; import static org.webeid.security.util.SubjectCertificatePolicies.*; /** * Stores configuration parameters for {@link AuthTokenValidatorImpl}. */ -final class AuthTokenValidationConfiguration { +public final class AuthTokenValidationConfiguration { private URI siteOrigin; private Cache nonceCache; @@ -50,6 +52,7 @@ final class AuthTokenValidationConfiguration { private boolean isUserCertificateRevocationCheckWithOcspEnabled = true; private Duration ocspRequestTimeout = Duration.ofSeconds(5); private Duration allowedClientClockSkew = Duration.ofMinutes(3); + private DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration; private boolean isSiteCertificateFingerprintValidationEnabled = false; private String siteCertificateSha256Fingerprint; // Don't allow Estonian Mobile-ID policy by default. @@ -60,7 +63,7 @@ final class AuthTokenValidationConfiguration { ESTEID_SK_2015_MOBILE_ID_POLICY ); // Disable OCSP nonce extension for EstEID 2015 cards by default. - private Collection nonceDisabledOcspUrls = Sets.newHashSet(ESTEID_2015); + private Collection nonceDisabledOcspUrls = Sets.newHashSet(AIA_ESTEID_2015); AuthTokenValidationConfiguration() { } @@ -68,14 +71,15 @@ final class AuthTokenValidationConfiguration { private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) { this.siteOrigin = other.siteOrigin; this.nonceCache = other.nonceCache; - this.trustedCACertificates = new HashSet<>(other.trustedCACertificates); + this.trustedCACertificates = Collections.unmodifiableSet(new HashSet<>(other.trustedCACertificates)); this.isUserCertificateRevocationCheckWithOcspEnabled = other.isUserCertificateRevocationCheckWithOcspEnabled; this.ocspRequestTimeout = other.ocspRequestTimeout; this.allowedClientClockSkew = other.allowedClientClockSkew; + this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration; this.isSiteCertificateFingerprintValidationEnabled = other.isSiteCertificateFingerprintValidationEnabled; this.siteCertificateSha256Fingerprint = other.siteCertificateSha256Fingerprint; - this.disallowedSubjectCertificatePolicies = new HashSet<>(other.disallowedSubjectCertificatePolicies); - this.nonceDisabledOcspUrls = new HashSet<>(other.nonceDisabledOcspUrls); + this.disallowedSubjectCertificatePolicies = Collections.unmodifiableSet(new HashSet<>(other.disallowedSubjectCertificatePolicies)); + this.nonceDisabledOcspUrls = Collections.unmodifiableSet(new HashSet<>(other.nonceDisabledOcspUrls)); } void setSiteOrigin(URI siteOrigin) { @@ -122,6 +126,14 @@ Duration getAllowedClientClockSkew() { return allowedClientClockSkew; } + public DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() { + return designatedOcspServiceConfiguration; + } + + public void setDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration) { + this.designatedOcspServiceConfiguration = designatedOcspServiceConfiguration; + } + boolean isSiteCertificateFingerprintValidationEnabled() { return isSiteCertificateFingerprintValidationEnabled; } diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidator.java b/src/main/java/org/webeid/security/validator/AuthTokenValidator.java index 8e5ba604..69d8c55d 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidator.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidator.java @@ -23,7 +23,7 @@ package org.webeid.security.validator; import org.webeid.security.exceptions.TokenValidationException; -import org.webeid.security.util.CertUtil; +import org.webeid.security.certificate.CertificateData; import org.webeid.security.util.TitleCase; import java.security.cert.X509Certificate; @@ -37,7 +37,7 @@ public interface AuthTokenValidator { * Validates the Web eID authentication token signed by the subject and returns * the subject certificate that can be used for retrieving information about the subject. *

- * See {@link CertUtil} and {@link TitleCase} for convenience methods for retrieving user + * See {@link CertificateData} and {@link TitleCase} for convenience methods for retrieving user * information from the certificate. * * @param tokenWithSignature the Web eID authentication token, in OpenID X509 ID Token format, with signature diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java b/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java index 9c5be7ef..c9e632c6 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 The Web eID Project + * Copyright (c) 2020-2021 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.webeid.security.exceptions.JceException; +import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; import javax.cache.Cache; import java.net.URI; @@ -33,6 +34,7 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.util.Collections; +import java.util.stream.Collectors; /** * Builder for constructing {@link AuthTokenValidator} instances. @@ -73,9 +75,10 @@ public AuthTokenValidatorBuilder withNonceCache(Cache cac } /** - * Adds the given certificates to the list of trusted subject certificate intermediate Certificate Authorities. - * In order for the user certificate to be considered valid, the certificate of the issuer of the user certificate - * must be present in this list. + * Adds the given certificates to the list of trusted intermediate Certificate Authorities + * used during validation of subject and OCSP responder certificates. + * In order for a user or OCSP responder certificate to be considered valid, the certificate + * of the issuer of the certificate must be present in this list. *

* At least one trusted intermediate Certificate Authority must be provided as a mandatory configuration parameter. * @@ -84,7 +87,12 @@ public AuthTokenValidatorBuilder withNonceCache(Cache cac */ public AuthTokenValidatorBuilder withTrustedCertificateAuthorities(X509Certificate... certificates) { Collections.addAll(configuration.getTrustedCACertificates(), certificates); - LOG.debug("Trusted intermediate certificate authorities set to {}", configuration.getTrustedCACertificates()); + if (LOG.isDebugEnabled()) { + LOG.debug("Trusted intermediate certificate authorities set to {}", + configuration.getTrustedCACertificates().stream() + .map(X509Certificate::getSubjectDN) + .collect(Collectors.toList())); + } return this; } @@ -125,7 +133,7 @@ public AuthTokenValidatorBuilder withoutUserCertificateRevocationCheckWithOcsp() */ public AuthTokenValidatorBuilder withOcspRequestTimeout(Duration ocspRequestTimeout) { configuration.setOcspRequestTimeout(ocspRequestTimeout); - LOG.debug("OCSP request timeout set to {}.", ocspRequestTimeout); + LOG.debug("OCSP request timeout set to {}", ocspRequestTimeout); return this; } @@ -142,6 +150,21 @@ public AuthTokenValidatorBuilder withNonceDisabledOcspUrls(URI... urls) { return this; } + /** + * Activates the provided designated OCSP service for user certificate revocation check with OCSP. + * The designated service is only used for checking the status of the certificates whose issuers are + * supported by the service, falling back to the default OCSP service access location from + * the certificate's AIA extension if not. + * + * @param serviceConfiguration configuration of the designated OCSP service + * @return the builder instance for method chaining + */ + public AuthTokenValidatorBuilder withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration) { + configuration.setDesignatedOcspServiceConfiguration(serviceConfiguration); + LOG.debug("Using designated OCSP service configuration"); + return this; + } + /** * Sets the tolerated clock skew of the client computer when verifying the token expiration field {@code exp}. *

diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidatorData.java b/src/main/java/org/webeid/security/validator/AuthTokenValidatorData.java index 9115758a..9b0fb5d3 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorData.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorData.java @@ -36,7 +36,7 @@ public final class AuthTokenValidatorData { private String origin; private String siteCertificateFingerprint; - AuthTokenValidatorData(X509Certificate subjectCertificate) { + public AuthTokenValidatorData(X509Certificate subjectCertificate) { this.subjectCertificate = subjectCertificate; } diff --git a/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java b/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java index d2952021..917e1d2c 100644 --- a/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java +++ b/src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java @@ -22,23 +22,24 @@ package org.webeid.security.validator; -import com.google.common.base.Suppliers; -import okhttp3.OkHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.webeid.security.exceptions.JceException; import org.webeid.security.exceptions.TokenParseException; import org.webeid.security.exceptions.TokenValidationException; +import org.webeid.security.validator.ocsp.OcspClient; +import org.webeid.security.validator.ocsp.OcspClientImpl; +import org.webeid.security.validator.ocsp.OcspServiceProvider; +import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; import org.webeid.security.validator.validators.*; -import java.security.GeneralSecurityException; import java.security.cert.CertStore; -import java.security.cert.CollectionCertStoreParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; + +import static org.webeid.security.certificate.CertificateValidator.buildCertStoreFromCertificates; +import static org.webeid.security.certificate.CertificateValidator.buildTrustAnchorsFromCertificates; /** * Provides the default implementation of {@link AuthTokenValidator}. @@ -50,16 +51,16 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { private static final Logger LOG = LoggerFactory.getLogger(AuthTokenValidatorImpl.class); private final AuthTokenValidationConfiguration configuration; - /* - * OkHttp performs best when a single OkHttpClient instance is created and reused for all HTTP calls. - * This is because each client holds its own connection pool and thread pools. - * Reusing connections and threads reduces latency and saves memory. - */ - private final Supplier httpClientSupplier; private final ValidatorBatch simpleSubjectCertificateValidators; private final ValidatorBatch tokenBodyValidators; private final Set trustedCACertificateAnchors; private final CertStore trustedCACertificateCertStore; + // OcspClient uses OkHttp internally. + // OkHttp performs best when a single OkHttpClient instance is created and reused for all HTTP calls. + // This is because each client holds its own connection pool and thread pools. + // Reusing connections and threads reduces latency and saves memory. + private OcspClient ocspClient; + private OcspServiceProvider ocspServiceProvider; /** * @param configuration configuration parameters for the token validator @@ -67,19 +68,14 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration) throws JceException { // Copy the configuration object to make AuthTokenValidatorImpl immutable and thread-safe. this.configuration = configuration.copy(); - // Lazy initialization, avoid constructing the OkHttpClient object when certificate revocation check is not enabled. - // Returns a supplier which caches the instance retrieved during the first call to get() and returns - // that value on subsequent calls to get(). The returned supplier is thread-safe. - // The OkHttpClient build() method will be invoked at most once. - this.httpClientSupplier = Suppliers.memoize(() -> new OkHttpClient.Builder() - .connectTimeout(configuration.getOcspRequestTimeout()) - .callTimeout(configuration.getOcspRequestTimeout()) - .build() - ); + + // Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator and AiaOcspService. + trustedCACertificateAnchors = buildTrustAnchorsFromCertificates(configuration.getTrustedCACertificates()); + trustedCACertificateCertStore = buildCertStoreFromCertificates(configuration.getTrustedCACertificates()); simpleSubjectCertificateValidators = ValidatorBatch.createFrom( - FunctionalSubjectCertificateValidators::validateCertificateExpiry, - FunctionalSubjectCertificateValidators::validateCertificatePurpose, + new CertificateExpiryValidator(trustedCACertificateAnchors)::validateCertificateExpiry, + SubjectCertificatePurposeValidator::validateCertificatePurpose, new SubjectCertificatePolicyValidator(configuration.getDisallowedSubjectCertificatePolicies())::validateCertificatePolicies ); tokenBodyValidators = ValidatorBatch.createFrom( @@ -89,19 +85,13 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { new SiteCertificateFingerprintValidator(configuration.getSiteCertificateSha256Fingerprint())::validateSiteCertificateFingerprint ); - // Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator. - trustedCACertificateAnchors = configuration.getTrustedCACertificates() - .stream() - .map(cert -> new TrustAnchor(cert, null)) - .collect(Collectors.toSet()); - try { - // We use the default JCE provider as there is no reason to use Bouncy Castle, moreover BC requires - // the validated certificate to be in the certificate store which breaks the clean immutable usage of - // trustedCACertificateCertStore in SubjectCertificateTrustedValidator. - trustedCACertificateCertStore = CertStore.getInstance("Collection", - new CollectionCertStoreParameters(configuration.getTrustedCACertificates())); - } catch (GeneralSecurityException e) { - throw new JceException(e); + if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) { + ocspClient = OcspClientImpl.build(configuration.getOcspRequestTimeout()); + ocspServiceProvider = new OcspServiceProvider( + configuration.getDesignatedOcspServiceConfiguration(), + new AiaOcspServiceConfiguration(configuration.getNonceDisabledOcspUrls(), + trustedCACertificateAnchors, + trustedCACertificateCertStore)); } } @@ -153,7 +143,7 @@ private ValidatorBatch getCertTrustValidators() { return ValidatorBatch.createFrom( certTrustedValidator::validateCertificateTrusted ).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(), - new SubjectCertificateNotRevokedValidator(certTrustedValidator, httpClientSupplier.get(), configuration.getNonceDisabledOcspUrls())::validateCertificateNotRevoked + new SubjectCertificateNotRevokedValidator(certTrustedValidator, ocspClient, ocspServiceProvider)::validateCertificateNotRevoked ); } } diff --git a/src/main/java/org/webeid/security/validator/ocsp/Digester.java b/src/main/java/org/webeid/security/validator/ocsp/Digester.java index feb7248c..777b6b58 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/Digester.java +++ b/src/main/java/org/webeid/security/validator/ocsp/Digester.java @@ -1,25 +1,3 @@ -/* - * Copyright (c) 2020 The Web eID Project - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - /* * Copyright 2017 The Netty Project * Copyright 2020 The Web eID project diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java b/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java new file mode 100644 index 00000000..7b5ad6d6 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspClient.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp; + +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; + +import java.io.IOException; +import java.net.URI; + +public interface OcspClient { + + OCSPResp request(URI url, OCSPReq request) throws IOException; + +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java b/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java new file mode 100644 index 00000000..cb63cba4 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspClientImpl.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.util.Objects; + +public class OcspClientImpl implements OcspClient { + + private static final Logger LOG = LoggerFactory.getLogger(OcspClientImpl.class); + private static final MediaType OCSP_REQUEST_TYPE = MediaType.get("application/ocsp-request"); + private static final MediaType OCSP_RESPONSE_TYPE = MediaType.get("application/ocsp-response"); + + private final OkHttpClient httpClient; + + public static OcspClient build(Duration ocspRequestTimeout) { + return new OcspClientImpl( + new OkHttpClient.Builder() + .connectTimeout(ocspRequestTimeout) + .callTimeout(ocspRequestTimeout) + .build() + ); + } + + /** + * Use OkHttpClient to fetch the OCSP response from the OCSP responder service. + * + * @param uri OCSP server URL + * @param ocspReq OCSP request + * @return OCSP response from the server + * @throws IOException if the request could not be executed due to cancellation, a connectivity problem or timeout, + * or if the response status is not successful, or if response has wrong content type. + */ + @Override + public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException { + final RequestBody requestBody = RequestBody.create(ocspReq.getEncoded(), OCSP_REQUEST_TYPE); + final Request request = new Request.Builder() + .url(uri.toURL()) + .post(requestBody) + .build(); + + try (final Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("OCSP request was not successful, response: " + response); + } else { + LOG.debug("OCSP response: {}", response); + } + try (final ResponseBody responseBody = Objects.requireNonNull(response.body(), "response body")) { + Objects.requireNonNull(responseBody.contentType(), "response content type"); + if (!OCSP_RESPONSE_TYPE.type().equals(responseBody.contentType().type()) || + !OCSP_RESPONSE_TYPE.subtype().equals(responseBody.contentType().subtype())) { + throw new IOException("OCSP response content type is not " + OCSP_RESPONSE_TYPE); + } + return new OCSPResp(responseBody.bytes()); + } + } + } + + private OcspClientImpl(OkHttpClient httpClient) { + this.httpClient = httpClient; + } + +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java b/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java index 3805cbb5..bfbb0dd1 100644 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspRequestBuilder.java @@ -1,28 +1,6 @@ -/* - * Copyright (c) 2020 The Web eID Project - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - /* * Copyright 2017 The Netty Project - * Copyright 2020 The Web eID project + * Copyright (c) 2020-2021 Estonian Information System Authority * * The Netty Project and The Web eID Project license this file to you under the * Apache License, version 2.0 (the "License"); you may not use this file except @@ -43,18 +21,12 @@ import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.Extensions; -import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.ocsp.CertificateID; import org.bouncycastle.cert.ocsp.OCSPException; import org.bouncycastle.cert.ocsp.OCSPReq; import org.bouncycastle.cert.ocsp.OCSPReqBuilder; -import org.bouncycastle.operator.DigestCalculator; -import java.io.IOException; -import java.math.BigInteger; import java.security.SecureRandom; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; import java.util.Objects; /** @@ -66,29 +38,11 @@ public final class OcspRequestBuilder { private static final SecureRandom GENERATOR = new SecureRandom(); - private SecureRandom randomGenerator = GENERATOR; - private DigestCalculator digestCalculator = Digester.sha1(); - private X509Certificate subjectCertificate; - private X509Certificate issuerCertificate; private boolean ocspNonceEnabled = true; + private CertificateID certificateId; - public OcspRequestBuilder generator(SecureRandom generator) { - this.randomGenerator = generator; - return this; - } - - public OcspRequestBuilder calculator(DigestCalculator calculator) { - this.digestCalculator = calculator; - return this; - } - - public OcspRequestBuilder certificate(X509Certificate certificate) { - this.subjectCertificate = certificate; - return this; - } - - public OcspRequestBuilder issuer(X509Certificate issuer) { - this.issuerCertificate = issuer; + public OcspRequestBuilder withCertificateId(CertificateID certificateId) { + this.certificateId = certificateId; return this; } @@ -98,21 +52,12 @@ public OcspRequestBuilder enableOcspNonce(boolean ocspNonceEnabled) { } /** - * ATTENTION: The returned {@link OCSPReq} is not re-usable/cacheable! It contains a one-time nonce - * and CA's will (should) reject subsequent requests that have the same nonce value. + * The returned {@link OCSPReq} is not re-usable/cacheable. It contains a one-time nonce + * and responders will reject subsequent requests that have the same nonce value. */ - public OCSPReq build() throws OCSPException, IOException, CertificateEncodingException { - final DigestCalculator calculator = Objects.requireNonNull(this.digestCalculator, "digestCalculator"); - final X509Certificate certificate = Objects.requireNonNull(this.subjectCertificate, "subjectCertificate"); - final X509Certificate issuer = Objects.requireNonNull(this.issuerCertificate, "issuerCertificate"); - - final BigInteger serial = certificate.getSerialNumber(); - - final CertificateID certId = new CertificateID(calculator, - new X509CertificateHolder(issuer.getEncoded()), serial); - + public OCSPReq build() throws OCSPException { final OCSPReqBuilder builder = new OCSPReqBuilder(); - builder.addRequest(certId); + builder.addRequest(Objects.requireNonNull(certificateId, "certificateId")); if (ocspNonceEnabled) { addNonce(builder); @@ -122,16 +67,13 @@ public OCSPReq build() throws OCSPException, IOException, CertificateEncodingExc } private void addNonce(OCSPReqBuilder builder) { - final SecureRandom generator = Objects.requireNonNull(this.randomGenerator, "randomGenerator"); - final byte[] nonce = new byte[8]; - generator.nextBytes(nonce); + GENERATOR.nextBytes(nonce); final Extension[] extensions = new Extension[]{ new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, new DEROctetString(nonce)) }; - builder.setRequestExtensions(new Extensions(extensions)); } diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspResponseValidator.java b/src/main/java/org/webeid/security/validator/ocsp/OcspResponseValidator.java new file mode 100644 index 00000000..8f7610cc --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspResponseValidator.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.CertificateStatus; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.RevokedStatus; +import org.bouncycastle.cert.ocsp.SingleResp; +import org.bouncycastle.cert.ocsp.UnknownStatus; +import org.bouncycastle.operator.ContentVerifierProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; +import org.webeid.security.exceptions.OCSPCertificateException; +import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; +import org.webeid.security.exceptions.UserCertificateRevokedException; + +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Objects; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +public final class OcspResponseValidator { + + /** + * Indicates that a X.509 Certificates corresponding private key may be used by an authority to sign OCSP responses. + *

+ * https://oidref.com/1.3.6.1.5.5.7.3.9 + */ + private static final String OID_OCSP_SIGNING = "1.3.6.1.5.5.7.3.9"; + + private static final long ALLOWED_TIME_SKEW = TimeUnit.MINUTES.toMillis(15); + + public static void validateHasSigningExtension(X509Certificate certificate) throws OCSPCertificateException { + Objects.requireNonNull(certificate, "certificate"); + try { + if (certificate.getExtendedKeyUsage() == null || !certificate.getExtendedKeyUsage().contains(OID_OCSP_SIGNING)) { + throw new OCSPCertificateException("Certificate " + certificate.getSubjectDN() + + " does not contain the key usage extension for OCSP response signing"); + } + } catch (CertificateParsingException e) { + throw new OCSPCertificateException("Certificate parsing failed:", e); + } + } + + public static void validateResponseSignature(BasicOCSPResp basicResponse, X509CertificateHolder responderCert) throws CertificateException, OperatorCreationException, OCSPException, UserCertificateOCSPCheckFailedException { + final ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder() + .setProvider("BC") + .build(responderCert); + if (!basicResponse.isSignatureValid(verifierProvider)) { + throw new UserCertificateOCSPCheckFailedException("OCSP response signature is invalid"); + } + } + + public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Date producedAt) throws UserCertificateOCSPCheckFailedException { + // From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt: + // 4.2.2. Notes on OCSP Responses + // 4.2.2.1. Time + // Responses whose nextUpdate value is earlier than + // the local system time value SHOULD be considered unreliable. + // Responses whose thisUpdate time is later than the local system time + // SHOULD be considered unreliable. + // If nextUpdate is not set, the responder is indicating that newer + // revocation information is available all the time. + final Date notAllowedBefore = new Date(producedAt.getTime() - ALLOWED_TIME_SKEW); + final Date notAllowedAfter = new Date(producedAt.getTime() + ALLOWED_TIME_SKEW); + if (notAllowedAfter.before(certStatusResponse.getThisUpdate()) || + notAllowedBefore.after(certStatusResponse.getNextUpdate() != null ? + certStatusResponse.getNextUpdate() : + certStatusResponse.getThisUpdate())) { + throw new UserCertificateOCSPCheckFailedException("Certificate status update time check failed: " + + "notAllowedBefore: " + toUtcString(notAllowedBefore) + + ", notAllowedAfter: " + toUtcString(notAllowedAfter) + + ", thisUpdate: " + toUtcString(certStatusResponse.getThisUpdate()) + + ", nextUpdate: " + toUtcString(certStatusResponse.getNextUpdate())); + } + } + + public static void validateSubjectCertificateStatus(SingleResp certStatusResponse) throws UserCertificateRevokedException { + final CertificateStatus status = certStatusResponse.getCertStatus(); + if (status == null) { + return; + } + if (status instanceof RevokedStatus) { + RevokedStatus revokedStatus = (RevokedStatus) status; + throw (revokedStatus.hasRevocationReason() ? + new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason()) : + new UserCertificateRevokedException()); + } else if (status instanceof UnknownStatus) { + throw new UserCertificateRevokedException("Unknown status"); + } else { + throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown"); + } + } + + private static String toUtcString(Date date) { + if (date == null) { + return String.valueOf((Object) null); + } + final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); + dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormatter.format(date); + } + + private OcspResponseValidator() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java b/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java new file mode 100644 index 00000000..b85831a6 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspServiceProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp; + +import org.webeid.security.exceptions.TokenValidationException; +import org.webeid.security.validator.ocsp.service.AiaOcspService; +import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; +import org.webeid.security.validator.ocsp.service.DesignatedOcspService; +import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; +import org.webeid.security.validator.ocsp.service.OcspService; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Objects; + +public class OcspServiceProvider { + + private final DesignatedOcspService designatedOcspService; + private final AiaOcspServiceConfiguration aiaOcspServiceConfiguration; + + public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration, AiaOcspServiceConfiguration aiaOcspServiceConfiguration) { + designatedOcspService = designatedOcspServiceConfiguration != null ? + new DesignatedOcspService(designatedOcspServiceConfiguration) + : null; + this.aiaOcspServiceConfiguration = Objects.requireNonNull(aiaOcspServiceConfiguration, "aiaOcspServiceConfiguration"); + } + + /** + * A static factory method that returns either the designated or AIA OCSP service instance depending on whether + * the designated OCSP service is configured and supports the issuer of the certificate. + * + * @param certificate subject certificate that is to be checked with OCSP + * @return either the designated or AIA OCSP service instance + * @throws TokenValidationException when AIA URL is not found in certificate + * @throws CertificateEncodingException when certificate is invalid + */ + public OcspService getService(X509Certificate certificate) throws TokenValidationException, CertificateEncodingException { + if (designatedOcspService != null && designatedOcspService.supportsIssuerOf(certificate)) { + return designatedOcspService; + } + return new AiaOcspService(aiaOcspServiceConfiguration, certificate); + } + +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java b/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java new file mode 100644 index 00000000..ca8fcc83 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/OcspUrl.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp; + +import org.bouncycastle.asn1.ASN1String; +import org.bouncycastle.asn1.x509.AccessDescription; +import org.bouncycastle.asn1.x509.AuthorityInformationAccess; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.cert.X509CertificateHolder; + +import java.io.IOException; +import java.net.URI; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Objects; + +public final class OcspUrl { + + public static final URI AIA_ESTEID_2015 = URI.create("http://aia.sk.ee/esteid2015"); + + /** + * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one. + */ + public static URI getOcspUri(X509Certificate certificate) { + Objects.requireNonNull(certificate, "certificate"); + final X509CertificateHolder certificateHolder; + try { + certificateHolder = new X509CertificateHolder(certificate.getEncoded()); + final AuthorityInformationAccess authorityInformationAccess = + AuthorityInformationAccess.fromExtensions(certificateHolder.getExtensions()); + for (AccessDescription accessDescription : + authorityInformationAccess.getAccessDescriptions()) { + if (accessDescription.getAccessMethod().equals(AccessDescription.id_ad_ocsp) && + accessDescription.getAccessLocation().getTagNo() == GeneralName.uniformResourceIdentifier) { + final String accessLocationUrl = ((ASN1String) accessDescription.getAccessLocation().getName()) + .getString(); + return URI.create(accessLocationUrl); + } + } + } catch (IOException | CertificateEncodingException | IllegalArgumentException | NullPointerException e) { + return null; + } + return null; + } + + private OcspUrl() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java b/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java deleted file mode 100644 index 168cf369..00000000 --- a/src/main/java/org/webeid/security/validator/ocsp/OcspUtils.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (c) 2020 The Web eID Project - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/* - * Copyright 2017 The Netty Project - * Copyright 2020 The Web eID project - * - * The Netty Project and The Web eID Project license this file to you under the - * Apache License, version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.webeid.security.validator.ocsp; - -import okhttp3.*; -import org.bouncycastle.asn1.*; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; -import org.bouncycastle.cert.ocsp.OCSPReq; -import org.bouncycastle.cert.ocsp.OCSPResp; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.Objects; - -public final class OcspUtils { - - private static final Logger LOG = LoggerFactory.getLogger(OcspUtils.class); - - /** - * The OID for OCSP responder URLs. - *

- * http://www.alvestrand.no/objectid/1.3.6.1.5.5.7.48.1.html - */ - private static final ASN1ObjectIdentifier OCSP_RESPONDER_OID - = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1").intern(); - - private static final MediaType OCSP_REQUEST_TYPE = MediaType.get("application/ocsp-request"); - private static final MediaType OCSP_RESPONSE_TYPE = MediaType.get("application/ocsp-response"); - - private OcspUtils() { - } - - /** - * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one. - */ - public static URI ocspUri(X509Certificate certificate) throws IOException { - final byte[] value = certificate.getExtensionValue(Extension.authorityInfoAccess.getId()); - if (value == null) { - return null; - } - - final ASN1Primitive authorityInfoAccess = JcaX509ExtensionUtils.parseExtensionValue(value); - if (!(authorityInfoAccess instanceof DLSequence)) { - return null; - } - - final DLSequence aiaSequence = (DLSequence) authorityInfoAccess; - final DLTaggedObject taggedObject = findObject(aiaSequence, OCSP_RESPONDER_OID, DLTaggedObject.class); - if (taggedObject == null) { - return null; - } - - if (taggedObject.getTagNo() != BERTags.OBJECT_IDENTIFIER) { - return null; - } - - final byte[] encoded = taggedObject.getEncoded(); - int length = (int) encoded[1] & 0xFF; - final String uri = new String(encoded, 2, length, StandardCharsets.UTF_8); - return URI.create(uri); - } - - private static T findObject(DLSequence sequence, ASN1ObjectIdentifier oid, Class type) { - for (final ASN1Encodable element : sequence) { - if (!(element instanceof DLSequence)) { - continue; - } - - final DLSequence subSequence = (DLSequence) element; - if (subSequence.size() != 2) { - continue; - } - - final ASN1Encodable key = subSequence.getObjectAt(0); - final ASN1Encodable value = subSequence.getObjectAt(1); - - if (key.equals(oid) && type.isInstance(value)) { - return type.cast(value); - } - } - - return null; - } - - /** - * Use OkHttpClient to fetch the OCSP response from the CA's OCSP responder server. - * - * @param uri OCSP server URL - * @param ocspReq OCSP request - * @param httpClient OkHttpClient instance - * @return OCSP response from the server - * @throws IOException if the request could not be executed due to cancellation, a connectivity problem or timeout, - * or if the response status is not successful, or if response has wrong content type. - */ - public static OCSPResp request(URI uri, OCSPReq ocspReq, OkHttpClient httpClient) throws IOException { - final RequestBody requestBody = RequestBody.create(ocspReq.getEncoded(), OCSP_REQUEST_TYPE); - final Request request = new Request.Builder() - .url(uri.toURL()) - .post(requestBody) - .build(); - - try (final Response response = httpClient.newCall(request).execute()) { - if (!response.isSuccessful()) { - throw new IOException("OCSP request was not successful, response: " + response); - } else { - LOG.debug("OCSP response: {}", response); - } - try (final ResponseBody responseBody = Objects.requireNonNull(response.body())) { - if (!OCSP_RESPONSE_TYPE.equals(responseBody.contentType())) { - throw new IOException("OCSP response content type is not " + OCSP_RESPONSE_TYPE); - } - return new OCSPResp(responseBody.bytes()); - } - } - } - -} diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java new file mode 100644 index 00000000..4b8bef36 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspService.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp.service; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.webeid.security.exceptions.OCSPCertificateException; +import org.webeid.security.exceptions.TokenValidationException; +import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; + +import java.net.URI; +import java.security.cert.CertStore; +import java.security.cert.CertificateException; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Objects; +import java.util.Set; + +import static org.webeid.security.certificate.CertificateValidator.certificateIsValidOnDate; +import static org.webeid.security.certificate.CertificateValidator.validateIsSignedByTrustedCA; +import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateHasSigningExtension; +import static org.webeid.security.validator.ocsp.OcspUrl.getOcspUri; + +/** + * An OCSP service that uses the responders from the Certificates' Authority Information Access (AIA) extension. + */ +public class AiaOcspService implements OcspService { + + private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + private final Set trustedCACertificateAnchors; + private final CertStore trustedCACertificateCertStore; + private final URI url; + private final boolean supportsNonce; + + public AiaOcspService(AiaOcspServiceConfiguration configuration, X509Certificate certificate) throws TokenValidationException { + Objects.requireNonNull(configuration); + this.trustedCACertificateAnchors = configuration.getTrustedCACertificateAnchors(); + this.trustedCACertificateCertStore = configuration.getTrustedCACertificateCertStore(); + this.url = getOcspAiaUrlFromCertificate(Objects.requireNonNull(certificate)); + this.supportsNonce = !configuration.getNonceDisabledOcspUrls().contains(this.url); + } + + @Override + public boolean doesSupportNonce() { + return supportsNonce; + } + + @Override + public URI getAccessLocation() { + return url; + } + + @Override + public void validateResponderCertificate(X509CertificateHolder cert, Date producedAt) throws TokenValidationException { + try { + final X509Certificate certificate = certificateConverter.getCertificate(cert); + certificateIsValidOnDate(certificate, producedAt, "AIA OCSP responder"); + // Trusted certificates' validity has been already verified in validateCertificateExpiry(). + validateHasSigningExtension(certificate); + validateIsSignedByTrustedCA(certificate, trustedCACertificateAnchors, trustedCACertificateCertStore); + } catch (CertificateException e) { + throw new OCSPCertificateException("Invalid responder certificate", e); + } + } + + private static URI getOcspAiaUrlFromCertificate(X509Certificate certificate) throws TokenValidationException { + final URI uri = getOcspUri(certificate); + if (uri == null) { + throw new UserCertificateOCSPCheckFailedException("Getting the AIA OCSP responder field from the certificate failed"); + } + return uri; + } + +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java new file mode 100644 index 00000000..128b405e --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp.service; + +import java.net.URI; +import java.security.cert.CertStore; +import java.security.cert.TrustAnchor; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; + +public class AiaOcspServiceConfiguration { + + private final Collection nonceDisabledOcspUrls; + private final Set trustedCACertificateAnchors; + private final CertStore trustedCACertificateCertStore; + + public AiaOcspServiceConfiguration(Collection nonceDisabledOcspUrls, Set trustedCACertificateAnchors, CertStore trustedCACertificateCertStore) { + this.nonceDisabledOcspUrls = Objects.requireNonNull(nonceDisabledOcspUrls); + this.trustedCACertificateAnchors = Objects.requireNonNull(trustedCACertificateAnchors); + this.trustedCACertificateCertStore = Objects.requireNonNull(trustedCACertificateCertStore); + } + + public Collection getNonceDisabledOcspUrls() { + return nonceDisabledOcspUrls; + } + + public Set getTrustedCACertificateAnchors() { + return trustedCACertificateAnchors; + } + + public CertStore getTrustedCACertificateCertStore() { + return trustedCACertificateCertStore; + } + +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java new file mode 100644 index 00000000..d7220ffb --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspService.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp.service; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.webeid.security.exceptions.OCSPCertificateException; +import org.webeid.security.exceptions.TokenValidationException; + +import java.net.URI; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Objects; + +import static org.webeid.security.certificate.CertificateValidator.certificateIsValidOnDate; + +/** + * An OCSP service that uses a single designated OCSP responder. + */ +public class DesignatedOcspService implements OcspService { + + private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + private final DesignatedOcspServiceConfiguration configuration; + + public DesignatedOcspService(DesignatedOcspServiceConfiguration configuration) { + this.configuration = Objects.requireNonNull(configuration, "configuration"); + } + + @Override + public boolean doesSupportNonce() { + return configuration.doesSupportNonce(); + } + + @Override + public URI getAccessLocation() { + return configuration.getOcspServiceAccessLocation(); + } + + @Override + public void validateResponderCertificate(X509CertificateHolder cert, Date producedAt) throws TokenValidationException { + try { + final X509Certificate responderCertificate = certificateConverter.getCertificate(cert); + // Certificate pinning is implemented simply by comparing the certificates or their public keys, + // see https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning. + if (!configuration.getResponderCertificate().equals(responderCertificate)) { + throw new OCSPCertificateException("Responder certificate from the OCSP response is not equal to " + + "the configured designated OCSP responder certificate"); + } + certificateIsValidOnDate(responderCertificate, producedAt, "Designated OCSP responder"); + } catch (CertificateException e) { + throw new OCSPCertificateException("X509CertificateHolder conversion to X509Certificate failed"); + } + } + + public boolean supportsIssuerOf(X509Certificate certificate) throws CertificateEncodingException { + return configuration.supportsIssuerOf(certificate); + } + +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java new file mode 100644 index 00000000..029905b6 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp.service; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.webeid.security.exceptions.OCSPCertificateException; + +import java.net.URI; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateHasSigningExtension; + +public class DesignatedOcspServiceConfiguration { + + private final URI ocspServiceAccessLocation; + private final X509Certificate responderCertificate; + private final boolean doesSupportNonce; + private final Collection supportedIssuers; + + /** + * Configuration of a designated OCSP service. + * + * @param ocspServiceAccessLocation the URL where the service is located + * @param responderCertificate the service's OCSP responder certificate + * @param supportedCertificateIssuers the certificate issuers supported by the service + * @param doesSupportNonce true if the service supports the OCSP protocol nonce extension + * @throws OCSPCertificateException when an error occurs while extracting issuer names from certificates + */ + public DesignatedOcspServiceConfiguration(URI ocspServiceAccessLocation, X509Certificate responderCertificate, Collection supportedCertificateIssuers, boolean doesSupportNonce) throws OCSPCertificateException { + this.ocspServiceAccessLocation = Objects.requireNonNull(ocspServiceAccessLocation, "OCSP service access location"); + this.responderCertificate = Objects.requireNonNull(responderCertificate, "OCSP responder certificate"); + this.supportedIssuers = getIssuerX500Names(Objects.requireNonNull(supportedCertificateIssuers, "supported issuers")); + validateHasSigningExtension(responderCertificate); + this.doesSupportNonce = doesSupportNonce; + } + + public URI getOcspServiceAccessLocation() { + return ocspServiceAccessLocation; + } + + public X509Certificate getResponderCertificate() { + return responderCertificate; + } + + public boolean doesSupportNonce() { + return doesSupportNonce; + } + + public boolean supportsIssuerOf(X509Certificate certificate) throws CertificateEncodingException { + return supportedIssuers.contains(new JcaX509CertificateHolder(Objects.requireNonNull(certificate)).getIssuer()); + } + + private Collection getIssuerX500Names(Collection supportedIssuers) throws OCSPCertificateException { + try { + return supportedIssuers.stream() + .map(this::getSubject) + .collect(Collectors.toList()); + } catch (IllegalArgumentException e) { + throw new OCSPCertificateException("Supported issuer list contains an invalid certificate", e.getCause()); + } + } + + private X500Name getSubject(X509Certificate certificate) throws IllegalArgumentException { + try { + return new JcaX509CertificateHolder(certificate).getSubject(); + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java b/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java new file mode 100644 index 00000000..d098b440 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/ocsp/service/OcspService.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.ocsp.service; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.webeid.security.exceptions.TokenValidationException; + +import java.net.URI; +import java.util.Date; + +public interface OcspService { + + boolean doesSupportNonce(); + + URI getAccessLocation(); + + void validateResponderCertificate(X509CertificateHolder cert, Date date) throws TokenValidationException; + +} diff --git a/src/main/java/org/webeid/security/validator/validators/CertificateExpiryValidator.java b/src/main/java/org/webeid/security/validator/validators/CertificateExpiryValidator.java new file mode 100644 index 00000000..1e3d44f0 --- /dev/null +++ b/src/main/java/org/webeid/security/validator/validators/CertificateExpiryValidator.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020-2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.validators; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.webeid.security.exceptions.TokenValidationException; +import org.webeid.security.validator.AuthTokenValidatorData; + +import java.security.cert.TrustAnchor; +import java.util.Date; +import java.util.Set; + +import static org.webeid.security.certificate.CertificateValidator.certificateIsValidOnDate; +import static org.webeid.security.certificate.CertificateValidator.trustedCACertificatesAreValidOnDate; + +public final class CertificateExpiryValidator { + + private static final Logger LOG = LoggerFactory.getLogger(CertificateExpiryValidator.class); + + private final Set trustedCACertificateAnchors; + + public CertificateExpiryValidator(Set trustedCACertificateAnchors) { + this.trustedCACertificateAnchors = trustedCACertificateAnchors; + } + + /** + * Checks the validity of the user certificate from the authentication token + * and the validity of trusted CA certificates. + * + * @param actualTokenData authentication token data that contains the user certificate + * @throws TokenValidationException when a CA certificate or the user certificate is expired or not yet valid + */ + public void validateCertificateExpiry(AuthTokenValidatorData actualTokenData) throws TokenValidationException { + // Use JJWT Clock interface so that the date can be mocked in tests. + final Date now = SubjectCertificatePurposeValidator.DefaultClock.INSTANCE.now(); + trustedCACertificatesAreValidOnDate(trustedCACertificateAnchors, now); + LOG.debug("CA certificates are valid."); + certificateIsValidOnDate(actualTokenData.getSubjectCertificate(), now, "User"); + LOG.debug("User certificate is valid."); + } + +} diff --git a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java index d72e52ba..aeaf21bd 100644 --- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java +++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java @@ -22,89 +22,188 @@ package org.webeid.security.validator.validators; -import okhttp3.OkHttpClient; +import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; -import org.bouncycastle.cert.ocsp.*; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.SingleResp; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.OperatorCreationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.webeid.security.exceptions.TokenValidationException; -import org.webeid.security.exceptions.UserCertificateRevocationCheckFailedException; -import org.webeid.security.exceptions.UserCertificateRevokedException; +import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; import org.webeid.security.validator.AuthTokenValidatorData; +import org.webeid.security.validator.ocsp.Digester; +import org.webeid.security.validator.ocsp.OcspClient; import org.webeid.security.validator.ocsp.OcspRequestBuilder; -import org.webeid.security.validator.ocsp.OcspUtils; +import org.webeid.security.validator.ocsp.OcspServiceProvider; +import org.webeid.security.validator.ocsp.service.OcspService; import java.io.IOException; -import java.net.URI; +import java.math.BigInteger; +import java.security.Security; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.Collection; +import java.util.Date; import java.util.Objects; +import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateCertificateStatusUpdateTime; +import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateResponseSignature; +import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateSubjectCertificateStatus; + public final class SubjectCertificateNotRevokedValidator { private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateNotRevokedValidator.class); + private static final DigestCalculator DIGEST_CALCULATOR = Digester.sha1(); private final SubjectCertificateTrustedValidator trustValidator; - private final OkHttpClient httpClient; - private final Collection nonceDisabledOcspUrls; + private final OcspClient ocspClient; + private final OcspServiceProvider ocspServiceProvider; + + static { + Security.addProvider(new BouncyCastleProvider()); + } - public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator, OkHttpClient httpClient, Collection nonceDisabledOcspUrls) { + public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator, + OcspClient ocspClient, + OcspServiceProvider ocspServiceProvider) { this.trustValidator = trustValidator; - this.httpClient = httpClient; - this.nonceDisabledOcspUrls = nonceDisabledOcspUrls; + this.ocspClient = ocspClient; + this.ocspServiceProvider = ocspServiceProvider; } /** * Validates that the user certificate from the authentication token is not revoked with OCSP. * * @param actualTokenData authentication token data that contains the user certificate. - * @throws TokenValidationException when user certificate is revoked. + * @throws TokenValidationException when user certificate is revoked or revocation check fails. */ public void validateCertificateNotRevoked(AuthTokenValidatorData actualTokenData) throws TokenValidationException { try { final X509Certificate certificate = actualTokenData.getSubjectCertificate(); - final URI uri = OcspUtils.ocspUri(certificate); + OcspService ocspService = ocspServiceProvider.getService(certificate); - if (uri == null) { - throw new UserCertificateRevocationCheckFailedException("The CA/certificate doesn't have an OCSP responder"); - } - final boolean ocspNonceDisabled = nonceDisabledOcspUrls.contains(uri); - if (ocspNonceDisabled) { + if (!ocspService.doesSupportNonce()) { LOG.debug("Disabling OCSP nonce extension"); } + final CertificateID certificateId = getCertificateId(certificate, + Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerCertificate())); + final OCSPReq request = new OcspRequestBuilder() - .certificate(certificate) - .enableOcspNonce(!ocspNonceDisabled) - .issuer(Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerCertificate())) + .withCertificateId(certificateId) + .enableOcspNonce(ocspService.doesSupportNonce()) .build(); LOG.debug("Sending OCSP request"); - final OCSPResp response = OcspUtils.request(uri, request, httpClient); + final OCSPResp response = Objects.requireNonNull(ocspClient.request(ocspService.getAccessLocation(), request)); if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) { - throw new UserCertificateRevocationCheckFailedException("Response status: " + response.getStatus()); + throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus())); } final BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject(); - final SingleResp first = basicResponse.getResponses()[0]; - - final CertificateStatus status = first.getCertStatus(); - - if (status instanceof RevokedStatus) { - RevokedStatus revokedStatus = (RevokedStatus) status; - throw (revokedStatus.hasRevocationReason() ? - new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason()) : - new UserCertificateRevokedException()); - } else if (status instanceof UnknownStatus) { - throw new UserCertificateRevokedException("Unknown status"); - } else if (status == null) { - LOG.debug("OCSP check result is GOOD"); - } else { - throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown"); + verifyOcspResponse(basicResponse, ocspService, certificateId); + if (ocspService.doesSupportNonce()) { + checkNonce(request, basicResponse); } - } catch (CertificateEncodingException | OCSPException | IOException e) { - throw new UserCertificateRevocationCheckFailedException(e); + } catch (OCSPException | CertificateException | OperatorCreationException | IOException e) { + throw new UserCertificateOCSPCheckFailedException(e); } } + + private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId) throws TokenValidationException, OCSPException, CertificateException, OperatorCreationException { + // The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt. + // + // 3.2. Signed Response Acceptance Requirements + // Prior to accepting a signed response for a particular certificate as + // valid, OCSP clients SHALL confirm that: + // + // 1. The certificate identified in a received response corresponds to + // the certificate that was identified in the corresponding request. + + // As we sent the request for only a single certificate, we expect only a single response. + if (basicResponse.getResponses().length != 1) { + throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one response, " + + "received " + basicResponse.getResponses().length + " responses instead"); + } + final SingleResp certStatusResponse = basicResponse.getResponses()[0]; + if (!requestCertificateId.equals(certStatusResponse.getCertID())) { + throw new UserCertificateOCSPCheckFailedException("OCSP responded with certificate ID that differs from the requested ID"); + } + + // 2. The signature on the response is valid. + + // We assume that the responder includes its certificate in the certs field of the response + // that helps us to verify it. According to RFC 2560 this field is optional, but including it + // is standard practice. + if (basicResponse.getCerts().length != 1) { + throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one responder certificate, " + + "received " + basicResponse.getCerts().length + " certificates instead"); + } + final X509CertificateHolder responderCert = basicResponse.getCerts()[0]; + validateResponseSignature(basicResponse, responderCert); + + // 3. The identity of the signer matches the intended recipient of the + // request. + // + // 4. The signer is currently authorized to provide a response for the + // certificate in question. + + final Date producedAt = basicResponse.getProducedAt(); + ocspService.validateResponderCertificate(responderCert, producedAt); + + // 5. The time at which the status being indicated is known to be + // correct (thisUpdate) is sufficiently recent. + // + // 6. When available, the time at or before which newer information will + // be available about the status of the certificate (nextUpdate) is + // greater than the current time. + + validateCertificateStatusUpdateTime(certStatusResponse, producedAt); + + // Now we can accept the signed response as valid and validate the certificate status. + validateSubjectCertificateStatus(certStatusResponse); + LOG.debug("OCSP check result is GOOD"); + } + + private static void checkNonce(OCSPReq request, BasicOCSPResp response) throws UserCertificateOCSPCheckFailedException { + final Extension requestNonce = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce); + final Extension responseNonce = response.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce); + if (!requestNonce.equals(responseNonce)) { + throw new UserCertificateOCSPCheckFailedException("OCSP request and response nonces differ, " + + "possible replay attack"); + } + } + + private static CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws CertificateEncodingException, IOException, OCSPException { + final BigInteger serial = subjectCertificate.getSerialNumber(); + return new CertificateID(DIGEST_CALCULATOR, + new X509CertificateHolder(issuerCertificate.getEncoded()), serial); + } + + private static String ocspStatusToString(int status) { + switch (status) { + case OCSPResp.MALFORMED_REQUEST: + return "malformed request"; + case OCSPResp.INTERNAL_ERROR: + return "internal error"; + case OCSPResp.TRY_LATER: + return "service unavailable"; + case OCSPResp.SIG_REQUIRED: + return "request signature missing"; + case OCSPResp.UNAUTHORIZED: + return "unauthorized"; + default: + return "unknown"; + } + } + } diff --git a/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePolicyValidator.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePolicyValidator.java index 6f9c0334..fd0c78fa 100644 --- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePolicyValidator.java +++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePolicyValidator.java @@ -7,7 +7,7 @@ import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; import org.webeid.security.exceptions.TokenValidationException; import org.webeid.security.exceptions.UserCertificateDisallowedPolicyException; -import org.webeid.security.exceptions.UserCertificateInvalidPolicyException; +import org.webeid.security.exceptions.UserCertificateParseException; import org.webeid.security.validator.AuthTokenValidatorData; import java.io.IOException; @@ -16,7 +16,7 @@ import java.util.Collection; import java.util.Optional; -public class SubjectCertificatePolicyValidator { +public final class SubjectCertificatePolicyValidator { private final Collection disallowedSubjectCertificatePolicies; @@ -29,7 +29,7 @@ public SubjectCertificatePolicyValidator(Collection disall * * @param actualTokenData authentication token data that contains the user certificate. * @throws UserCertificateDisallowedPolicyException when user certificate policy does not match the configured policies. - * @throws UserCertificateInvalidPolicyException when user certificate policy is invalid. + * @throws UserCertificateParseException when user certificate policy is invalid. */ public void validateCertificatePolicies(AuthTokenValidatorData actualTokenData) throws TokenValidationException { final X509Certificate certificate = actualTokenData.getSubjectCertificate(); @@ -46,7 +46,7 @@ public void validateCertificatePolicies(AuthTokenValidatorData actualTokenData) throw new UserCertificateDisallowedPolicyException(); } } catch (IOException e) { - throw new UserCertificateInvalidPolicyException(); + throw new UserCertificateParseException(e); } } } diff --git a/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePurposeValidator.java similarity index 69% rename from src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java rename to src/main/java/org/webeid/security/validator/validators/SubjectCertificatePurposeValidator.java index 03e5a81e..bbf3ea0c 100644 --- a/src/main/java/org/webeid/security/validator/validators/FunctionalSubjectCertificateValidators.java +++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificatePurposeValidator.java @@ -25,38 +25,21 @@ import io.jsonwebtoken.Clock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.webeid.security.exceptions.*; +import org.webeid.security.exceptions.TokenValidationException; +import org.webeid.security.exceptions.UserCertificateMissingPurposeException; +import org.webeid.security.exceptions.UserCertificateParseException; +import org.webeid.security.exceptions.UserCertificateWrongPurposeException; import org.webeid.security.validator.AuthTokenValidatorData; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; import java.security.cert.CertificateParsingException; import java.util.Date; import java.util.List; -public final class FunctionalSubjectCertificateValidators { +public final class SubjectCertificatePurposeValidator { - private static final Logger LOG = LoggerFactory.getLogger(FunctionalSubjectCertificateValidators.class); + private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificatePurposeValidator.class); private static final String EXTENDED_KEY_USAGE_CLIENT_AUTHENTICATION = "1.3.6.1.5.5.7.3.2"; - /** - * Checks the validity of the user certificate from the authentication token. - * - * @param actualTokenData authentication token data that contains the user certificate - * @throws TokenValidationException when the user certificate is expired or not yet valid - */ - public static void validateCertificateExpiry(AuthTokenValidatorData actualTokenData) throws TokenValidationException { - try { - // Use JJWT Clock interface so that the date can be mocked in tests. - actualTokenData.getSubjectCertificate().checkValidity(DefaultClock.INSTANCE.now()); - LOG.debug("User certificate is valid."); - } catch (CertificateNotYetValidException e) { - throw new UserCertificateNotYetValidException(e); - } catch (CertificateExpiredException e) { - throw new UserCertificateExpiredException(e); - } - } - /** * Validates that the purpose of the user certificate from the authentication token contains client authentication. * @@ -78,7 +61,7 @@ public static void validateCertificatePurpose(AuthTokenValidatorData actualToken } } - private FunctionalSubjectCertificateValidators() { + private SubjectCertificatePurposeValidator() { throw new IllegalStateException("Functional class"); } diff --git a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java index b6497f52..d2993258 100644 --- a/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java +++ b/src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java @@ -24,15 +24,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.webeid.security.exceptions.JceException; -import org.webeid.security.exceptions.UserCertificateNotTrustedException; +import org.webeid.security.exceptions.CertificateNotTrustedException; +import org.webeid.security.exceptions.TokenValidationException; import org.webeid.security.validator.AuthTokenValidatorData; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.*; +import java.security.cert.CertStore; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; import java.util.Set; +import static org.webeid.security.certificate.CertificateValidator.validateIsSignedByTrustedCA; + public final class SubjectCertificateTrustedValidator { private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateTrustedValidator.class); @@ -50,32 +52,12 @@ public SubjectCertificateTrustedValidator(Set trustedCACertificateA * Validates that the user certificate from the authentication token is signed by a trusted certificate authority. * * @param actualTokenData authentication token data that contains the user certificate. - * @throws UserCertificateNotTrustedException when user certificate is not signed by a trusted CA or is valid after CA certificate. + * @throws CertificateNotTrustedException when user certificate is not signed by a trusted CA. */ - public void validateCertificateTrusted(AuthTokenValidatorData actualTokenData) throws UserCertificateNotTrustedException, JceException { - + public void validateCertificateTrusted(AuthTokenValidatorData actualTokenData) throws TokenValidationException { final X509Certificate certificate = actualTokenData.getSubjectCertificate(); - - final X509CertSelector selector = new X509CertSelector(); - selector.setCertificate(certificate); - - try { - final PKIXBuilderParameters pkixBuilderParameters = new PKIXBuilderParameters(trustedCACertificateAnchors, selector); - pkixBuilderParameters.setRevocationEnabled(false); - pkixBuilderParameters.addCertStore(trustedCACertificateCertStore); - - // See the comment in AuthTokenValidatorImpl constructor why we use the default JCE provider. - final CertPathBuilder certPathBuilder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType()); - final PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) certPathBuilder.build(pkixBuilderParameters); - - subjectCertificateIssuerCertificate = result.getTrustAnchor().getTrustedCert(); - - } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { - throw new JceException(e); - } catch (CertPathBuilderException e) { - LOG.trace("Error verifying signer's certificate {}: {}", certificate.getSubjectDN(), e); - throw new UserCertificateNotTrustedException(); - } + subjectCertificateIssuerCertificate = validateIsSignedByTrustedCA(certificate, trustedCACertificateAnchors, trustedCACertificateCertStore); + LOG.debug("Subject certificate is signed by a trusted CA"); } public X509Certificate getSubjectCertificateIssuerCertificate() { diff --git a/src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java b/src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java index 3f1f06e2..b3a68d7a 100644 --- a/src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java +++ b/src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java @@ -26,6 +26,7 @@ import org.webeid.security.exceptions.JceException; import org.webeid.security.validator.AuthTokenValidator; +import java.io.IOException; import java.security.cert.CertificateException; import static org.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidator; @@ -40,7 +41,7 @@ public void setup() { super.setup(); try { validator = getAuthTokenValidator(cache); - } catch (CertificateException | JceException e) { + } catch (CertificateException | JceException | IOException e) { throw new RuntimeException(e); } } diff --git a/src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java b/src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java index d83ad223..e84fb801 100644 --- a/src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java +++ b/src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java @@ -26,6 +26,7 @@ import org.webeid.security.exceptions.JceException; import org.webeid.security.validator.AuthTokenValidator; +import java.io.IOException; import java.security.cert.CertificateException; import static org.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidator; @@ -40,7 +41,7 @@ protected void setup() { super.setup(); try { validator = getAuthTokenValidator(cache); - } catch (CertificateException | JceException e) { + } catch (CertificateException | JceException | IOException e) { throw new RuntimeException(e); } } diff --git a/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java b/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java index 3439c5ba..93046e8c 100644 --- a/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java +++ b/src/test/java/org/webeid/security/testutil/AuthTokenValidators.java @@ -23,27 +23,36 @@ package org.webeid.security.testutil; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.webeid.security.certificate.CertificateLoader; import org.webeid.security.exceptions.JceException; +import org.webeid.security.exceptions.OCSPCertificateException; +import org.webeid.security.validator.AuthTokenValidationConfiguration; import org.webeid.security.validator.AuthTokenValidator; import org.webeid.security.validator.AuthTokenValidatorBuilder; +import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; import javax.cache.Cache; +import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.ZonedDateTime; +import static org.webeid.security.testutil.Certificates.getTestEsteid2018CA; +import static org.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceConfiguration; + public final class AuthTokenValidators { private static final String TOKEN_ORIGIN_URL = "https://ria.ee"; private static final ASN1ObjectIdentifier EST_IDEMIA_POLICY = new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.2.1"); - public static AuthTokenValidator getAuthTokenValidator(Cache cache) throws CertificateException, JceException { + public static AuthTokenValidator getAuthTokenValidator(Cache cache) throws CertificateException, JceException, IOException { return getAuthTokenValidator(TOKEN_ORIGIN_URL, cache); } - public static AuthTokenValidator getAuthTokenValidator(String url, Cache cache) throws CertificateException, JceException { + public static AuthTokenValidator getAuthTokenValidator(String url, Cache cache) throws CertificateException, JceException, IOException { return getAuthTokenValidator(url, cache, getCACertificates()); } @@ -57,25 +66,33 @@ public static AuthTokenValidator getAuthTokenValidator(String url, Cache cache, String certFingerprint) throws CertificateException, JceException { + public static AuthTokenValidator getAuthTokenValidator(Cache cache, String certFingerprint) throws CertificateException, JceException, IOException { return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates()) .withSiteCertificateSha256Fingerprint(certFingerprint) .withoutUserCertificateRevocationCheckWithOcsp() .build(); } - public static AuthTokenValidator getAuthTokenValidatorWithOcspCheck(Cache cache) throws CertificateException, JceException { - return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates()).build(); + public static AuthTokenValidator getAuthTokenValidatorWithOcspCheck(Cache cache) throws CertificateException, JceException, URISyntaxException, IOException { + return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates()) + .build(); + } + + public static AuthTokenValidator getAuthTokenValidatorWithDesignatedOcspCheck(Cache cache) throws CertificateException, JceException, URISyntaxException, IOException, OCSPCertificateException { + return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates()) + .withDesignatedOcspServiceConfiguration(getDesignatedOcspServiceConfiguration()) + .build(); } - public static AuthTokenValidator getAuthTokenValidatorWithWrongTrustedCA(Cache cache) throws CertificateException, JceException { + public static AuthTokenValidator getAuthTokenValidatorWithWrongTrustedCA(Cache cache) throws CertificateException, JceException, IOException { return getAuthTokenValidator(TOKEN_ORIGIN_URL, cache, CertificateLoader.loadCertificatesFromResources("ESTEID2018.cer")); } - public static AuthTokenValidator getAuthTokenValidatorWithDisallowedESTEIDPolicy(Cache cache) throws CertificateException, JceException { + public static AuthTokenValidator getAuthTokenValidatorWithDisallowedESTEIDPolicy(Cache cache) throws CertificateException, JceException, IOException { return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, cache, getCACertificates()) .withDisallowedCertificatePolicies(EST_IDEMIA_POLICY) + .withoutUserCertificateRevocationCheckWithOcsp() .build(); } @@ -86,7 +103,7 @@ private static AuthTokenValidatorBuilder getAuthTokenValidatorBuilder(String uri .withTrustedCertificateAuthorities(certificates); } - private static X509Certificate[] getCACertificates() throws CertificateException { + private static X509Certificate[] getCACertificates() throws CertificateException, IOException { return CertificateLoader.loadCertificatesFromResources("TEST_of_ESTEID2018.cer"); } diff --git a/src/test/java/org/webeid/security/testutil/Certificates.java b/src/test/java/org/webeid/security/testutil/Certificates.java new file mode 100644 index 00000000..1e5cbe48 --- /dev/null +++ b/src/test/java/org/webeid/security/testutil/Certificates.java @@ -0,0 +1,64 @@ +package org.webeid.security.testutil; + +import org.webeid.security.certificate.CertificateLoader; + +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import static org.webeid.security.certificate.CertificateLoader.loadCertificatesFromResources; + +public class Certificates { + + private static final String JAAK_KRISTJAN_ESTEID2018_CERT = "MIIEAzCCA2WgAwIBAgIQOWkBWXNDJm1byFd3XsWkvjAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE4MTAxODA5NTA0N1oXDTIzMTAxNzIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR5k1lXzvSeI9O/1s1pZvjhEW8nItJoG0EBFxmLEY6S7ki1vF2Q3TEDx6dNztI1Xtx96cs8r4zYTwdiQoDg7k3diUuR9nTWGxQEMO1FDo4Y9fAmiPGWT++GuOVoZQY3XxijggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFOQsvTQJEBVMMSmhyZX5bibYJubAMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgH1UsmMdtLZti51Fq2QR4wUkAwpsnhsBV2HQqUXFYBJ7EXnLCkaXjdZKkHpABfM0QEx7UUhaI4i53jiJ7E1Y7WOAAJBDX4z61pniHJapI1bkMIiJQ/ti7ha8fdJSMSpAds5CyHIyHkQzWlVy86f9mA7Eu3oRO/1q+eFUzDbNN3Vvy7gQWQ="; + private static final String MARILIIS_ESTEID2015_CERT = "MIIFwjCCA6qgAwIBAgIQY+LgQ6n0BURZ048wIEiYHjANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2YgRVNURUlELVNLIDIwMTUwHhcNMTcxMDAzMTMyMjU2WhcNMjIxMDAyMjA1OTU5WjCBnjELMAkGA1UEBhMCRUUxDzANBgNVBAoMBkVTVEVJRDEaMBgGA1UECwwRZGlnaXRhbCBzaWduYXR1cmUxJjAkBgNVBAMMHU3DhE5OSUssTUFSSS1MSUlTLDYxNzEwMDMwMTYzMRAwDgYDVQQEDAdNw4ROTklLMRIwEAYDVQQqDAlNQVJJLUxJSVMxFDASBgNVBAUTCzYxNzEwMDMwMTYzMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+nNdtmZ2Ve3XXtjBEGwpvVrDIg7slPfLlyHbCBFMXevfqW5KsXIOy6E2A+Yof+/cqRlY4IhsX2Ka9SsJSo8/EekasFasLFPw9ZBE3MG0nn5zaatg45VSjnPinMmrzFzxo4IB2jCCAdYwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwgYsGA1UdIASBgzCBgDBzBgkrBgEEAc4fAwEwZjAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvcmVwb3NpdG9vcml1bS9DUFMwMwYIKwYBBQUHAgIwJwwlQWludWx0IHRlc3RpbWlzZWtzLiBPbmx5IGZvciB0ZXN0aW5nLjAJBgcEAIvsQAECMB0GA1UdDgQWBBTiw6M0uow+u6sfhgJAWCSvtkB/ejAiBggrBgEFBQcBAwQWMBQwCAYGBACORgEBMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBRJwPJEOWXVm0Y7DThgg7HWLSiGpjCBgwYIKwYBBQUHAQEEdzB1MCwGCCsGAQUFBzABhiBodHRwOi8vYWlhLmRlbW8uc2suZWUvZXN0ZWlkMjAxNTBFBggrBgEFBQcwAoY5aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FU1RFSUQtU0tfMjAxNS5kZXIuY3J0MEEGA1UdHwQ6MDgwNqA0oDKGMGh0dHA6Ly93d3cuc2suZWUvY3Jscy9lc3RlaWQvdGVzdF9lc3RlaWQyMDE1LmNybDANBgkqhkiG9w0BAQsFAAOCAgEAEWBdwmzo/yRncJXKvrE+A1G6yQaBNarKectI5uk18BewYEA4QkhmIwOCwD83jBDB9JF+kuODMHsnvz2mfhwaB/uJIPwfBDQ5JCMBdHPsxLN9nzW/UUzqv2UDMwFkibHCcfV5lTBcmOd7FagUHTUm+8gRlWbDiVl5yPochdJgGYPV+fs/jc5ttHaBvBon0z9LbI4qi0VXdRmV0iogErh8JF5yfGkbfGRaMkWkNYQtQ68i/hPe6MaUxL2/MMt4YTyXtVghmc3ZKZIyp4j0+jlK4vL+d4gaE+TvoQvh6HrmP145FqlMDurATWdB069+hdDLO5fI6AYkc79D5XPKwQ/f1MBufLtBYtOJmtpLT+tdBt/EqOEIO/0FeHcXZlFioNMuxBBeTE/QcDtJ2jxTcg8jNOoepS0wjuxBon9iI1710SR53DLGSWdL52lPoBFacnyPQI1htXVUkJ8icMQKYe3BLt1Ha2cvsA4n4IpjqVROX4mzoPL1hg/aJlD+W2uI2ppYRUNY5FX7C0R+AYzMpOahQ7STQfUxtEnKW98e1I33LWwpjJW9q4htsZeXs4Zatf9ssfUW0VA49tnI28kkN2D8aw1NgWfzVlnJKkEj0qa3ewLZK577j8MexAetT/7leH6mqewr9ewC/tKbYjhufieXx6RPcRC4OZsxtii7ih8TqRg="; + + private static X509Certificate testEsteid2018CA; + private static X509Certificate testEsteid2015CA; + + private static X509Certificate jaakKristjanEsteid2018Cert; + private static X509Certificate mariliisEsteid2015Cert; + private static X509Certificate testSkOcspResponder2020; + + static void loadCertificates() throws CertificateException, IOException { + X509Certificate[] certificates = loadCertificatesFromResources("TEST_of_ESTEID-SK_2015.cer", "TEST_of_ESTEID2018.cer", "TEST_of_SK_OCSP_RESPONDER_2020.cer"); + testEsteid2015CA = certificates[0]; + testEsteid2018CA = certificates[1]; + testSkOcspResponder2020 = certificates[2]; + } + + public static X509Certificate getTestEsteid2018CA() throws CertificateException, IOException { + if (testEsteid2018CA == null) { + loadCertificates(); + } + return testEsteid2018CA; + } + + public static X509Certificate getTestEsteid2015CA() throws CertificateException, IOException { + if (testEsteid2015CA == null) { + loadCertificates(); + } + return testEsteid2015CA; + } + + public static X509Certificate getTestSkOcspResponder2020() throws CertificateException, IOException { + if (testSkOcspResponder2020 == null) { + loadCertificates(); + } + return testSkOcspResponder2020; + } + + public static X509Certificate getJaakKristjanEsteid2018Cert() throws CertificateException, IOException { + if (jaakKristjanEsteid2018Cert == null) { + jaakKristjanEsteid2018Cert = CertificateLoader.loadCertificateFromBase64String(JAAK_KRISTJAN_ESTEID2018_CERT); + } + return jaakKristjanEsteid2018Cert; + } + + public static X509Certificate getMariliisEsteid2015Cert() throws CertificateException, IOException { + if (mariliisEsteid2015Cert == null) { + mariliisEsteid2015Cert = CertificateLoader.loadCertificateFromBase64String(MARILIIS_ESTEID2015_CERT); + } + return mariliisEsteid2015Cert; + } +} diff --git a/src/test/java/org/webeid/security/testutil/Dates.java b/src/test/java/org/webeid/security/testutil/Dates.java index 3dc5a378..ed57570c 100644 --- a/src/test/java/org/webeid/security/testutil/Dates.java +++ b/src/test/java/org/webeid/security/testutil/Dates.java @@ -25,7 +25,7 @@ import com.fasterxml.jackson.databind.util.StdDateFormat; import io.jsonwebtoken.Clock; import io.jsonwebtoken.impl.DefaultClock; -import org.webeid.security.validator.validators.FunctionalSubjectCertificateValidators; +import org.webeid.security.validator.validators.SubjectCertificatePurposeValidator; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -44,11 +44,11 @@ public static void setMockedDefaultJwtParserDate(Date mockedDate) throws NoSuchF } public static void setMockedFunctionalSubjectCertificateValidatorsDate(Date mockedDate) throws NoSuchFieldException, IllegalAccessException { - setClockField(FunctionalSubjectCertificateValidators.DefaultClock.class, mockedDate); + setClockField(SubjectCertificatePurposeValidator.DefaultClock.class, mockedDate); } public static void resetMockedFunctionalSubjectCertificateValidatorsDate() throws NoSuchFieldException, IllegalAccessException { - setClockField(FunctionalSubjectCertificateValidators.DefaultClock.class, new Date()); + setClockField(SubjectCertificatePurposeValidator.DefaultClock.class, new Date()); } private static void setClockField(Class cls, Date date) throws NoSuchFieldException, IllegalAccessException { diff --git a/src/test/java/org/webeid/security/testutil/OcspServiceMaker.java b/src/test/java/org/webeid/security/testutil/OcspServiceMaker.java new file mode 100644 index 00000000..eefdbaf1 --- /dev/null +++ b/src/test/java/org/webeid/security/testutil/OcspServiceMaker.java @@ -0,0 +1,80 @@ +package org.webeid.security.testutil; + +import com.google.common.collect.Sets; +import org.jetbrains.annotations.NotNull; +import org.webeid.security.exceptions.JceException; +import org.webeid.security.exceptions.OCSPCertificateException; +import org.webeid.security.validator.ocsp.OcspServiceProvider; +import org.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; +import org.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; + +import java.io.IOException; +import java.net.URI; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; + +import static org.webeid.security.certificate.CertificateValidator.buildCertStoreFromCertificates; +import static org.webeid.security.certificate.CertificateValidator.buildTrustAnchorsFromCertificates; +import static org.webeid.security.testutil.Certificates.*; +import static org.webeid.security.validator.ocsp.OcspUrl.AIA_ESTEID_2015; + +public class OcspServiceMaker { + + private static final String TEST_OCSP_ACCESS_LOCATION = "http://demo.sk.ee/ocsp"; + private static final List TRUSTED_CA_CERTIFICATES; + private static final URI TEST_ESTEID_2015 = URI.create("http://aia.demo.sk.ee/esteid2015"); + + static { + try { + TRUSTED_CA_CERTIFICATES = Arrays.asList(getTestEsteid2018CA(), getTestEsteid2015CA()); + } catch (CertificateException | IOException e) { + throw new ExceptionInInitializerError(e); + } + } + + @NotNull + public static OcspServiceProvider getAiaOcspServiceProvider() throws JceException { + return new OcspServiceProvider(null, getAiaOcspServiceConfiguration()); + } + + @NotNull + public static OcspServiceProvider getDesignatedOcspServiceProvider() throws CertificateException, IOException, OCSPCertificateException, JceException { + return new OcspServiceProvider(getDesignatedOcspServiceConfiguration(), getAiaOcspServiceConfiguration()); + } + + @NotNull + public static OcspServiceProvider getDesignatedOcspServiceProvider(boolean doesSupportNonce) throws CertificateException, IOException, JceException, OCSPCertificateException { + return new OcspServiceProvider(getDesignatedOcspServiceConfiguration(doesSupportNonce), getAiaOcspServiceConfiguration()); + } + + @NotNull + public static OcspServiceProvider getDesignatedOcspServiceProvider(String ocspServiceAccessLocation) throws CertificateException, IOException, OCSPCertificateException, JceException { + return new OcspServiceProvider(getDesignatedOcspServiceConfiguration(true, ocspServiceAccessLocation), getAiaOcspServiceConfiguration()); + } + + private static AiaOcspServiceConfiguration getAiaOcspServiceConfiguration() throws JceException { + return new AiaOcspServiceConfiguration( + Sets.newHashSet(AIA_ESTEID_2015, TEST_ESTEID_2015), + buildTrustAnchorsFromCertificates(TRUSTED_CA_CERTIFICATES), + buildCertStoreFromCertificates(TRUSTED_CA_CERTIFICATES)); + } + + public static DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() throws CertificateException, IOException, OCSPCertificateException { + return getDesignatedOcspServiceConfiguration(true, TEST_OCSP_ACCESS_LOCATION); + } + + private static DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration(boolean doesSupportNonce) throws CertificateException, IOException, OCSPCertificateException { + return getDesignatedOcspServiceConfiguration(doesSupportNonce, TEST_OCSP_ACCESS_LOCATION); + } + + private static DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration(boolean doesSupportNonce, String ocspServiceAccessLocation) throws CertificateException, IOException, OCSPCertificateException { + return new DesignatedOcspServiceConfiguration( + URI.create(ocspServiceAccessLocation), + getTestSkOcspResponder2020(), + TRUSTED_CA_CERTIFICATES, + doesSupportNonce); + } + +} diff --git a/src/test/java/org/webeid/security/validator/OcspTest.java b/src/test/java/org/webeid/security/validator/OcspTest.java index 41e1170c..86ef7803 100644 --- a/src/test/java/org/webeid/security/validator/OcspTest.java +++ b/src/test/java/org/webeid/security/validator/OcspTest.java @@ -24,14 +24,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.webeid.security.exceptions.*; +import org.webeid.security.exceptions.JceException; +import org.webeid.security.exceptions.TokenValidationException; +import org.webeid.security.exceptions.CertificateExpiredException; +import org.webeid.security.exceptions.UserCertificateRevokedException; import org.webeid.security.testutil.AbstractTestWithMockedDateAndCorrectNonce; import org.webeid.security.testutil.Tokens; +import java.io.IOException; +import java.net.URISyntaxException; import java.security.cert.CertificateException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidatorWithDesignatedOcspCheck; import static org.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidatorWithOcspCheck; class OcspTest extends AbstractTestWithMockedDateAndCorrectNonce { @@ -44,36 +50,42 @@ protected void setup() { super.setup(); try { validator = getAuthTokenValidatorWithOcspCheck(cache); - } catch (CertificateException | JceException e) { + } catch (CertificateException | JceException | URISyntaxException | IOException e) { throw new RuntimeException(e); } } @Test void detectRevokedUserCertificate() { - // This test had flaky results as OCSP requests sometimes failed, sometimes passed. - // Hence the catch which may or may not execute instead of assertThatThrownBy(). try { validator.validate(Tokens.SIGNED); } catch (TokenValidationException e) { - assertThat(e).isInstanceOfAny( - UserCertificateRevokedException.class, - UserCertificateRevocationCheckFailedException.class - ); + assertThat(e).isInstanceOf(UserCertificateRevokedException.class); + } + } + + @Test + void detectRevokedUserCertificateWithDesignatedOcspService() throws Exception { + final AuthTokenValidator validatorWithDesignatedOcspCheck = getAuthTokenValidatorWithDesignatedOcspCheck(cache); + try { + validatorWithDesignatedOcspCheck.validate(Tokens.SIGNED); + } catch (TokenValidationException e) { + assertThat(e).isInstanceOf(UserCertificateRevokedException.class); } } @Test void testTokenCertRsaExpired() { assertThatThrownBy(() -> validator.validate(Tokens.TOKEN_CERT_RSA_EXIPRED)) - .isInstanceOf(UserCertificateExpiredException.class) - .hasMessageStartingWith("User certificate has expired:"); + .isInstanceOf(CertificateExpiredException.class) + .hasMessageStartingWith("User certificate has expired"); } @Test void testTokenCertEcdsaExpired() { assertThatThrownBy(() -> validator.validate(Tokens.TOKEN_CERT_ECDSA_EXIPRED)) - .isInstanceOf(UserCertificateExpiredException.class) - .hasMessageStartingWith("User certificate has expired:"); + .isInstanceOf(CertificateExpiredException.class) + .hasMessageStartingWith("User certificate has expired"); } + } diff --git a/src/test/java/org/webeid/security/validator/ParseTest.java b/src/test/java/org/webeid/security/validator/ParseTest.java index 31a154e2..4d8efcf2 100644 --- a/src/test/java/org/webeid/security/validator/ParseTest.java +++ b/src/test/java/org/webeid/security/validator/ParseTest.java @@ -27,7 +27,7 @@ import org.webeid.security.exceptions.TokenSignatureValidationException; import org.webeid.security.testutil.AbstractTestWithMockedDateValidatorAndCorrectNonce; import org.webeid.security.testutil.Tokens; -import org.webeid.security.util.CertUtil; +import org.webeid.security.certificate.CertificateData; import java.security.cert.X509Certificate; @@ -48,15 +48,15 @@ class ParseTest extends AbstractTestWithMockedDateValidatorAndCorrectNonce { @Test void parseSignedToken() throws Exception { final X509Certificate result = validator.validate(Tokens.SIGNED); - assertThat(CertUtil.getSubjectCN(result)) + assertThat(CertificateData.getSubjectCN(result)) .isEqualTo("JÕEORG\\,JAAK-KRISTJAN\\,38001085718"); - assertThat(toTitleCase(CertUtil.getSubjectGivenName(result))) + assertThat(toTitleCase(CertificateData.getSubjectGivenName(result))) .isEqualTo("Jaak-Kristjan"); - assertThat(toTitleCase(CertUtil.getSubjectSurname(result))) + assertThat(toTitleCase(CertificateData.getSubjectSurname(result))) .isEqualTo("Jõeorg"); - assertThat(CertUtil.getSubjectIdCode(result)) + assertThat(CertificateData.getSubjectIdCode(result)) .isEqualTo("PNOEE-38001085718"); - assertThat(CertUtil.getSubjectCountryCode(result)) + assertThat(CertificateData.getSubjectCountryCode(result)) .isEqualTo("EE"); } diff --git a/src/test/java/org/webeid/security/validator/SubjectCertificatePolicyValidatorTest.java b/src/test/java/org/webeid/security/validator/SubjectCertificatePolicyValidatorTest.java index 0dbea857..3172a512 100644 --- a/src/test/java/org/webeid/security/validator/SubjectCertificatePolicyValidatorTest.java +++ b/src/test/java/org/webeid/security/validator/SubjectCertificatePolicyValidatorTest.java @@ -7,6 +7,7 @@ import org.webeid.security.testutil.AbstractTestWithMockedDateAndCorrectNonce; import org.webeid.security.testutil.Tokens; +import java.io.IOException; import java.security.cert.CertificateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -20,7 +21,7 @@ class SubjectCertificatePolicyValidatorTest extends AbstractTestWithMockedDateAn void setUp() { try { validator = getAuthTokenValidatorWithDisallowedESTEIDPolicy(cache); - } catch (CertificateException | JceException e) { + } catch (CertificateException | JceException | IOException e) { throw new RuntimeException(e); } } diff --git a/src/test/java/org/webeid/security/validator/TrustedCaTest.java b/src/test/java/org/webeid/security/validator/TrustedCaTest.java index fe0b312b..eac5037f 100644 --- a/src/test/java/org/webeid/security/validator/TrustedCaTest.java +++ b/src/test/java/org/webeid/security/validator/TrustedCaTest.java @@ -25,10 +25,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.webeid.security.exceptions.JceException; -import org.webeid.security.exceptions.UserCertificateNotTrustedException; +import org.webeid.security.exceptions.CertificateNotTrustedException; import org.webeid.security.testutil.AbstractTestWithMockedDateAndCorrectNonce; import org.webeid.security.testutil.Tokens; +import java.io.IOException; import java.security.cert.CertificateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -44,7 +45,7 @@ protected void setup() { super.setup(); try { validator = getAuthTokenValidatorWithWrongTrustedCA(cache); - } catch (CertificateException | JceException e) { + } catch (CertificateException | JceException | IOException e) { throw new RuntimeException(e); } } @@ -52,7 +53,7 @@ protected void setup() { @Test void detectUntrustedUserCertificate() { assertThatThrownBy(() -> validator.validate(Tokens.SIGNED)) - .isInstanceOf(UserCertificateNotTrustedException.class); + .isInstanceOf(CertificateNotTrustedException.class); } } diff --git a/src/test/java/org/webeid/security/validator/ValidateTest.java b/src/test/java/org/webeid/security/validator/ValidateTest.java index b2743704..912e70e5 100644 --- a/src/test/java/org/webeid/security/validator/ValidateTest.java +++ b/src/test/java/org/webeid/security/validator/ValidateTest.java @@ -1,10 +1,10 @@ /* - * Copyright (c) 2020 The Web eID Project + * Copyright (c) 2020-2021 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * to use, copy, modify, mergCertificateExpiryValidatore, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * @@ -25,8 +25,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.webeid.security.exceptions.TokenParseException; -import org.webeid.security.exceptions.UserCertificateExpiredException; -import org.webeid.security.exceptions.UserCertificateNotYetValidException; +import org.webeid.security.exceptions.CertificateExpiredException; +import org.webeid.security.exceptions.CertificateNotYetValidException; import org.webeid.security.testutil.AbstractTestWithMockedDateValidatorAndCorrectNonce; import org.webeid.security.testutil.Dates; import org.webeid.security.testutil.Tokens; @@ -50,22 +50,51 @@ public void tearDown() { } } + // Subject certificate validity: + // - not before: Thu Oct 18 12:50:47 EEST 2018 + // - not after: Wed Oct 18 00:59:59 EEST 2023 + // Issuer certificate validity: + // - not before: Thu Sep 06 12:03:52 EEST 2018 + // - not after: Tue Aug 30 15:48:28 EEST 2033 + @Test - void certificateIsNotValidYet() throws Exception { + void userCertificateIsNotYetValid() throws Exception { final Date certValidFrom = Dates.create("2018-10-17"); setMockedFunctionalSubjectCertificateValidatorsDate(certValidFrom); assertThatThrownBy(() -> validator.validate(Tokens.SIGNED)) - .isInstanceOf(UserCertificateNotYetValidException.class); + .isInstanceOf(CertificateNotYetValidException.class) + .hasMessage("User certificate is not yet valid"); + } + + @Test + void trustedCACertificateIsNotYetValid() throws Exception { + final Date certValidFrom = Dates.create("2018-08-17"); + setMockedFunctionalSubjectCertificateValidatorsDate(certValidFrom); + + assertThatThrownBy(() -> validator.validate(Tokens.SIGNED)) + .isInstanceOf(CertificateNotYetValidException.class) + .hasMessage("Trusted CA certificate is not yet valid"); } @Test - void certificateIsNoLongerValid() throws Exception { + void userCertificateIsNoLongerValid() throws Exception { final Date certValidFrom = Dates.create("2023-10-19"); setMockedFunctionalSubjectCertificateValidatorsDate(certValidFrom); assertThatThrownBy(() -> validator.validate(Tokens.SIGNED)) - .isInstanceOf(UserCertificateExpiredException.class); + .isInstanceOf(CertificateExpiredException.class) + .hasMessage("User certificate has expired"); + } + + @Test + void trustedCACertificateIsNoLongerValid() throws Exception { + final Date certValidFrom = Dates.create("2033-10-19"); + setMockedFunctionalSubjectCertificateValidatorsDate(certValidFrom); + + assertThatThrownBy(() -> validator.validate(Tokens.SIGNED)) + .isInstanceOf(CertificateExpiredException.class) + .hasMessage("Trusted CA certificate has expired"); } @Test diff --git a/src/test/java/org/webeid/security/validator/ocsp/OcspResponseValidatorTest.java b/src/test/java/org/webeid/security/validator/ocsp/OcspResponseValidatorTest.java new file mode 100644 index 00000000..7bdd739d --- /dev/null +++ b/src/test/java/org/webeid/security/validator/ocsp/OcspResponseValidatorTest.java @@ -0,0 +1,70 @@ +package org.webeid.security.validator.ocsp; + +import okhttp3.ResponseBody; +import org.bouncycastle.asn1.ocsp.SingleResponse; +import org.bouncycastle.cert.ocsp.SingleResp; +import org.junit.jupiter.api.Test; +import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; +import org.webeid.security.testutil.Dates; +import org.webeid.security.validator.validators.SubjectCertificateNotRevokedValidator; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.webeid.security.validator.ocsp.OcspResponseValidator.validateCertificateStatusUpdateTime; + +class OcspResponseValidatorTest { + + @Test + void whenThisUpdateDayBeforeProducedAt_thenThrows() throws Exception { + final SingleResp mockResponse = mock(SingleResp.class); + // yyyy-MM-dd'T'HH:mm:ss.SSSZ + when(mockResponse.getThisUpdate()).thenReturn(Dates.create("2021-09-01T00:00:00.000Z")); + final Date producedAt = Dates.create("2021-09-02T00:00:00.000Z"); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validateCertificateStatusUpdateTime(mockResponse, producedAt)) + .withMessage("User certificate revocation check has failed: " + + "Certificate status update time check failed: " + + "notAllowedBefore: 2021-09-01 23:45:00 UTC, " + + "notAllowedAfter: 2021-09-02 00:15:00 UTC, " + + "thisUpdate: 2021-09-01 00:00:00 UTC, " + + "nextUpdate: null"); + } + + @Test + void whenThisUpdateDayAfterProducedAt_thenThrows() throws Exception { + final SingleResp mockResponse = mock(SingleResp.class); + when(mockResponse.getThisUpdate()).thenReturn(Dates.create("2021-09-02")); + final Date producedAt = Dates.create("2021-09-01"); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validateCertificateStatusUpdateTime(mockResponse, producedAt)) + .withMessage("User certificate revocation check has failed: " + + "Certificate status update time check failed: " + + "notAllowedBefore: 2021-08-31 23:45:00 UTC, " + + "notAllowedAfter: 2021-09-01 00:15:00 UTC, " + + "thisUpdate: 2021-09-02 00:00:00 UTC, " + + "nextUpdate: null"); + } + + @Test + void whenNextUpdateDayBeforeProducedAt_thenThrows() throws Exception { + final SingleResp mockResponse = mock(SingleResp.class); + when(mockResponse.getThisUpdate()).thenReturn(Dates.create("2021-09-02")); + when(mockResponse.getNextUpdate()).thenReturn(Dates.create("2021-09-01")); + final Date producedAt = Dates.create("2021-09-02"); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validateCertificateStatusUpdateTime(mockResponse, producedAt)) + .withMessage("User certificate revocation check has failed: " + + "Certificate status update time check failed: " + + "notAllowedBefore: 2021-09-01 23:45:00 UTC, " + + "notAllowedAfter: 2021-09-02 00:15:00 UTC, " + + "thisUpdate: 2021-09-02 00:00:00 UTC, " + + "nextUpdate: 2021-09-01 00:00:00 UTC"); + } + +} diff --git a/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java b/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java new file mode 100644 index 00000000..bf76569a --- /dev/null +++ b/src/test/java/org/webeid/security/validator/ocsp/OcspServiceProviderTest.java @@ -0,0 +1,70 @@ +package org.webeid.security.validator.ocsp; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.junit.jupiter.api.Test; +import org.webeid.security.exceptions.OCSPCertificateException; +import org.webeid.security.validator.ocsp.service.OcspService; + +import java.net.URI; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert; +import static org.webeid.security.testutil.Certificates.getMariliisEsteid2015Cert; +import static org.webeid.security.testutil.Certificates.getTestEsteid2015CA; +import static org.webeid.security.testutil.Certificates.getTestEsteid2018CA; +import static org.webeid.security.testutil.Certificates.getTestSkOcspResponder2020; +import static org.webeid.security.testutil.OcspServiceMaker.getAiaOcspServiceProvider; +import static org.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceProvider; + +class OcspServiceProviderTest { + + @Test + void whenDesignatedOcspServiceConfigurationProvided_thenCreatesDesignatedOcspService() throws Exception { + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider(); + final OcspService service = ocspServiceProvider.getService(getJaakKristjanEsteid2018Cert()); + assertThat(service.getAccessLocation()).isEqualTo(new URI("http://demo.sk.ee/ocsp")); + assertThat(service.doesSupportNonce()).isTrue(); + assertThatCode(() -> + service.validateResponderCertificate(new X509CertificateHolder(getTestSkOcspResponder2020().getEncoded()), new Date(1630000000000L))) + .doesNotThrowAnyException(); + assertThatCode(() -> + service.validateResponderCertificate(new X509CertificateHolder(getTestEsteid2018CA().getEncoded()), new Date(1630000000000L))) + .isInstanceOf(OCSPCertificateException.class) + .hasMessage("Responder certificate from the OCSP response is not equal to the configured designated OCSP responder certificate"); + } + + @Test + void whenAiaOcspServiceConfigurationProvided_thenCreatesAiaOcspService() throws Exception { + final OcspServiceProvider ocspServiceProvider = getAiaOcspServiceProvider(); + final OcspService service2018 = ocspServiceProvider.getService(getJaakKristjanEsteid2018Cert()); + assertThat(service2018.getAccessLocation()).isEqualTo(new URI("http://aia.demo.sk.ee/esteid2018")); + assertThat(service2018.doesSupportNonce()).isTrue(); + assertThatCode(() -> + // Use the CA certificate instead of responder certificate for convenience. + service2018.validateResponderCertificate(new X509CertificateHolder(getTestEsteid2018CA().getEncoded()), new Date(1630000000000L))) + .doesNotThrowAnyException(); + + final OcspService service2015 = ocspServiceProvider.getService(getMariliisEsteid2015Cert()); + assertThat(service2015.getAccessLocation()).isEqualTo(new URI("http://aia.demo.sk.ee/esteid2015")); + assertThat(service2015.doesSupportNonce()).isFalse(); + assertThatCode(() -> + // Use the CA certificate instead of responder certificate for convenience. + service2015.validateResponderCertificate(new X509CertificateHolder(getTestEsteid2015CA().getEncoded()), new Date(1630000000000L))) + .doesNotThrowAnyException(); + } + + @Test + void whenAiaOcspServiceConfigurationDoesNotHaveResponderCertTrustedCA_thenThrows() throws Exception { + final OcspServiceProvider ocspServiceProvider = getAiaOcspServiceProvider(); + final OcspService service2018 = ocspServiceProvider.getService(getJaakKristjanEsteid2018Cert()); + final X509CertificateHolder wrongResponderCert = new X509CertificateHolder(getMariliisEsteid2015Cert().getEncoded()); + assertThatExceptionOfType(OCSPCertificateException.class) + .isThrownBy(() -> + service2018.validateResponderCertificate(wrongResponderCert, new Date(1630000000000L))); + } + + +} diff --git a/src/test/java/org/webeid/security/validator/ocsp/OcspUrlTest.java b/src/test/java/org/webeid/security/validator/ocsp/OcspUrlTest.java new file mode 100644 index 00000000..645825e7 --- /dev/null +++ b/src/test/java/org/webeid/security/validator/ocsp/OcspUrlTest.java @@ -0,0 +1,41 @@ +package org.webeid.security.validator.ocsp; + +import org.bouncycastle.asn1.x509.Extension; +import org.junit.jupiter.api.Test; + +import java.security.cert.X509Certificate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.webeid.security.validator.ocsp.OcspUrl.getOcspUri; + +class OcspUrlTest { + + @Test + void whenExtensionValueIsNull_thenReturnsNull() throws Exception { + final X509Certificate mockCertificate = mock(X509Certificate.class); + when(mockCertificate.getExtensionValue(anyString())).thenReturn(null); + assertThat(getOcspUri(mockCertificate)).isNull(); + } + + @Test + void whenExtensionValueIsInvalid_thenReturnsNull() throws Exception { + final X509Certificate mockCertificate = mock(X509Certificate.class); + when(mockCertificate.getExtensionValue(anyString())).thenReturn(new byte[]{1, 2, 3}); + assertThat(getOcspUri(mockCertificate)).isNull(); + } + + @Test + void whenExtensionValueIsNotAia_thenReturnsNull() throws Exception { + final X509Certificate mockCertificate = mock(X509Certificate.class); + when(mockCertificate.getExtensionValue(anyString())).thenReturn(new byte[]{ + 4, 64, 48, 62, 48, 50, 6, 11, 43, 6, 1, 4, 1, -125, -111, 33, 1, 2, 1, 48, + 35, 48, 33, 6, 8, 43, 6, 1, 5, 5, 7, 2, 1, 22, 21, 104, 116, 116, 112, 115, + 58, 47, 47, 119, 119, 119, 46, 115, 107, 46, 101, 101, 47, 67, 80, 83, 48, + 8, 6, 6, 4, 0, -113, 122, 1, 2}); + assertThat(getOcspUri(mockCertificate)).isNull(); + } + +} diff --git a/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java b/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java new file mode 100644 index 00000000..33298db3 --- /dev/null +++ b/src/test/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidatorTest.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2021 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.webeid.security.validator.validators; + +import com.google.common.io.ByteStreams; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.webeid.security.exceptions.CertificateNotTrustedException; +import org.webeid.security.exceptions.JceException; +import org.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; +import org.webeid.security.exceptions.UserCertificateRevokedException; +import org.webeid.security.validator.AuthTokenValidatorData; +import org.webeid.security.validator.ocsp.OcspClient; +import org.webeid.security.validator.ocsp.OcspClientImpl; +import org.webeid.security.validator.ocsp.OcspServiceProvider; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.URISyntaxException; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert; +import static org.webeid.security.testutil.Certificates.getTestEsteid2018CA; +import static org.webeid.security.testutil.OcspServiceMaker.getAiaOcspServiceProvider; +import static org.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceProvider; + +class SubjectCertificateNotRevokedValidatorTest { + + private static final MediaType OCSP_RESPONSE = MediaType.get("application/ocsp-response"); + + private final OcspClient ocspClient = OcspClientImpl.build(Duration.ofSeconds(5)); + private SubjectCertificateTrustedValidator trustedValidator; + private AuthTokenValidatorData authTokenValidatorWithEsteid2018Cert; + + @BeforeEach + void setUp() throws Exception { + trustedValidator = new SubjectCertificateTrustedValidator(null, null); + setSubjectCertificateIssuerCertificate(trustedValidator); + authTokenValidatorWithEsteid2018Cert = new AuthTokenValidatorData(getJaakKristjanEsteid2018Cert()); + } + + @Test + void whenValidAiaOcspResponderConfiguration_thenSucceeds() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp(ocspClient); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .doesNotThrowAnyException(); + } + + @Test + void whenValidDesignatedOcspResponderConfiguration_thenSucceeds() throws Exception { + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider(); + final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .doesNotThrowAnyException(); + } + + @Test + void whenValidOcspNonceDisabledConfiguration_thenSucceeds() throws Exception { + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider(false); + final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .doesNotThrowAnyException(); + } + + @Test + void whenOcspUrlIsInvalid_thenThrows() throws Exception { + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("http://invalid.invalid"); + final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .isInstanceOf(UserCertificateOCSPCheckFailedException.class) + .getCause() + .isInstanceOf(IOException.class) + .hasMessageMatching("invalid.invalid: (Name or service not known|" + + "Temporary failure in name resolution)"); + } + + @Test + void whenOcspRequestFails_thenThrows() throws Exception { + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("https://web-eid-test.free.beeceptor.com"); + final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, ocspClient, ocspServiceProvider); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .isInstanceOf(UserCertificateOCSPCheckFailedException.class) + .getCause() + .isInstanceOf(IOException.class) + .hasMessageStartingWith("OCSP request was not successful, response: Response{"); + } + + @Test + void whenOcspRequestHasInvalidBody_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create("invalid", OCSP_RESPONSE)) + .build()); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .isInstanceOf(UserCertificateOCSPCheckFailedException.class) + .getCause() + .isInstanceOf(IOException.class) + .hasMessage("corrupted stream - out of bounds length found: 110 >= 7"); + } + + @Test + void whenOcspResponseIsNotSuccessful_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(buildOcspResponseBodyWithInternalErrorStatus(), OCSP_RESPONSE)) + .build()); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("User certificate revocation check has failed: Response status: internal error"); + } + + @Test + void whenOcspResponseHasInvalidCertificateId_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(buildOcspResponseBodyWithInvalidCertificateId(), OCSP_RESPONSE)) + .build()); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("User certificate revocation check has failed: OCSP responded with certificate ID that differs from the requested ID"); + } + + @Test + void whenOcspResponseHasInvalidSignature_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(buildOcspResponseBodyWithInvalidSignature(), OCSP_RESPONSE)) + .build()); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("User certificate revocation check has failed: OCSP response signature is invalid"); + } + + @Test + void whenOcspResponseHasInvalidResponderCert_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(buildOcspResponseBodyWithInvalidResponderCert(), OCSP_RESPONSE)) + .build()); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .isInstanceOf(UserCertificateOCSPCheckFailedException.class) + .getCause() + .isInstanceOf(OCSPException.class) + .hasMessage("exception processing sig: java.lang.IllegalArgumentException: invalid info structure in RSA public key"); + } + + @Test + void whenOcspResponseHasInvalidTag_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(buildOcspResponseBodyWithInvalidTag(), OCSP_RESPONSE)) + .build()); + assertThatCode(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .isInstanceOf(UserCertificateOCSPCheckFailedException.class) + .getCause() + .isInstanceOf(OCSPException.class) + .hasMessage("problem decoding object: java.io.IOException: unknown tag 23 encountered"); + } + + @Test + void whenOcspResponseHas2CertResponses_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_with_2_responses.der"), OCSP_RESPONSE)) + .build()); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("User certificate revocation check has failed: OCSP response must contain one response, received 2 responses instead"); + } + + @Disabled("It is difficult to make Python and Java CertId equal, needs more work") + void whenOcspResponseHas2ResponderCerts_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_with_2_responder_certs.der"), OCSP_RESPONSE)) + .build()); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("User certificate revocation check has failed: OCSP response must contain one responder certificate, received 2 certificates instead"); + } + + @Test + void whenOcspResponseRevoked_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_revoked.der"), OCSP_RESPONSE)) + .build()); + assertThatExceptionOfType(UserCertificateRevokedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("User certificate has been revoked: Revocation reason: 0"); + } + + @Test + void whenOcspResponseUnknown_thenThrows() throws Exception { + final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("https://web-eid-test.free.beeceptor.com"); + final Response response = getResponseBuilder() + .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_unknown.der"), OCSP_RESPONSE)) + .build(); + final OcspClient client = (url, request) -> new OCSPResp(Objects.requireNonNull(response.body()).bytes()); + final SubjectCertificateNotRevokedValidator validator = new SubjectCertificateNotRevokedValidator(trustedValidator, client, ocspServiceProvider); + assertThatExceptionOfType(UserCertificateRevokedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("User certificate has been revoked: Unknown status"); + } + + @Test + void whenOcspResponseCANotTrusted_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(getOcspResponseBytesFromResources("ocsp_response_unknown.der"), OCSP_RESPONSE)) + .build()); + assertThatExceptionOfType(CertificateNotTrustedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("Certificate EMAILADDRESS=pki@sk.ee, CN=TEST of SK OCSP RESPONDER 2020, OU=OCSP, O=AS Sertifitseerimiskeskus, C=EE is not trusted"); + } + + @Test + void whenNonceDiffers_thenThrows() throws Exception { + final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + getResponseBuilder() + .body(ResponseBody.create(getOcspResponseBytesFromResources(), OCSP_RESPONSE)) + .build()); + assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) + .isThrownBy(() -> + validator.validateCertificateNotRevoked(authTokenValidatorWithEsteid2018Cert)) + .withMessage("User certificate revocation check has failed: OCSP request and response nonces differ, possible replay attack"); + } + + private static byte[] buildOcspResponseBodyWithInternalErrorStatus() throws IOException { + final byte[] ocspResponseBytes = getOcspResponseBytesFromResources(); + final int STATUS_OFFSET = 6; + ocspResponseBytes[STATUS_OFFSET] = OCSPResponseStatus.INTERNAL_ERROR; + return ocspResponseBytes; + } + + private static byte[] buildOcspResponseBodyWithInvalidCertificateId() throws IOException { + final byte[] ocspResponseBytes = getOcspResponseBytesFromResources(); + final int CERTIFICATE_ID_OFFSET = 234; + ocspResponseBytes[CERTIFICATE_ID_OFFSET + 3] = 0x42; + return ocspResponseBytes; + } + + private byte[] buildOcspResponseBodyWithInvalidSignature() throws IOException { + final byte[] ocspResponseBytes = getOcspResponseBytesFromResources(); + final int SIGNATURE_OFFSET = 349; + ocspResponseBytes[SIGNATURE_OFFSET + 5] = 0x01; + return ocspResponseBytes; + } + + private byte[] buildOcspResponseBodyWithInvalidResponderCert() throws IOException { + final byte[] ocspResponseBytes = getOcspResponseBytesFromResources(); + final int CERTIFICATE_OFFSET = 935; + ocspResponseBytes[CERTIFICATE_OFFSET + 3] = 0x42; + return ocspResponseBytes; + } + + private byte[] buildOcspResponseBodyWithInvalidTag() throws IOException { + final byte[] ocspResponseBytes = getOcspResponseBytesFromResources(); + final int TAG_OFFSET = 352; + ocspResponseBytes[TAG_OFFSET] = 0x42; + return ocspResponseBytes; + } + + // Either write the bytes of a real OCSP response to a file or use Python and asn1crypto.ocsp + // to create a mock response, see OCSPBuilder in https://github.com/wbond/ocspbuilder/blob/master/ocspbuilder/__init__.py + // and https://gist.github.com/mrts/bb0dcf93a2b9d2458eab1f9642ee97b2. + private static byte[] getOcspResponseBytesFromResources() throws IOException { + return getOcspResponseBytesFromResources("ocsp_response.der"); + } + + private static byte[] getOcspResponseBytesFromResources(String resource) throws IOException { + try (final InputStream resourceAsStream = ClassLoader.getSystemResourceAsStream(resource)) { + return ByteStreams.toByteArray(Objects.requireNonNull(resourceAsStream)); + } + } + + @NotNull + private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidatorWithAiaOcsp(Response response) throws JceException, URISyntaxException, CertificateException, IOException { + final OcspClient client = (url, request) -> new OCSPResp(Objects.requireNonNull(response.body()).bytes()); + return getSubjectCertificateNotRevokedValidatorWithAiaOcsp(client); + } + + @NotNull + private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidatorWithAiaOcsp(OcspClient client) throws JceException, URISyntaxException, CertificateException, IOException { + return new SubjectCertificateNotRevokedValidator(trustedValidator, client, getAiaOcspServiceProvider()); + } + + private static void setSubjectCertificateIssuerCertificate(SubjectCertificateTrustedValidator trustedValidator) throws NoSuchFieldException, IllegalAccessException, CertificateException, IOException { + final Field field = trustedValidator.getClass().getDeclaredField("subjectCertificateIssuerCertificate"); + field.setAccessible(true); + field.set(trustedValidator, getTestEsteid2018CA()); + } + + @NotNull + private static Response.Builder getResponseBuilder() { + return new Response.Builder() + .request(new Request.Builder().url("http://testing").build()) + .message("testing") + .protocol(Protocol.HTTP_1_1) + .code(200); + } + +} diff --git a/src/test/resources/TEST_of_ESTEID-SK_2015.cer b/src/test/resources/TEST_of_ESTEID-SK_2015.cer new file mode 100644 index 00000000..7749286c Binary files /dev/null and b/src/test/resources/TEST_of_ESTEID-SK_2015.cer differ diff --git a/src/test/resources/TEST_of_SK_OCSP_RESPONDER_2020.cer b/src/test/resources/TEST_of_SK_OCSP_RESPONDER_2020.cer new file mode 100644 index 00000000..f2c4bd5e Binary files /dev/null and b/src/test/resources/TEST_of_SK_OCSP_RESPONDER_2020.cer differ diff --git a/src/test/resources/ocsp_response.der b/src/test/resources/ocsp_response.der new file mode 100644 index 00000000..9bf94116 Binary files /dev/null and b/src/test/resources/ocsp_response.der differ diff --git a/src/test/resources/ocsp_response_revoked.der b/src/test/resources/ocsp_response_revoked.der new file mode 100644 index 00000000..551a99df Binary files /dev/null and b/src/test/resources/ocsp_response_revoked.der differ diff --git a/src/test/resources/ocsp_response_unknown.der b/src/test/resources/ocsp_response_unknown.der new file mode 100644 index 00000000..0c1e0187 Binary files /dev/null and b/src/test/resources/ocsp_response_unknown.der differ diff --git a/src/test/resources/ocsp_response_with_2_responder_certs.der b/src/test/resources/ocsp_response_with_2_responder_certs.der new file mode 100644 index 00000000..b8520125 Binary files /dev/null and b/src/test/resources/ocsp_response_with_2_responder_certs.der differ diff --git a/src/test/resources/ocsp_response_with_2_responses.der b/src/test/resources/ocsp_response_with_2_responses.der new file mode 100644 index 00000000..387b8aab Binary files /dev/null and b/src/test/resources/ocsp_response_with_2_responses.der differ