diff --git a/README.md b/README.md index f4c5105a..fc499db0 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,23 @@ 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. +Use `withAllowedCertificatePolicies(ASN1ObjectIdentifier... policies)` to require one of the configured certificate policies. A non-empty allow-list takes precedence over the deny-list. The deprecated `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` method remains as a compatibility fallback when no allowed policies are configured. Estonian Mobile-ID policies are disallowed by default because smart-card authentication must not accept Mobile-ID certificates. + +For example, restrict authentication to production Estonian and European Union citizen ID cards: + +```java +import static eu.webeid.security.certificate.SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES; + +... + AuthTokenValidator tokenValidator = new AuthTokenValidatorBuilder() + .withSiteOrigin(URI.create("https://example.org")) + .withTrustedCertificateAuthorities(trustedIntermediateCACertificates()) + .withAllowedCertificatePolicies(ESTEID_ID_CARD_POLICIES) + .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/src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java b/src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java index 11ef5442..f7769274 100644 --- a/src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java +++ b/src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java @@ -18,6 +18,36 @@ 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. + * + * @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 ASN1ObjectIdentifier[] ESTEID_ID_CARD_POLICIES = { + // 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"), + // 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..6344ac4d 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java @@ -24,7 +24,9 @@ public final class AuthTokenValidationConfiguration { private URI siteOrigin; private Collection trustedCACertificates = new HashSet<>(); + private Collection allowedSubjectCertificatePolicies = new HashSet<>(); // Don't allow Estonian Mobile-ID policy by default. + @Deprecated(since = "4.0.0") private Collection disallowedSubjectCertificatePolicies = new HashSet<>(Set.of( SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1, SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V2, @@ -43,6 +45,7 @@ public final class AuthTokenValidationConfiguration { private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) { this.siteOrigin = other.siteOrigin; this.trustedCACertificates = Set.copyOf(other.trustedCACertificates); + this.allowedSubjectCertificatePolicies = Set.copyOf(other.allowedSubjectCertificatePolicies); this.disallowedSubjectCertificatePolicies = Set.copyOf(other.disallowedSubjectCertificatePolicies); this.isUserCertificateRevocationCheckEnabled = other.isUserCertificateRevocationCheckEnabled; this.platformOcspNonceEnabled = other.platformOcspNonceEnabled; @@ -63,6 +66,18 @@ Collection getTrustedCACertificates() { return trustedCACertificates; } + public Collection getAllowedSubjectCertificatePolicies() { + return allowedSubjectCertificatePolicies; + } + + void setAllowedSubjectCertificatePolicies(Collection allowedSubjectCertificatePolicies) { + this.allowedSubjectCertificatePolicies = new HashSet<>(allowedSubjectCertificatePolicies); + } + + /** + * @deprecated Configure allowed certificate policies instead. + */ + @Deprecated(since = "4.0.0") public Collection getDisallowedSubjectCertificatePolicies() { return disallowedSubjectCertificatePolicies; } diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java index d072979f..6aa6ae96 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java @@ -12,6 +12,7 @@ import java.net.URI; import java.security.cert.PKIXRevocationChecker; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Collections; /** @@ -60,6 +61,20 @@ public AuthTokenValidatorBuilder withTrustedCertificateAuthorities(X509Certifica return this; } + /** + * Sets the list of allowed user certificate policies. + * When this list is not empty, the user certificate must contain at least one allowed policy and the configured + * disallowed policies are ignored. When this list is empty, validation falls back to the disallowed policy list. + * + * @param policies allowed user certificate policies + * @return the builder instance for method chaining + */ + public AuthTokenValidatorBuilder withAllowedCertificatePolicies(ASN1ObjectIdentifier... policies) { + configuration.setAllowedSubjectCertificatePolicies(Arrays.asList(policies)); + LOG.debug("Allowed subject certificate policies set to {}", configuration.getAllowedSubjectCertificatePolicies()); + return this; + } + /** * 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 @@ -67,7 +82,9 @@ public AuthTokenValidatorBuilder withTrustedCertificateAuthorities(X509Certifica * * @param policies disallowed user certificate policies * @return the builder instance for method chaining + * @deprecated Use {@link #withAllowedCertificatePolicies(ASN1ObjectIdentifier...)} instead. */ + @Deprecated(since = "4.0.0") public AuthTokenValidatorBuilder withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies) { Collections.addAll(configuration.getDisallowedSubjectCertificatePolicies(), policies); LOG.debug("Disallowed subject certificate policies set to {}", configuration.getDisallowedSubjectCertificatePolicies()); 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..3a002ede 100644 --- a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java +++ b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java @@ -17,28 +17,47 @@ import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collection; +import java.util.Set; public final class SubjectCertificatePolicyValidator { private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificatePolicyValidator.class); + private final Collection allowedSubjectCertificatePolicies; private final Collection disallowedSubjectCertificatePolicies; + /** + * @deprecated Configure both allowed and fallback disallowed policies with + * {@link #SubjectCertificatePolicyValidator(Collection, Collection)}. + */ + @Deprecated(since = "4.0.0") public SubjectCertificatePolicyValidator(Collection disallowedSubjectCertificatePolicies) { - this.disallowedSubjectCertificatePolicies = disallowedSubjectCertificatePolicies; + this(Set.of(), disallowedSubjectCertificatePolicies); + } + + public SubjectCertificatePolicyValidator( + Collection allowedSubjectCertificatePolicies, + Collection disallowedSubjectCertificatePolicies) { + this.allowedSubjectCertificatePolicies = Set.copyOf(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 allow-list is not empty. Otherwise, rejects any configured + * disallowed policy present in the certificate policies extension. + * A certificate without the certificate policies extension passes only when the allow-list is empty. * * @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 byte[] extensionValue = subjectCertificate.getExtensionValue(Extension.certificatePolicies.getId()); if (extensionValue == null) { + if (!allowedSubjectCertificatePolicies.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; } @@ -49,9 +68,18 @@ public void validateCertificatePolicies(X509Certificate subjectCertificate) thro if (policies == null) { throw new IllegalArgumentException("Certificate policies extension is empty"); } + if (!allowedSubjectCertificatePolicies.isEmpty()) { + if (Arrays.stream(policies.getPolicyInformation()) + .noneMatch(policyInformation -> + allowedSubjectCertificatePolicies.contains(policyInformation.getPolicyIdentifier()))) { + throw new UserCertificateDisallowedPolicyException(); + } + LOG.debug("User certificate contains an allowed policy."); + return; + } if (Arrays.stream(policies.getPolicyInformation()) .anyMatch(policyInformation -> - disallowedSubjectCertificatePolicies.contains(policyInformation.getPolicyIdentifier()))) { + disallowedSubjectCertificatePolicies.contains(policyInformation.getPolicyIdentifier()))) { throw new UserCertificateDisallowedPolicyException(); } } catch (IOException | IllegalArgumentException e) { 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..2b9e4542 --- /dev/null +++ b/src/test/java/eu/webeid/security/certificate/SubjectCertificatePoliciesTest.java @@ -0,0 +1,22 @@ +// 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 static org.assertj.core.api.Assertions.assertThat; + +class SubjectCertificatePoliciesTest { + + @Test + void esteidIdCardPoliciesContainOnlyProductionIdCardPolicies() { + assertThat(SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES).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"), + 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/validator/AuthTokenCertificateTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java index c5043a2d..7ea65123 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java @@ -18,6 +18,7 @@ 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; @@ -35,6 +36,10 @@ 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 String AUTH_TOKEN = "{\"algorithm\":\"ES384\"," + "\"unverifiedCertificate\":\"X5C\"," + "\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," + @@ -178,6 +183,30 @@ void whenCertificatePolicyIsDisallowed_thenValidationFails() throws Exception { .isInstanceOf(UserCertificateDisallowedPolicyException.class); } + @Test + void whenCertificatePolicyIsAllowed_thenValidationSucceeds() throws Exception { + final AuthTokenValidator validatorWithAllowedESTEIDPolicy = AuthTokenValidators + .getDefaultAuthTokenValidatorBuilder() + .withAllowedCertificatePolicies(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(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); diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenValidationConfigurationTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenValidationConfigurationTest.java index f70eae22..e6d111b0 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenValidationConfigurationTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenValidationConfigurationTest.java @@ -25,10 +25,12 @@ 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 java.net.URI; import java.security.cert.PKIXRevocationChecker; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -36,6 +38,30 @@ class AuthTokenValidationConfigurationTest { + private static final ASN1ObjectIdentifier ALLOWED_POLICY = new ASN1ObjectIdentifier("1.2.3"); + + @Test + void copyRetainsAllowedPoliciesIndependently() { + final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration(); + configuration.setAllowedSubjectCertificatePolicies(List.of(ALLOWED_POLICY)); + + final AuthTokenValidationConfiguration copy = configuration.copy(); + configuration.getAllowedSubjectCertificatePolicies().clear(); + + assertThat(copy.getAllowedSubjectCertificatePolicies()).containsExactly(ALLOWED_POLICY); + } + + @Test + void setAllowedPoliciesCopiesInputCollection() { + final List policies = new ArrayList<>(List.of(ALLOWED_POLICY)); + final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration(); + + configuration.setAllowedSubjectCertificatePolicies(policies); + policies.clear(); + + assertThat(configuration.getAllowedSubjectCertificatePolicies()).containsExactly(ALLOWED_POLICY); + } + @Test void whenNoCustomCheckerProvided_thenRevocationModeIsPlatformOcsp() throws Exception { final AuthTokenValidationConfiguration configuration = getValidConfiguration(); 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..e4557e63 100644 --- a/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidatorTest.java +++ b/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidatorTest.java @@ -46,6 +46,30 @@ void whenCertificateDoesNotContainDisallowedPolicies_thenValidationSucceeds() th .doesNotThrowAnyException(); } + @Test + void whenCertificateContainsAllowedPolicy_thenValidationSucceeds() throws Exception { + final SubjectCertificatePolicyValidator validator = + new SubjectCertificatePolicyValidator(List.of(ESTEID2018_POLICY), Set.of()); + assertThatCode(() -> validator.validateCertificatePolicies(getJaakKristjanEsteid2018Cert())) + .doesNotThrowAnyException(); + } + + @Test + void whenCertificateDoesNotContainAllowedPolicy_thenValidationFails() throws Exception { + final SubjectCertificatePolicyValidator validator = + new SubjectCertificatePolicyValidator(List.of(UNRELATED_POLICY), Set.of()); + assertThatExceptionOfType(UserCertificateDisallowedPolicyException.class) + .isThrownBy(() -> validator.validateCertificatePolicies(getJaakKristjanEsteid2018Cert())); + } + + @Test + void whenPolicyIsBothAllowedAndDisallowed_thenAllowListTakesPrecedence() throws Exception { + final SubjectCertificatePolicyValidator validator = + new SubjectCertificatePolicyValidator(List.of(ESTEID2018_POLICY), List.of(ESTEID2018_POLICY)); + assertThatCode(() -> validator.validateCertificatePolicies(getJaakKristjanEsteid2018Cert())) + .doesNotThrowAnyException(); + } + @Test void whenCertificateDoesNotContainCertificatePoliciesExtension_thenValidationSucceeds() throws Exception { final SubjectCertificatePolicyValidator validator = new SubjectCertificatePolicyValidator(List.of(ESTEID2018_POLICY)); @@ -53,6 +77,14 @@ void whenCertificateDoesNotContainCertificatePoliciesExtension_thenValidationSuc .doesNotThrowAnyException(); } + @Test + void whenAllowListIsConfiguredAndCertificatePoliciesExtensionIsMissing_thenValidationFails() throws Exception { + final SubjectCertificatePolicyValidator validator = + new SubjectCertificatePolicyValidator(List.of(ESTEID2018_POLICY), Set.of()); + assertThatExceptionOfType(UserCertificateDisallowedPolicyException.class) + .isThrownBy(() -> validator.validateCertificatePolicies(getCertificateWithoutCertificatePolicies())); + } + @Test void whenPoliciesHaveInvalidEncoding_thenPreservesParsingFailure() { when(certificate.getExtensionValue(Extension.certificatePolicies.getId())).thenReturn(new byte[] {4, 2, 4, 5});