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
46 changes: 43 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,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.

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

Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 <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 Map<X500Principal, Set<ASN1ObjectIdentifier>> 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");
}
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 @@ -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;

/**
Expand All @@ -24,13 +27,9 @@

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

Check warning on line 32 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=AaFEHzSYSBXvSp8l_KBo&open=AaFEHzSYSBXvSp8l_KBo&pullRequest=142

Check warning on line 32 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=AaFEHzSYSBXvSp8l_KBn&open=AaFEHzSYSBXvSp8l_KBn&pullRequest=142
private boolean isUserCertificateRevocationCheckEnabled = true;
private boolean platformOcspNonceEnabled = true;
private CertificateRevocationChecker certificateRevocationChecker;
Expand All @@ -43,6 +42,8 @@
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;
Expand All @@ -63,10 +64,41 @@
return trustedCACertificates;
}

public Map<X500Principal, Set<ASN1ObjectIdentifier>> getAllowedSubjectCertificatePolicies() {
return allowedSubjectCertificatePolicies;
}

void setAllowedSubjectCertificatePolicies(
X500Principal issuer, Collection<ASN1ObjectIdentifier> allowedSubjectCertificatePolicies) {
Objects.requireNonNull(issuer, "Issuer must not be null");
final Set<ASN1ObjectIdentifier> 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<ASN1ObjectIdentifier> getDisallowedSubjectCertificatePolicies() {

Check warning on line 89 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;
}

/**
* @deprecated Configure allowed certificate policies instead.
*/
@Deprecated(since = "4.0.0")
void setDisallowedSubjectCertificatePolicies(

Check warning on line 97 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=AaFEJGIza2PiRfMYyz28&open=AaFEJGIza2PiRfMYyz28&pullRequest=142
Collection<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies) {
this.disallowedSubjectCertificatePolicies = new HashSet<>(disallowedSubjectCertificatePolicies);
}

boolean isUserCertificateRevocationCheckEnabled() {
return isUserCertificateRevocationCheckEnabled;
}
Expand Down Expand Up @@ -120,6 +152,16 @@
return new AuthTokenValidationConfiguration(this);
}

private static Map<X500Principal, Set<ASN1ObjectIdentifier>> copyAllowedSubjectCertificatePolicies(
Map<X500Principal, ? extends Collection<ASN1ObjectIdentifier>> policiesByIssuer) {
final Map<X500Principal, Set<ASN1ObjectIdentifier>> 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 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/origin">MDN</a>,
* in the form of {@code <scheme> "://" <hostname> [ ":" <port> ]}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}

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