Skip to content

Commit 52fa2f6

Browse files
committed
feat: change allowed issuers to map and only apply when issuer is present as map key
WE2-1308 Signed-off-by: Sven Mitt <svenzik@users.noreply.github.com>
1 parent d55854e commit 52fa2f6

12 files changed

Lines changed: 318 additions & 66 deletions

File tree

‎README.md‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -323,22 +323,29 @@ toTitleCase(CertificateData.getSubjectSurname(userCertificate).orElseThrow()); /
323323

324324
The default validator uses the platform OCSP implementation to check certificate revocation. A revoked certificate or an unsuccessful revocation check causes authentication to fail.
325325

326-
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.
326+
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 disallows Estonian Mobile-ID policies by default. Calling `withAllowedCertificatePolicies(issuer)` without policy OIDs removes that issuer's entry. If no entries remain, the Estonian defaults are used.
327327

328-
For example, restrict authentication to production Estonian and European Union citizen ID cards:
328+
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.
329329

330330
```java
331-
import static eu.webeid.security.certificate.SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES;
332-
333-
...
334-
AuthTokenValidator tokenValidator = new AuthTokenValidatorBuilder()
335-
.withSiteOrigin(URI.create("https://example.org"))
336-
.withTrustedCertificateAuthorities(trustedIntermediateCACertificates())
337-
.withAllowedCertificatePolicies(ESTEID_ID_CARD_POLICIES)
338-
.build();
339-
...
331+
AuthTokenValidator tokenValidator = new AuthTokenValidatorBuilder()
332+
.withSiteOrigin(URI.create("https://example.org"))
333+
.withTrustedCertificateAuthorities(trustedIntermediateCACertificates())
334+
.withAllowedCertificatePolicies(
335+
new X500Principal("CN=ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133,O=SK ID Solutions AS,C=EE"),
336+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.1"),
337+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.2")
338+
)
339+
.withAllowedCertificatePolicies(
340+
new X500Principal("C=EE,O=Zetes Estonia OÜ,2.5.4.97=#0c0e4e545245452d3137303636303439,CN=ESTEID2025"),
341+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.1"),
342+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.2")
343+
)
344+
.build();
340345
```
341346

347+
For another country, add each supported issuer and its policy OIDs to the same builder chain and provide the corresponding trusted intermediate CAs.
348+
342349
See the [certificate policy allow-list guide](readme-policy.md) for the Estonian ID-card whitelist and its rationale.
343350

344351
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.

‎example/src/test/java/eu/webeid/example/config/TestValidatorConfiguration.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import eu.webeid.security.exceptions.JceException;
77
import eu.webeid.security.validator.AuthTokenValidator;
88
import eu.webeid.security.validator.AuthTokenValidatorBuilder;
9+
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
910
import org.springframework.boot.test.context.TestConfiguration;
1011
import org.springframework.context.annotation.Bean;
1112
import org.springframework.context.annotation.Primary;
1213

14+
import javax.security.auth.x500.X500Principal;
1315
import java.net.URI;
1416
import java.util.List;
1517

@@ -30,6 +32,13 @@ AuthTokenValidator validatorWithoutOcspCall(ValidationConfiguration validationCo
3032
.withSiteOrigin(URI.create(yamlConfig.getLocalOrigin()))
3133
.withTrustedCertificateAuthorities(validationConfiguration.loadTrustedCACertificatesFromCerFiles())
3234
.withTrustedCertificateAuthorities(validationConfiguration.loadTrustedCACertificatesFromTrustStore(yamlConfig))
35+
.withAllowedCertificatePolicies(
36+
new X500Principal(
37+
"CN=TEST of ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133," +
38+
"O=SK ID Solutions AS,C=EE"
39+
),
40+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.2.1")
41+
)
3342
// Do not call the real OCSP service in tests.
3443
.withCertificateRevocationChecker((subjectCertificate, issuerCertificate) -> List.of())
3544
.build();

‎src/main/java/eu/webeid/security/certificate/SubjectCertificatePolicies.java‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
77

8+
import javax.security.auth.x500.X500Principal;
9+
import java.util.Map;
10+
import java.util.Set;
11+
812
public final class SubjectCertificatePolicies {
913

1014
private static final String ESTEID_SK_2015_MOBILE_ID_POLICY_PREFIX = "1.3.6.1.4.1.10015.1.3";
@@ -20,7 +24,7 @@ public final class SubjectCertificatePolicies {
2024

2125
/**
2226
* Production document policies for Estonian and European Union citizen ID cards issued under the ESTEID2018
23-
* and eID certificate-policy generations.
27+
* and eID certificate-policy generations, grouped by issuer distinguished name.
2428
*
2529
* @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/">
2630
* ID.ee certificate policy index for Estonian identity documents</a>
@@ -39,14 +43,22 @@ public final class SubjectCertificatePolicies {
3943
* @see <a href="https://repository.eidpki.ee/static/documents/CertificateProfiles-20260520.pdf">
4044
* ESTEID2025 certificate profile</a>
4145
*/
42-
public static final ASN1ObjectIdentifier[] ESTEID_ID_CARD_POLICIES = {
43-
// ESTEID2018 / IDEMIA generation
44-
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.1"),
45-
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.2"),
46-
// eID / Thales generation
47-
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.1"),
48-
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.2")
49-
};
46+
public static final Map<X500Principal, Set<ASN1ObjectIdentifier>> ESTEID_ID_CARD_POLICIES = Map.of(
47+
new X500Principal(
48+
"CN=ESTEID2018,2.5.4.97=#0c0e4e545245452d3130373437303133,O=SK ID Solutions AS,C=EE"
49+
), Set.of(
50+
// ESTEID2018 / IDEMIA generation
51+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.1"),
52+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.1.1.2")
53+
),
54+
new X500Principal(
55+
"C=EE,O=Zetes Estonia OÜ,2.5.4.97=#0c0e4e545245452d3137303636303439,CN=ESTEID2025"
56+
), Set.of(
57+
// eID / Thales generation
58+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.1"),
59+
new ASN1ObjectIdentifier("1.3.6.1.4.1.51361.2.1.2")
60+
)
61+
);
5062

5163
private SubjectCertificatePolicies() {
5264
throw new IllegalStateException("Constants class");

‎src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
import eu.webeid.security.validator.revocationcheck.RevocationMode;
99
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
1010

11+
import javax.security.auth.x500.X500Principal;
1112
import java.net.MalformedURLException;
1213
import java.net.URI;
1314
import java.net.URISyntaxException;
1415
import java.security.cert.PKIXRevocationChecker;
1516
import java.security.cert.X509Certificate;
1617
import java.util.Collection;
18+
import java.util.HashMap;
1719
import java.util.HashSet;
20+
import java.util.Map;
21+
import java.util.Objects;
1822
import java.util.Set;
1923

2024
/**
@@ -24,7 +28,7 @@ public final class AuthTokenValidationConfiguration {
2428

2529
private URI siteOrigin;
2630
private Collection<X509Certificate> trustedCACertificates = new HashSet<>();
27-
private Collection<ASN1ObjectIdentifier> allowedSubjectCertificatePolicies = new HashSet<>();
31+
private Map<X500Principal, Set<ASN1ObjectIdentifier>> allowedSubjectCertificatePolicies = new HashMap<>();
2832
// Don't allow Estonian Mobile-ID policy by default.
2933
@Deprecated(since = "4.0.0")
3034
private Collection<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies = new HashSet<>(Set.of(
@@ -45,7 +49,8 @@ public final class AuthTokenValidationConfiguration {
4549
private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) {
4650
this.siteOrigin = other.siteOrigin;
4751
this.trustedCACertificates = Set.copyOf(other.trustedCACertificates);
48-
this.allowedSubjectCertificatePolicies = Set.copyOf(other.allowedSubjectCertificatePolicies);
52+
this.allowedSubjectCertificatePolicies =
53+
copyAllowedSubjectCertificatePolicies(other.allowedSubjectCertificatePolicies);
4954
this.disallowedSubjectCertificatePolicies = Set.copyOf(other.disallowedSubjectCertificatePolicies);
5055
this.isUserCertificateRevocationCheckEnabled = other.isUserCertificateRevocationCheckEnabled;
5156
this.platformOcspNonceEnabled = other.platformOcspNonceEnabled;
@@ -66,12 +71,22 @@ Collection<X509Certificate> getTrustedCACertificates() {
6671
return trustedCACertificates;
6772
}
6873

69-
public Collection<ASN1ObjectIdentifier> getAllowedSubjectCertificatePolicies() {
74+
public Map<X500Principal, Set<ASN1ObjectIdentifier>> getAllowedSubjectCertificatePolicies() {
7075
return allowedSubjectCertificatePolicies;
7176
}
7277

73-
void setAllowedSubjectCertificatePolicies(Collection<ASN1ObjectIdentifier> allowedSubjectCertificatePolicies) {
74-
this.allowedSubjectCertificatePolicies = new HashSet<>(allowedSubjectCertificatePolicies);
78+
void setAllowedSubjectCertificatePolicies(
79+
X500Principal issuer, Collection<ASN1ObjectIdentifier> allowedSubjectCertificatePolicies) {
80+
Objects.requireNonNull(issuer, "Issuer must not be null");
81+
final Set<ASN1ObjectIdentifier> policies = Set.copyOf(Objects.requireNonNull(
82+
allowedSubjectCertificatePolicies,
83+
"Allowed certificate policies must not be null"
84+
));
85+
if (policies.isEmpty()) {
86+
this.allowedSubjectCertificatePolicies.remove(issuer);
87+
} else {
88+
this.allowedSubjectCertificatePolicies.put(issuer, policies);
89+
}
7590
}
7691

7792
/**
@@ -135,6 +150,16 @@ AuthTokenValidationConfiguration copy() {
135150
return new AuthTokenValidationConfiguration(this);
136151
}
137152

153+
private static Map<X500Principal, Set<ASN1ObjectIdentifier>> copyAllowedSubjectCertificatePolicies(
154+
Map<X500Principal, ? extends Collection<ASN1ObjectIdentifier>> policiesByIssuer) {
155+
final Map<X500Principal, Set<ASN1ObjectIdentifier>> copy = new HashMap<>();
156+
policiesByIssuer.forEach((issuer, policies) -> copy.put(
157+
Objects.requireNonNull(issuer, "Issuer must not be null"),
158+
Set.copyOf(Objects.requireNonNull(policies, "Allowed certificate policies must not be null"))
159+
));
160+
return copy;
161+
}
162+
138163
/**
139164
* 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>,
140165
* in the form of {@code <scheme> "://" <hostname> [ ":" <port> ]}.

‎src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33

44
package eu.webeid.security.validator;
55

6+
import eu.webeid.security.certificate.SubjectCertificatePolicies;
67
import eu.webeid.security.exceptions.JceException;
78
import eu.webeid.security.validator.revocationcheck.CertificateRevocationChecker;
89
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
1112

13+
import javax.security.auth.x500.X500Principal;
1214
import java.net.URI;
1315
import java.security.cert.PKIXRevocationChecker;
1416
import java.security.cert.X509Certificate;
@@ -62,15 +64,19 @@ public AuthTokenValidatorBuilder withTrustedCertificateAuthorities(X509Certifica
6264
}
6365

6466
/**
65-
* Sets the list of allowed user certificate policies.
66-
* When this list is not empty, the user certificate must contain at least one allowed policy and the configured
67-
* disallowed policies are ignored. When this list is empty, validation falls back to the disallowed policy list.
67+
* Adds or replaces the allowed user certificate policies for the given issuer. When no policies are configured,
68+
* {@link SubjectCertificatePolicies#ESTEID_ID_CARD_POLICIES} is used by default.
69+
* When the subject certificate's issuer matches a configured issuer, the certificate must contain at least one
70+
* of that issuer's allowed policies and the configured disallowed policies are ignored. Otherwise, validation
71+
* falls back to the disallowed policy list. Passing no policies removes the issuer's allow-list entry.
6872
*
69-
* @param policies allowed user certificate policies
73+
* @param issuer exact subject certificate issuer distinguished name
74+
* @param policies allowed user certificate policies for the issuer
7075
* @return the builder instance for method chaining
7176
*/
72-
public AuthTokenValidatorBuilder withAllowedCertificatePolicies(ASN1ObjectIdentifier... policies) {
73-
configuration.setAllowedSubjectCertificatePolicies(Arrays.asList(policies));
77+
public AuthTokenValidatorBuilder withAllowedCertificatePolicies(
78+
X500Principal issuer, ASN1ObjectIdentifier... policies) {
79+
configuration.setAllowedSubjectCertificatePolicies(issuer, Arrays.asList(policies));
7480
LOG.debug("Allowed subject certificate policies set to {}", configuration.getAllowedSubjectCertificatePolicies());
7581
return this;
7682
}
@@ -82,7 +88,7 @@ public AuthTokenValidatorBuilder withAllowedCertificatePolicies(ASN1ObjectIdenti
8288
*
8389
* @param policies disallowed user certificate policies
8490
* @return the builder instance for method chaining
85-
* @deprecated Use {@link #withAllowedCertificatePolicies(ASN1ObjectIdentifier...)} instead.
91+
* @deprecated Use {@link #withAllowedCertificatePolicies(X500Principal, ASN1ObjectIdentifier...)} instead.
8692
*/
8793
@Deprecated(since = "4.0.0")
8894
public AuthTokenValidatorBuilder withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies) {
@@ -164,8 +170,13 @@ public AuthTokenValidatorBuilder withPKIXRevocationChecker(PKIXRevocationChecker
164170
* @throws JceException when JCE configuration is invalid
165171
*/
166172
public AuthTokenValidator build() throws IllegalArgumentException, JceException {
167-
configuration.validate();
168-
return new AuthTokenValidatorImpl(configuration);
173+
final AuthTokenValidationConfiguration configurationSnapshot = configuration.copy();
174+
if (configurationSnapshot.getAllowedSubjectCertificatePolicies().isEmpty()) {
175+
configurationSnapshot.getAllowedSubjectCertificatePolicies()
176+
.putAll(SubjectCertificatePolicies.ESTEID_ID_CARD_POLICIES);
177+
}
178+
configurationSnapshot.validate();
179+
return new AuthTokenValidatorImpl(configurationSnapshot);
169180
}
170181

171182
}

0 commit comments

Comments
 (0)