diff --git a/README.md b/README.md
index f4c5105a..6622a660 100644
--- a/README.md
+++ b/README.md
@@ -101,7 +101,7 @@ import eu.webeid.security.challenge.ChallengeNonceStore;
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`](src/main/java/eu/webeid/security/certificate/CertificateLoader.java) helper class to load CA certificates from resources here, but consider using [the truststore file](example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java) instead.
-First, copy the trusted certificates, for example `ESTEID2018.cer`, to `resources/cacerts/`, then load the certificates as follows:
+To accept both ID-card generations that are currently in use, copy the `ESTEID2018` intermediate certificate from [SK ID Solutions](https://www.skidsolutions.eu/resources/certificates/) and the `ESTEID2025` intermediate certificate from [Zetes](https://repository.eidpki.ee/crt/) to `resources/cacerts/`, then load both certificates as follows:
```java
import java.io.IOException;
@@ -111,7 +111,10 @@ import eu.webeid.security.certificate.CertificateLoader;
...
private X509Certificate[] trustedIntermediateCACertificates() throws CertificateException, IOException {
- return CertificateLoader.loadCertificatesFromResources("cacerts/ESTEID2018.cer");
+ return CertificateLoader.loadCertificatesFromResources(
+ "cacerts/ESTEID2018.cer",
+ "cacerts/ESTEID2025.crt"
+ );
}
...
```
@@ -320,7 +323,44 @@ toTitleCase(CertificateData.getSubjectSurname(userCertificate).orElseThrow()); /
The default validator uses the platform OCSP implementation to check certificate revocation. A revoked certificate or an unsuccessful revocation check causes authentication to fail.
-Use `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` to add disallowed certificate policies. Estonian Mobile-ID policies are disallowed by default because smart-card authentication must not accept Mobile-ID certificates.
+Certificate policy allow-lists are selected by the subject certificate's exact issuer `X500Principal`. At build time, production Estonian ID-card policies from `ESTEID_ID_CARD_POLICIES` are used when the configured allow-list is empty. When one or more issuer entries are configured, only those entries are used. A matching issuer must contain one of its configured policy OIDs; other issuers fall back to the deprecated deny-list, which is empty by default. Calling `withAllowedCertificatePolicies(issuer)` without policy OIDs removes that issuer's entry. If no entries remain, the Estonian defaults are used.
+
+Use `withAllowedCertificatePolicies(X500Principal issuer, ASN1ObjectIdentifier... policies)` to define a custom allow-list. Repeated calls configure all supported issuers on the same validator. The following example explicitly configures both Estonian ID-card generations in one builder chain.
+
+```java
+AuthTokenValidator tokenValidator = new AuthTokenValidatorBuilder()
+ .withSiteOrigin(URI.create("https://example.org"))
+ .withTrustedCertificateAuthorities(trustedIntermediateCACertificates())
+ .withAllowedCertificatePolicies(
+ new X500Principal("CN=ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133,O=SK ID Solutions AS,C=EE"),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.1"),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.2")
+ )
+ .withAllowedCertificatePolicies(
+ new X500Principal("C=EE,O=Zetes Estonia OÜ,2.5.4.97=#0c0e4e545245452d3137303636303439,CN=ESTEID2025"),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.1"),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.2")
+ )
+ .build();
+```
+
+For another country, add each supported issuer and its policy OIDs to the same builder chain and provide the corresponding trusted intermediate CAs.
+
+To restore old behaviour of deny list, use
+```java
+AuthTokenValidator tokenValidator = new AuthTokenValidatorBuilder()
+ .withSiteOrigin(URI.create("https://example.org"))
+ .withTrustedCertificateAuthorities(trustedIntermediateCACertificates())
+ .withDisallowedCertificatePolicies(
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V2,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V3,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY
+ )
+ .build();
+```
+
+See the [certificate policy allow-list guide](readme-policy.md) for the Estonian ID-card whitelist and its rationale.
For more advanced revocation requirements, supply a `CertificateRevocationChecker` with `withCertificateRevocationChecker(...)`. The [OCSP configuration guide](src/main/java/eu/webeid/ocsp/README.md) covers custom implementations, the bundled OCSP checker, custom PKIX checkers, responder selection, HTTP settings, and nonce policies.
diff --git a/example/src/test/java/eu/webeid/example/config/TestValidatorConfiguration.java b/example/src/test/java/eu/webeid/example/config/TestValidatorConfiguration.java
index f838a2da..6ca7e9ef 100644
--- a/example/src/test/java/eu/webeid/example/config/TestValidatorConfiguration.java
+++ b/example/src/test/java/eu/webeid/example/config/TestValidatorConfiguration.java
@@ -6,10 +6,12 @@
import eu.webeid.security.exceptions.JceException;
import eu.webeid.security.validator.AuthTokenValidator;
import eu.webeid.security.validator.AuthTokenValidatorBuilder;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
+import javax.security.auth.x500.X500Principal;
import java.net.URI;
import java.util.List;
@@ -30,6 +32,13 @@ AuthTokenValidator validatorWithoutOcspCall(ValidationConfiguration validationCo
.withSiteOrigin(URI.create(yamlConfig.getLocalOrigin()))
.withTrustedCertificateAuthorities(validationConfiguration.loadTrustedCACertificatesFromCerFiles())
.withTrustedCertificateAuthorities(validationConfiguration.loadTrustedCACertificatesFromTrustStore(yamlConfig))
+ .withAllowedCertificatePolicies(
+ new X500Principal(
+ "CN=TEST of ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133," +
+ "O=SK ID Solutions AS,C=EE"
+ ),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.2.1")
+ )
// Do not call the real OCSP service in tests.
.withCertificateRevocationChecker((subjectCertificate, issuerCertificate) -> List.of())
.build();
diff --git a/src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java b/src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java
index 11ef5442..0fa5b027 100644
--- a/src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java
+++ b/src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java
@@ -5,6 +5,10 @@
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import javax.security.auth.x500.X500Principal;
+import java.util.Map;
+import java.util.Set;
+
public final class SubjectCertificatePolicies {
private static final String ESTEID_SK_2015_MOBILE_ID_POLICY_PREFIX = "1.3.6.1.4.1.10015.1.3";
@@ -18,6 +22,44 @@ public final class SubjectCertificatePolicies {
public static final ASN1ObjectIdentifier ESTEID_SK_2015_MOBILE_ID_POLICY_V3 =
new ASN1ObjectIdentifier(ESTEID_SK_2015_MOBILE_ID_POLICY_PREFIX + ".3");
+ /**
+ * Production document policies for Estonian and European Union citizen ID cards issued under the ESTEID2018
+ * and eID certificate-policy generations, grouped by issuer distinguished name.
+ *
+ * @see
+ * ID.ee certificate policy index for Estonian identity documents
+ * @see
+ * IDEMIA-generation certificate policy
+ * @see
+ * SK ID Solutions certificate policies
+ * @see
+ * SK ID Solutions certificate profiles
+ * @see
+ * ESTEID2018 certificate profile
+ * @see
+ * Thales-generation certificate policy
+ * @see
+ * Zetes Estonia trust-service repository
+ * @see
+ * ESTEID2025 certificate profile
+ */
+ public static final Map> ESTEID_ID_CARD_POLICIES = Map.of(
+ new X500Principal(
+ "CN=ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133,O=SK ID Solutions AS,C=EE"
+ ), Set.of(
+ // ESTEID2018 / IDEMIA generation
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.1"),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.2")
+ ),
+ new X500Principal(
+ "C=EE,O=Zetes Estonia OÜ,2.5.4.97=#0c0e4e545245452d3137303636303439,CN=ESTEID2025"
+ ), Set.of(
+ // eID / Thales generation
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.1"),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.2")
+ )
+ );
+
private SubjectCertificatePolicies() {
throw new IllegalStateException("Constants class");
}
diff --git a/src/main/java/eu/webeid/security/exceptions/UserCertificateDisallowedPolicyException.java b/src/main/java/eu/webeid/security/exceptions/UserCertificateDisallowedPolicyException.java
index 1a41cedd..ef68091a 100644
--- a/src/main/java/eu/webeid/security/exceptions/UserCertificateDisallowedPolicyException.java
+++ b/src/main/java/eu/webeid/security/exceptions/UserCertificateDisallowedPolicyException.java
@@ -4,7 +4,7 @@
package eu.webeid.security.exceptions;
/**
- * Thrown when any of the configured disallowed policies is present in the user certificate.
+ * Thrown when the user certificate does not satisfy the configured allowed or disallowed policy restrictions.
*/
public class UserCertificateDisallowedPolicyException extends AuthTokenException {
public UserCertificateDisallowedPolicyException() {
diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java
index 4dbfcb54..15a7aba8 100644
--- a/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java
+++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java
@@ -3,18 +3,21 @@
package eu.webeid.security.validator;
-import eu.webeid.security.certificate.SubjectCertificatePolicies;
import eu.webeid.security.validator.revocationcheck.CertificateRevocationChecker;
import eu.webeid.security.validator.revocationcheck.RevocationMode;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import javax.security.auth.x500.X500Principal;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.cert.PKIXRevocationChecker;
import java.security.cert.X509Certificate;
import java.util.Collection;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -24,13 +27,9 @@ public final class AuthTokenValidationConfiguration {
private URI siteOrigin;
private Collection trustedCACertificates = new HashSet<>();
- // Don't allow Estonian Mobile-ID policy by default.
- private Collection disallowedSubjectCertificatePolicies = new HashSet<>(Set.of(
- SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1,
- SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V2,
- SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V3,
- SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY
- ));
+ private Map> allowedSubjectCertificatePolicies = new HashMap<>();
+ @Deprecated(since = "4.0.0")
+ private Collection disallowedSubjectCertificatePolicies = new HashSet<>();
private boolean isUserCertificateRevocationCheckEnabled = true;
private boolean platformOcspNonceEnabled = true;
private CertificateRevocationChecker certificateRevocationChecker;
@@ -43,6 +42,8 @@ public final class AuthTokenValidationConfiguration {
private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) {
this.siteOrigin = other.siteOrigin;
this.trustedCACertificates = Set.copyOf(other.trustedCACertificates);
+ this.allowedSubjectCertificatePolicies =
+ copyAllowedSubjectCertificatePolicies(other.allowedSubjectCertificatePolicies);
this.disallowedSubjectCertificatePolicies = Set.copyOf(other.disallowedSubjectCertificatePolicies);
this.isUserCertificateRevocationCheckEnabled = other.isUserCertificateRevocationCheckEnabled;
this.platformOcspNonceEnabled = other.platformOcspNonceEnabled;
@@ -63,10 +64,41 @@ Collection getTrustedCACertificates() {
return trustedCACertificates;
}
+ public Map> getAllowedSubjectCertificatePolicies() {
+ return allowedSubjectCertificatePolicies;
+ }
+
+ void setAllowedSubjectCertificatePolicies(
+ X500Principal issuer, Collection allowedSubjectCertificatePolicies) {
+ Objects.requireNonNull(issuer, "Issuer must not be null");
+ final Set policies = Set.copyOf(Objects.requireNonNull(
+ allowedSubjectCertificatePolicies,
+ "Allowed certificate policies must not be null"
+ ));
+ if (policies.isEmpty()) {
+ this.allowedSubjectCertificatePolicies.remove(issuer);
+ } else {
+ this.allowedSubjectCertificatePolicies.put(issuer, policies);
+ }
+ }
+
+ /**
+ * @deprecated Configure allowed certificate policies instead.
+ */
+ @Deprecated(since = "4.0.0")
public Collection getDisallowedSubjectCertificatePolicies() {
return disallowedSubjectCertificatePolicies;
}
+ /**
+ * @deprecated Configure allowed certificate policies instead.
+ */
+ @Deprecated(since = "4.0.0")
+ void setDisallowedSubjectCertificatePolicies(
+ Collection disallowedSubjectCertificatePolicies) {
+ this.disallowedSubjectCertificatePolicies = new HashSet<>(disallowedSubjectCertificatePolicies);
+ }
+
boolean isUserCertificateRevocationCheckEnabled() {
return isUserCertificateRevocationCheckEnabled;
}
@@ -120,6 +152,16 @@ AuthTokenValidationConfiguration copy() {
return new AuthTokenValidationConfiguration(this);
}
+ private static Map> copyAllowedSubjectCertificatePolicies(
+ Map> policiesByIssuer) {
+ final Map> copy = new HashMap<>();
+ policiesByIssuer.forEach((issuer, policies) -> copy.put(
+ Objects.requireNonNull(issuer, "Issuer must not be null"),
+ Set.copyOf(Objects.requireNonNull(policies, "Allowed certificate policies must not be null"))
+ ));
+ return copy;
+ }
+
/**
* Validates that the given URI is an origin URL as defined in MDN,
* in the form of {@code "://" [ ":" ]}.
diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java
index d072979f..93ea9867 100644
--- a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java
+++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java
@@ -3,15 +3,18 @@
package eu.webeid.security.validator;
+import eu.webeid.security.certificate.SubjectCertificatePolicies;
import eu.webeid.security.exceptions.JceException;
import eu.webeid.security.validator.revocationcheck.CertificateRevocationChecker;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.security.auth.x500.X500Principal;
import java.net.URI;
import java.security.cert.PKIXRevocationChecker;
import java.security.cert.X509Certificate;
+import java.util.Arrays;
import java.util.Collections;
/**
@@ -61,15 +64,35 @@ public AuthTokenValidatorBuilder withTrustedCertificateAuthorities(X509Certifica
}
/**
- * Adds the given policies to the list of disallowed user certificate policies.
+ * Adds or replaces the allowed user certificate policies for the given issuer. When no policies are configured,
+ * {@link SubjectCertificatePolicies#ESTEID_ID_CARD_POLICIES} is used by default.
+ * When the subject certificate's issuer matches a configured issuer, the certificate must contain at least one
+ * of that issuer's allowed policies and the configured disallowed policies are ignored. Otherwise, validation
+ * falls back to the disallowed policy list. Passing no policies removes the issuer's allow-list entry.
+ *
+ * @param issuer exact subject certificate issuer distinguished name
+ * @param policies allowed user certificate policies for the issuer
+ * @return the builder instance for method chaining
+ */
+ public AuthTokenValidatorBuilder withAllowedCertificatePolicies(
+ X500Principal issuer, ASN1ObjectIdentifier... policies) {
+ configuration.setAllowedSubjectCertificatePolicies(issuer, Arrays.asList(policies));
+ LOG.debug("Allowed subject certificate policies set to {}", configuration.getAllowedSubjectCertificatePolicies());
+ return this;
+ }
+
+ /**
+ * Sets 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.
*
* @param policies disallowed user certificate policies
* @return the builder instance for method chaining
+ * @deprecated Use {@link #withAllowedCertificatePolicies(X500Principal, ASN1ObjectIdentifier...)} instead.
*/
+ @Deprecated(since = "4.0.0")
public AuthTokenValidatorBuilder withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies) {
- Collections.addAll(configuration.getDisallowedSubjectCertificatePolicies(), policies);
+ configuration.setDisallowedSubjectCertificatePolicies(Arrays.asList(policies));
LOG.debug("Disallowed subject certificate policies set to {}", configuration.getDisallowedSubjectCertificatePolicies());
return this;
}
@@ -147,8 +170,13 @@ public AuthTokenValidatorBuilder withPKIXRevocationChecker(PKIXRevocationChecker
* @throws JceException when JCE configuration is invalid
*/
public AuthTokenValidator build() throws IllegalArgumentException, JceException {
- configuration.validate();
- return new AuthTokenValidatorImpl(configuration);
+ final AuthTokenValidationConfiguration configurationSnapshot = configuration.copy();
+ if (configurationSnapshot.getAllowedSubjectCertificatePolicies().isEmpty()) {
+ configurationSnapshot.getAllowedSubjectCertificatePolicies()
+ .putAll(SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES);
+ }
+ configurationSnapshot.validate();
+ return new AuthTokenValidatorImpl(configurationSnapshot);
}
}
diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java
index afae8093..a11245db 100644
--- a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java
+++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java
@@ -54,7 +54,10 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
trustedCACertificateAnchors = CertificateValidator.buildTrustAnchorsFromCertificates(configuration.getTrustedCACertificates());
trustedCACertificateCertStore = CertificateValidator.buildCertStoreFromCertificates(configuration.getTrustedCACertificates());
- subjectCertificatePolicyValidator = new SubjectCertificatePolicyValidator(configuration.getDisallowedSubjectCertificatePolicies());
+ subjectCertificatePolicyValidator = new SubjectCertificatePolicyValidator(
+ this.configuration.getAllowedSubjectCertificatePolicies(),
+ this.configuration.getDisallowedSubjectCertificatePolicies()
+ );
authTokenSignatureValidator = new AuthTokenSignatureValidator(configuration.getSiteOrigin());
}
diff --git a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java
index f05109b2..4ed930d4 100644
--- a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java
+++ b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java
@@ -4,44 +4,71 @@
package eu.webeid.security.validator.certvalidators;
import eu.webeid.security.exceptions.AuthTokenException;
+import eu.webeid.security.exceptions.UserCertificateDisallowedPolicyException;
+import eu.webeid.security.exceptions.UserCertificateParseException;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x509.CertificatePolicies;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
-import eu.webeid.security.exceptions.UserCertificateDisallowedPolicyException;
-import eu.webeid.security.exceptions.UserCertificateParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.security.auth.x500.X500Principal;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
public final class SubjectCertificatePolicyValidator {
private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificatePolicyValidator.class);
+ private final Map> allowedSubjectCertificatePolicies;
private final Collection disallowedSubjectCertificatePolicies;
+ /**
+ * @deprecated Configure both allowed and fallback disallowed policies with
+ * {@link #SubjectCertificatePolicyValidator(Map, Collection)}.
+ */
+ @Deprecated(since = "4.0.0")
public SubjectCertificatePolicyValidator(Collection disallowedSubjectCertificatePolicies) {
- this.disallowedSubjectCertificatePolicies = disallowedSubjectCertificatePolicies;
+ this(Map.of(), disallowedSubjectCertificatePolicies);
+ }
+
+ public SubjectCertificatePolicyValidator(
+ Map> allowedSubjectCertificatePolicies,
+ Collection disallowedSubjectCertificatePolicies) {
+ this.allowedSubjectCertificatePolicies = copyAllowedSubjectCertificatePolicies(allowedSubjectCertificatePolicies);
+ this.disallowedSubjectCertificatePolicies = Set.copyOf(disallowedSubjectCertificatePolicies);
}
/**
- * Rejects any configured disallowed policy present in the certificate policies extension.
- * A certificate without the certificate policies extension does not contain disallowed policies and passes validation.
+ * Requires a configured allowed policy when the certificate issuer has an allow-list entry.
+ * Otherwise, rejects any configured disallowed policy present in the certificate policies extension.
+ * A certificate without the certificate policies extension passes only when its issuer has no allow-list entry.
*
* @param subjectCertificate user certificate to be validated
- * @throws UserCertificateDisallowedPolicyException when a disallowed policy is present.
+ * @throws UserCertificateDisallowedPolicyException when no allowed policy is present or a disallowed policy is present.
* @throws UserCertificateParseException when the certificate policies extension is invalid.
*/
public void validateCertificatePolicies(X509Certificate subjectCertificate) throws AuthTokenException {
+ final X500Principal issuer = subjectCertificate.getIssuerX500Principal();
final byte[] extensionValue = subjectCertificate.getExtensionValue(Extension.certificatePolicies.getId());
+ final Collection issuerAllowedPolicies = allowedSubjectCertificatePolicies.getOrDefault(issuer, Set.of());
+
if (extensionValue == null) {
+ if (!issuerAllowedPolicies.isEmpty()) {
+ LOG.debug("User certificate does not contain the certificate policies extension, hence it does not contain an allowed policy.");
+ throw new UserCertificateDisallowedPolicyException();
+ }
LOG.debug("User certificate does not contain the certificate policies extension, hence it does not contain disallowed policies.");
return;
}
+
try {
final CertificatePolicies policies = CertificatePolicies.getInstance(
JcaX509ExtensionUtils.parseExtensionValue(extensionValue)
@@ -49,9 +76,18 @@ public void validateCertificatePolicies(X509Certificate subjectCertificate) thro
if (policies == null) {
throw new IllegalArgumentException("Certificate policies extension is empty");
}
+ if (!issuerAllowedPolicies.isEmpty()) {
+ if (Arrays.stream(policies.getPolicyInformation())
+ .noneMatch(policyInformation ->
+ issuerAllowedPolicies.contains(policyInformation.getPolicyIdentifier()))) {
+ throw new UserCertificateDisallowedPolicyException();
+ }
+ LOG.debug("User certificate contains a policy allowed for issuer '{}'.", issuer);
+ return;
+ }
if (Arrays.stream(policies.getPolicyInformation())
.anyMatch(policyInformation ->
- disallowedSubjectCertificatePolicies.contains(policyInformation.getPolicyIdentifier()))) {
+ disallowedSubjectCertificatePolicies.contains(policyInformation.getPolicyIdentifier()))) {
throw new UserCertificateDisallowedPolicyException();
}
} catch (IOException | IllegalArgumentException e) {
@@ -59,4 +95,14 @@ public void validateCertificatePolicies(X509Certificate subjectCertificate) thro
}
LOG.debug("User certificate does not contain disallowed policies.");
}
+
+ private static Map> copyAllowedSubjectCertificatePolicies(
+ Map> policiesByIssuer) {
+ final Map> copy = new HashMap<>();
+ policiesByIssuer.forEach((issuer, policies) -> copy.put(
+ Objects.requireNonNull(issuer, "Issuer must not be null"),
+ Set.copyOf(Objects.requireNonNull(policies, "Allowed certificate policies must not be null"))
+ ));
+ return Map.copyOf(copy);
+ }
}
diff --git a/src/test/java/eu/webeid/security/certificate/SubjectCertificatePoliciesTest.java b/src/test/java/eu/webeid/security/certificate/SubjectCertificatePoliciesTest.java
new file mode 100644
index 00000000..f6f0e06a
--- /dev/null
+++ b/src/test/java/eu/webeid/security/certificate/SubjectCertificatePoliciesTest.java
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: Estonian Information System Authority
+// SPDX-License-Identifier: MIT
+
+package eu.webeid.security.certificate;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.junit.jupiter.api.Test;
+
+import javax.security.auth.x500.X500Principal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SubjectCertificatePoliciesTest {
+
+ private static final X500Principal ESTEID2018_ISSUER = new X500Principal(
+ "CN=ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133,O=SK ID Solutions AS,C=EE"
+ );
+ private static final X500Principal ESTEID2025_ISSUER = new X500Principal(
+ "C=EE,O=Zetes Estonia OÜ,2.5.4.97=#0c0e4e545245452d3137303636303439,CN=ESTEID2025"
+ );
+
+ @Test
+ void esteidIdCardPoliciesContainOnlyProductionIdCardPolicies() {
+ assertThat(SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES)
+ .containsOnlyKeys(ESTEID2018_ISSUER, ESTEID2025_ISSUER);
+ assertThat(SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES.get(ESTEID2018_ISSUER))
+ .containsExactlyInAnyOrder(
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.1"),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.2")
+ );
+ assertThat(SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES.get(ESTEID2025_ISSUER))
+ .containsExactlyInAnyOrder(
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.1"),
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.2")
+ );
+ }
+}
diff --git a/src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java b/src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java
index fd7348e2..5ea9922d 100644
--- a/src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java
+++ b/src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java
@@ -9,6 +9,7 @@
import eu.webeid.security.validator.AuthTokenValidatorBuilder;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import javax.security.auth.x500.X500Principal;
import java.io.IOException;
import java.net.URI;
import java.security.cert.CertificateException;
@@ -17,7 +18,13 @@
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");
+ private static final X500Principal EST_IDEMIA_TEST_ISSUER = new X500Principal(
+ "CN=TEST of ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133,O=SK ID Solutions AS,C=EE"
+ );
+ private static final ASN1ObjectIdentifier EST_IDEMIA_PRODUCTION_POLICY =
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.1");
+ private static final ASN1ObjectIdentifier EST_IDEMIA_TEST_POLICY =
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.2.1");
public static AuthTokenValidator getAuthTokenValidator() throws CertificateException, JceException, IOException {
return getAuthTokenValidator(TOKEN_ORIGIN_URL);
@@ -49,8 +56,10 @@ public static AuthTokenValidator getAuthTokenValidatorWithJuly2024ExpiredUnrelat
}
public static AuthTokenValidator getAuthTokenValidatorWithDisallowedESTEIDPolicy() throws CertificateException, JceException, IOException {
- return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates())
- .withDisallowedCertificatePolicies(EST_IDEMIA_POLICY)
+ return new AuthTokenValidatorBuilder()
+ .withSiteOrigin(URI.create(TOKEN_ORIGIN_URL))
+ .withTrustedCertificateAuthorities(getCACertificates())
+ .withDisallowedCertificatePolicies(EST_IDEMIA_TEST_POLICY)
.withoutUserCertificateRevocationCheck()
.build();
}
@@ -76,7 +85,12 @@ public static AuthTokenValidatorBuilder getDefaultAuthTokenValidatorBuilder() th
private static AuthTokenValidatorBuilder getAuthTokenValidatorBuilder(String uri, X509Certificate[] certificates) {
return new AuthTokenValidatorBuilder()
.withSiteOrigin(URI.create(uri))
- .withTrustedCertificateAuthorities(certificates);
+ .withTrustedCertificateAuthorities(certificates)
+ .withAllowedCertificatePolicies(
+ EST_IDEMIA_TEST_ISSUER,
+ EST_IDEMIA_PRODUCTION_POLICY,
+ EST_IDEMIA_TEST_POLICY
+ );
}
private static X509Certificate[] getCACertificates() throws CertificateException, IOException {
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java
index c5043a2d..84310f70 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java
@@ -5,6 +5,7 @@
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import eu.webeid.security.authtoken.WebEidAuthToken;
+import eu.webeid.security.certificate.SubjectCertificatePolicies;
import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.security.exceptions.AuthTokenParseException;
import eu.webeid.security.exceptions.CertificateDecodingException;
@@ -18,12 +19,14 @@
import eu.webeid.security.testutil.AbstractTestWithValidator;
import eu.webeid.security.testutil.AuthTokenValidators;
import eu.webeid.security.util.DateAndTime;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
+import javax.security.auth.x500.X500Principal;
import java.security.cert.CertificateException;
import java.util.Base64;
@@ -35,6 +38,13 @@
class AuthTokenCertificateTest extends AbstractTestWithValidator {
+ private static final ASN1ObjectIdentifier ESTEID2018_TEST_POLICY =
+ new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.2.1");
+ private static final ASN1ObjectIdentifier UNRELATED_POLICY = new ASN1ObjectIdentifier("1.2.3");
+ private static final X500Principal ESTEID2018_TEST_ISSUER = new X500Principal(
+ "CN=TEST of ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133,O=SK ID Solutions AS,C=EE"
+ );
+
private static final String AUTH_TOKEN = "{\"algorithm\":\"ES384\"," +
"\"unverifiedCertificate\":\"X5C\"," +
"\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," +
@@ -178,12 +188,55 @@ void whenCertificatePolicyIsDisallowed_thenValidationFails() throws Exception {
.isInstanceOf(UserCertificateDisallowedPolicyException.class);
}
+ @Test
+ void whenCertificatePolicyIsAllowed_thenValidationSucceeds() throws Exception {
+ final AuthTokenValidator validatorWithAllowedESTEIDPolicy = AuthTokenValidators
+ .getDefaultAuthTokenValidatorBuilder()
+ .withAllowedCertificatePolicies(ESTEID2018_TEST_ISSUER, ESTEID2018_TEST_POLICY)
+ .withoutUserCertificateRevocationCheck()
+ .build();
+
+ assertThatCode(() -> validatorWithAllowedESTEIDPolicy.validate(validAuthToken, VALID_CHALLENGE_NONCE))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void whenCertificatePolicyIsNotAllowed_thenValidationFails() throws Exception {
+ final AuthTokenValidator validatorWithUnrelatedAllowedPolicy = AuthTokenValidators
+ .getDefaultAuthTokenValidatorBuilder()
+ .withAllowedCertificatePolicies(ESTEID2018_TEST_ISSUER, UNRELATED_POLICY)
+ .withoutUserCertificateRevocationCheck()
+ .build();
+
+ assertThatThrownBy(() -> validatorWithUnrelatedAllowedPolicy.validate(validAuthToken, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(UserCertificateDisallowedPolicyException.class);
+ }
+
@Test
void whenUsingOldMobileIdCertificate_thenValidationFails() throws AuthTokenException {
mockDate("2021-03-01", mockedClock);
final WebEidAuthToken token = replaceTokenField(AUTH_TOKEN, "X5C", OLD_MOBILE_ID_CERT);
assertThatThrownBy(() -> validator
.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(CertificateNotTrustedException.class);
+ }
+
+ @Test
+ void whenUsingOldMobileIdCertificateWithLegacyDenyList_thenPolicyValidationFails() throws Exception {
+ mockDate("2021-03-01", mockedClock);
+ final AuthTokenValidator validatorWithLegacyDenyList = AuthTokenValidators
+ .getDefaultAuthTokenValidatorBuilder()
+ .withDisallowedCertificatePolicies(
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V2,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V3,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY
+ )
+ .withoutUserCertificateRevocationCheck()
+ .build();
+ final WebEidAuthToken token = replaceTokenField(AUTH_TOKEN, "X5C", OLD_MOBILE_ID_CERT);
+
+ assertThatThrownBy(() -> validatorWithLegacyDenyList.validate(token, VALID_CHALLENGE_NONCE))
.isInstanceOf(UserCertificateDisallowedPolicyException.class);
}
@@ -192,6 +245,24 @@ void whenUsingNewMobileIdCertificate_thenValidationFails() throws AuthTokenExcep
final WebEidAuthToken token = replaceTokenField(AUTH_TOKEN, "X5C", NEW_MOBILE_ID_CERT);
assertThatThrownBy(() -> validator
.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(CertificateNotTrustedException.class);
+ }
+
+ @Test
+ void whenUsingNewMobileIdCertificateWithLegacyDenyList_thenPolicyValidationFails() throws Exception {
+ final AuthTokenValidator validatorWithLegacyDenyList = AuthTokenValidators
+ .getDefaultAuthTokenValidatorBuilder()
+ .withDisallowedCertificatePolicies(
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V2,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V3,
+ SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY
+ )
+ .withoutUserCertificateRevocationCheck()
+ .build();
+ final WebEidAuthToken token = replaceTokenField(AUTH_TOKEN, "X5C", NEW_MOBILE_ID_CERT);
+
+ assertThatThrownBy(() -> validatorWithLegacyDenyList.validate(token, VALID_CHALLENGE_NONCE))
.isInstanceOf(UserCertificateDisallowedPolicyException.class);
}
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenValidationConfigurationTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenValidationConfigurationTest.java
index f70eae22..92ebafca 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenValidationConfigurationTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenValidationConfigurationTest.java
@@ -22,20 +22,89 @@
package eu.webeid.security.validator;
+import eu.webeid.security.certificate.SubjectCertificatePolicies;
import eu.webeid.security.testutil.Certificates;
import eu.webeid.security.validator.revocationcheck.CertificateRevocationChecker;
import eu.webeid.security.validator.revocationcheck.RevocationMode;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.junit.jupiter.api.Test;
+import javax.security.auth.x500.X500Principal;
import java.net.URI;
import java.security.cert.PKIXRevocationChecker;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
class AuthTokenValidationConfigurationTest {
+ private static final ASN1ObjectIdentifier ALLOWED_POLICY = new ASN1ObjectIdentifier("1.2.3");
+ private static final X500Principal ISSUER = new X500Principal("CN=Example CA");
+
+ @Test
+ void allowedPoliciesAreEmptyByDefault() {
+ final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration();
+
+ assertThat(configuration.getAllowedSubjectCertificatePolicies())
+ .isEmpty();
+ }
+
+ @Test
+ void copyRetainsAllowedPoliciesIndependently() {
+ final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration();
+ configuration.setAllowedSubjectCertificatePolicies(ISSUER, List.of(ALLOWED_POLICY));
+
+ final AuthTokenValidationConfiguration copy = configuration.copy();
+ configuration.getAllowedSubjectCertificatePolicies().clear();
+
+ assertThat(copy.getAllowedSubjectCertificatePolicies())
+ .containsEntry(ISSUER, Set.of(ALLOWED_POLICY));
+ }
+
+ @Test
+ void setAllowedPoliciesCopiesInputCollection() {
+ final List policies = new ArrayList<>(List.of(ALLOWED_POLICY));
+ final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration();
+
+ configuration.setAllowedSubjectCertificatePolicies(ISSUER, policies);
+ policies.clear();
+
+ assertThat(configuration.getAllowedSubjectCertificatePolicies())
+ .containsOnly(entry(ISSUER, Set.of(ALLOWED_POLICY)));
+ }
+
+ @Test
+ void subsequentAllowedPolicyConfigurationAddsAnotherIssuer() {
+ final X500Principal anotherIssuer = new X500Principal("CN=Another CA");
+ final ASN1ObjectIdentifier anotherPolicy = new ASN1ObjectIdentifier("1.2.4");
+ final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration();
+
+ configuration.setAllowedSubjectCertificatePolicies(ISSUER, List.of(ALLOWED_POLICY));
+ configuration.setAllowedSubjectCertificatePolicies(anotherIssuer, List.of(anotherPolicy));
+
+ assertThat(configuration.getAllowedSubjectCertificatePolicies())
+ .containsOnly(
+ entry(ISSUER, Set.of(ALLOWED_POLICY)),
+ entry(anotherIssuer, Set.of(anotherPolicy))
+ );
+ }
+
+ @Test
+ void settingNoAllowedPoliciesRemovesIssuerEntry() {
+ final X500Principal defaultIssuer = SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES
+ .keySet().iterator().next();
+ final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration();
+
+ configuration.setAllowedSubjectCertificatePolicies(defaultIssuer, List.of());
+
+ assertThat(configuration.getAllowedSubjectCertificatePolicies())
+ .doesNotContainKey(defaultIssuer);
+ }
+
@Test
void whenNoCustomCheckerProvided_thenRevocationModeIsPlatformOcsp() throws Exception {
final AuthTokenValidationConfiguration configuration = getValidConfiguration();
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java
index 5aa9e0dc..17e681e4 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java
@@ -4,25 +4,35 @@
package eu.webeid.security.validator;
import eu.webeid.security.certificate.CertificateValidator;
+import eu.webeid.security.certificate.SubjectCertificatePolicies;
import eu.webeid.security.testutil.AuthTokenValidators;
import eu.webeid.security.validator.revocationcheck.CertificateRevocationChecker;
import eu.webeid.security.validator.revocationcheck.RevocationMode;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
+import javax.security.auth.x500.X500Principal;
import java.net.URI;
import java.security.cert.PKIXRevocationChecker;
+import java.security.cert.X509Certificate;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
import static eu.webeid.security.testutil.AbstractTestWithValidator.VALID_AUTH_TOKEN;
import static eu.webeid.security.testutil.AbstractTestWithValidator.VALID_CHALLENGE_NONCE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
@@ -137,6 +147,46 @@ void whenPlatformOcspNonceSettingChanges_thenBuiltValidatorsRetainTheirSetting()
}
}
+ @Test
+ void whenAllowedPoliciesAreEmpty_thenBuildUsesEsteidDefaults() throws Exception {
+ final AuthTokenValidationConfiguration builtConfiguration = captureBuiltConfiguration(validBuilder());
+
+ assertThat(builtConfiguration.getAllowedSubjectCertificatePolicies())
+ .isEqualTo(SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES);
+ }
+
+ @Test
+ void whenAllowedPoliciesAreConfigured_thenBuildUsesOnlyConfiguredPolicies() throws Exception {
+ final X500Principal issuer = new X500Principal("CN=Example CA");
+ final ASN1ObjectIdentifier policy = new ASN1ObjectIdentifier("1.2.3");
+
+ final AuthTokenValidationConfiguration builtConfiguration = captureBuiltConfiguration(
+ validBuilder().withAllowedCertificatePolicies(issuer, policy)
+ );
+
+ assertThat(builtConfiguration.getAllowedSubjectCertificatePolicies())
+ .containsOnly(entry(issuer, Set.of(policy)));
+ }
+
+ private static AuthTokenValidationConfiguration captureBuiltConfiguration(
+ AuthTokenValidatorBuilder builder) throws Exception {
+ final AtomicReference capturedConfiguration = new AtomicReference<>();
+ try (MockedConstruction ignored = mockConstruction(
+ AuthTokenValidatorImpl.class,
+ (mock, context) -> capturedConfiguration.set(
+ (AuthTokenValidationConfiguration) context.arguments().get(0)
+ ))) {
+ builder.build();
+ }
+ return capturedConfiguration.get();
+ }
+
+ private static AuthTokenValidatorBuilder validBuilder() {
+ return new AuthTokenValidatorBuilder()
+ .withSiteOrigin(URI.create("https://ria.ee"))
+ .withTrustedCertificateAuthorities(mock(X509Certificate.class));
+ }
+
private static CertificateRevocationChecker getNoopChecker() {
return (subjectCertificate, issuerCertificate) -> List.of();
}
diff --git a/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidatorTest.java b/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidatorTest.java
index 4dd7fe40..8650efd9 100644
--- a/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidatorTest.java
+++ b/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidatorTest.java
@@ -10,12 +10,16 @@
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.Extension;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.security.cert.X509Certificate;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import javax.security.auth.x500.X500Principal;
+
import static eu.webeid.security.testutil.Certificates.getCertificateWithoutCertificatePolicies;
import static eu.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert;
import static org.assertj.core.api.Assertions.assertThatCode;
@@ -28,10 +32,18 @@ class SubjectCertificatePolicyValidatorTest {
private static final ASN1ObjectIdentifier ESTEID2018_POLICY = new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.2.1");
private static final ASN1ObjectIdentifier UNRELATED_POLICY = new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.2.2");
+ private static final X500Principal ESTEID2018_TEST_ISSUER = new X500Principal(
+ "CN=TEST of ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133,O=SK ID Solutions AS,C=EE"
+ );
private final X509Certificate certificate = mock(X509Certificate.class);
private final SubjectCertificatePolicyValidator validator = new SubjectCertificatePolicyValidator(Set.of());
+ @BeforeEach
+ void setUpCertificateIssuer() {
+ when(certificate.getIssuerX500Principal()).thenReturn(new X500Principal("CN=Unconfigured CA"));
+ }
+
@Test
void whenCertificateContainsDisallowedPolicy_thenValidationFails() throws Exception {
final SubjectCertificatePolicyValidator validator = new SubjectCertificatePolicyValidator(List.of(ESTEID2018_POLICY));
@@ -46,6 +58,55 @@ void whenCertificateDoesNotContainDisallowedPolicies_thenValidationSucceeds() th
.doesNotThrowAnyException();
}
+ @Test
+ void whenCertificateContainsAllowedPolicy_thenValidationSucceeds() throws Exception {
+ final SubjectCertificatePolicyValidator validator =
+ new SubjectCertificatePolicyValidator(allowedPolicies(ESTEID2018_POLICY), Set.of());
+ assertThatCode(() -> validator.validateCertificatePolicies(getJaakKristjanEsteid2018Cert()))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void whenCertificateDoesNotContainAllowedPolicy_thenValidationFails() throws Exception {
+ final SubjectCertificatePolicyValidator validator =
+ new SubjectCertificatePolicyValidator(allowedPolicies(UNRELATED_POLICY), Set.of());
+ assertThatExceptionOfType(UserCertificateDisallowedPolicyException.class)
+ .isThrownBy(() -> validator.validateCertificatePolicies(getJaakKristjanEsteid2018Cert()));
+ }
+
+ @Test
+ void whenPolicyIsBothAllowedAndDisallowed_thenAllowListTakesPrecedence() throws Exception {
+ final SubjectCertificatePolicyValidator validator =
+ new SubjectCertificatePolicyValidator(
+ allowedPolicies(ESTEID2018_POLICY),
+ List.of(ESTEID2018_POLICY)
+ );
+ assertThatCode(() -> validator.validateCertificatePolicies(getJaakKristjanEsteid2018Cert()))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void whenIssuerHasNoAllowListEntry_thenValidationFallsBackToDisallowedPolicies() throws Exception {
+ final SubjectCertificatePolicyValidator validator = new SubjectCertificatePolicyValidator(
+ Map.of(new X500Principal("CN=Another CA"), Set.of(ESTEID2018_POLICY)),
+ List.of(ESTEID2018_POLICY)
+ );
+
+ assertThatExceptionOfType(UserCertificateDisallowedPolicyException.class)
+ .isThrownBy(() -> validator.validateCertificatePolicies(getJaakKristjanEsteid2018Cert()));
+ }
+
+ @Test
+ void whenIssuerAllowListIsEmpty_thenValidationFallsBackToDisallowedPolicies() throws Exception {
+ final SubjectCertificatePolicyValidator validator = new SubjectCertificatePolicyValidator(
+ Map.of(ESTEID2018_TEST_ISSUER, Set.of()),
+ List.of(ESTEID2018_POLICY)
+ );
+
+ assertThatExceptionOfType(UserCertificateDisallowedPolicyException.class)
+ .isThrownBy(() -> validator.validateCertificatePolicies(getJaakKristjanEsteid2018Cert()));
+ }
+
@Test
void whenCertificateDoesNotContainCertificatePoliciesExtension_thenValidationSucceeds() throws Exception {
final SubjectCertificatePolicyValidator validator = new SubjectCertificatePolicyValidator(List.of(ESTEID2018_POLICY));
@@ -53,6 +114,16 @@ void whenCertificateDoesNotContainCertificatePoliciesExtension_thenValidationSuc
.doesNotThrowAnyException();
}
+ @Test
+ void whenAllowListIsConfiguredAndCertificatePoliciesExtensionIsMissing_thenValidationFails() throws Exception {
+ when(certificate.getIssuerX500Principal())
+ .thenReturn(ESTEID2018_TEST_ISSUER);
+ final SubjectCertificatePolicyValidator validator =
+ new SubjectCertificatePolicyValidator(allowedPolicies(ESTEID2018_POLICY), Set.of());
+ assertThatExceptionOfType(UserCertificateDisallowedPolicyException.class)
+ .isThrownBy(() -> validator.validateCertificatePolicies(certificate));
+ }
+
@Test
void whenPoliciesHaveInvalidEncoding_thenPreservesParsingFailure() {
when(certificate.getExtensionValue(Extension.certificatePolicies.getId())).thenReturn(new byte[] {4, 2, 4, 5});
@@ -81,4 +152,9 @@ void whenPoliciesHaveWrongAsn1Type_thenPreservesParsingFailure() throws Exceptio
.isInstanceOf(UserCertificateParseException.class)
.hasCauseInstanceOf(IllegalArgumentException.class);
}
+
+ private static Map> allowedPolicies(
+ ASN1ObjectIdentifier... policies) {
+ return Map.of(ESTEID2018_TEST_ISSUER, Set.of(policies));
+ }
}