Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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"
);
}
...
```
Expand Down Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@
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 <a href="https://www.id.ee/en/article/certificate-policy-for-identity-card-digital-identity-card-residence-permit-card-and-diplomatic-identity-card-2/">
* ID.ee certificate policy index for Estonian identity documents</a>
* @see <a href="https://www.id.ee/wp-content/uploads/2026/08/cp_esteid_v4.1_17.08.2026_allkirjastatud.pdf">
* IDEMIA-generation certificate policy</a>
* @see <a href="https://www.skidsolutions.eu/resources/certificate-policies/">
* SK ID Solutions certificate policies</a>
* @see <a href="https://www.skidsolutions.eu/resources/profiles/">
* SK ID Solutions certificate profiles</a>
* @see <a href="https://www.skidsolutions.eu/upload/files/SK-CPR-ESTEID2018-EN-v1_3_20220217.pdf">
* ESTEID2018 certificate profile</a>
* @see <a href="https://www.id.ee/wp-content/uploads/2026/06/eid-cp-v-2.0_04.06.2026_allkirjastatud.pdf">
* Thales-generation certificate policy</a>
* @see <a href="https://repository.eidpki.ee/repository/">
* Zetes Estonia trust-service repository</a>
* @see <a href="https://repository.eidpki.ee/static/documents/CertificateProfiles-20260520.pdf">
* ESTEID2025 certificate profile</a>
*/
public static final ASN1ObjectIdentifier[] ESTEID_ID_CARD_POLICIES = {

Check warning on line 42 in src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make this member "protected".

See more on https://sonarcloud.io/project/issues?id=web-eid_web-eid-authtoken-validation-java&issues=AaE7eGjI2rqbzzSP7W9W&open=AaE7eGjI2rqbzzSP7W9W&pullRequest=142
// 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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

private URI siteOrigin;
private Collection<X509Certificate> trustedCACertificates = new HashSet<>();
private Collection<ASN1ObjectIdentifier> allowedSubjectCertificatePolicies = new HashSet<>();
// Don't allow Estonian Mobile-ID policy by default.
@Deprecated(since = "4.0.0")
private Collection<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies = new HashSet<>(Set.of(

Check warning on line 30 in src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add the missing @deprecated Javadoc tag.

See more on https://sonarcloud.io/project/issues?id=web-eid_web-eid-authtoken-validation-java&issues=AaE7eGlf2rqbzzSP7W9Y&open=AaE7eGlf2rqbzzSP7W9Y&pullRequest=142

Check warning on line 30 in src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=web-eid_web-eid-authtoken-validation-java&issues=AaE7eGlf2rqbzzSP7W9X&open=AaE7eGlf2rqbzzSP7W9X&pullRequest=142
SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1,
SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V2,
SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V3,
Expand All @@ -43,6 +45,7 @@
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;
Expand All @@ -63,7 +66,19 @@
return trustedCACertificates;
}

public Collection<ASN1ObjectIdentifier> getAllowedSubjectCertificatePolicies() {
return allowedSubjectCertificatePolicies;
}

void setAllowedSubjectCertificatePolicies(Collection<ASN1ObjectIdentifier> allowedSubjectCertificatePolicies) {
this.allowedSubjectCertificatePolicies = new HashSet<>(allowedSubjectCertificatePolicies);
}

/**
* @deprecated Configure allowed certificate policies instead.
*/
@Deprecated(since = "4.0.0")
public Collection<ASN1ObjectIdentifier> getDisallowedSubjectCertificatePolicies() {

Check warning on line 81 in src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=web-eid_web-eid-authtoken-validation-java&issues=AaE7eGlf2rqbzzSP7W9Z&open=AaE7eGlf2rqbzzSP7W9Z&pullRequest=142
return disallowedSubjectCertificatePolicies;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -60,14 +61,30 @@ 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
* present in this list.
*
* @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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@
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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ASN1ObjectIdentifier> allowedSubjectCertificatePolicies;
private final Collection<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies;

/**
* @deprecated Configure both allowed and fallback disallowed policies with
* {@link #SubjectCertificatePolicyValidator(Collection, Collection)}.
*/
@Deprecated(since = "4.0.0")
public SubjectCertificatePolicyValidator(Collection<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies) {

Check warning on line 34 in src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=web-eid_web-eid-authtoken-validation-java&issues=AaE7eGl12rqbzzSP7W9a&open=AaE7eGl12rqbzzSP7W9a&pullRequest=142
this.disallowedSubjectCertificatePolicies = disallowedSubjectCertificatePolicies;
this(Set.of(), disallowedSubjectCertificatePolicies);
}

public SubjectCertificatePolicyValidator(
Collection<ASN1ObjectIdentifier> allowedSubjectCertificatePolicies,
Collection<ASN1ObjectIdentifier> 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;
}
Expand All @@ -49,9 +68,18 @@
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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\"," +
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading