Skip to content

Commit d8d246d

Browse files
committed
refactor(SubjectCertificateTrustedValidator): use standard JCE PKIX classes for certificate trust validation (#5)
Signed-off-by: Mart Somermaa <mrts@users.noreply.github.com>
1 parent 02c7887 commit d8d246d

14 files changed

+171
-71
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.webeid.security.exceptions;
2+
3+
import java.security.GeneralSecurityException;
4+
5+
public class JceException extends TokenValidationException {
6+
public JceException(GeneralSecurityException e) {
7+
super("Java Cryptography Extension loading or configuration failed", e);
8+
}
9+
}

src/main/java/org/webeid/security/validator/AuthTokenValidationConfiguration.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ final class AuthTokenValidationConfiguration {
4646

4747
private URI siteOrigin;
4848
private Cache<String, LocalDateTime> nonceCache;
49-
private Collection<X509Certificate> trustedCACertificates = new HashSet<>();
49+
private Collection<X509Certificate> trustedRootCACertificates = new HashSet<>();
5050
private boolean isUserCertificateRevocationCheckWithOcspEnabled = true;
5151
private Duration ocspRequestTimeout = Duration.ofSeconds(5);
5252
private Duration allowedClientClockSkew = Duration.ofMinutes(3);
@@ -63,7 +63,7 @@ final class AuthTokenValidationConfiguration {
6363
private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) {
6464
this.siteOrigin = other.siteOrigin;
6565
this.nonceCache = other.nonceCache;
66-
this.trustedCACertificates = new HashSet<>(other.trustedCACertificates);
66+
this.trustedRootCACertificates = new HashSet<>(other.trustedRootCACertificates);
6767
this.isUserCertificateRevocationCheckWithOcspEnabled = other.isUserCertificateRevocationCheckWithOcspEnabled;
6868
this.ocspRequestTimeout = other.ocspRequestTimeout;
6969
this.allowedClientClockSkew = other.allowedClientClockSkew;
@@ -89,8 +89,8 @@ Cache<String, LocalDateTime> getNonceCache() {
8989
return nonceCache;
9090
}
9191

92-
Collection<X509Certificate> getTrustedCACertificates() {
93-
return trustedCACertificates;
92+
Collection<X509Certificate> getTrustedRootCACertificates() {
93+
return trustedRootCACertificates;
9494
}
9595

9696
boolean isUserCertificateRevocationCheckWithOcspEnabled() {
@@ -148,8 +148,8 @@ void validate() {
148148
Objects.requireNonNull(siteOrigin, "Origin URI must not be null");
149149
OriginValidator.validateIsOriginURL(siteOrigin);
150150
Objects.requireNonNull(nonceCache, "Nonce cache must not be null");
151-
if (trustedCACertificates.isEmpty()) {
152-
throw new IllegalArgumentException("At least one trusted certificate authority must be provided");
151+
if (trustedRootCACertificates.isEmpty()) {
152+
throw new IllegalArgumentException("At least one trusted root certificate authority must be provided");
153153
}
154154
requirePositiveDuration(ocspRequestTimeout, "OCSP request timeout");
155155
requirePositiveDuration(allowedClientClockSkew, "Allowed client clock skew");

src/main/java/org/webeid/security/validator/AuthTokenValidatorBuilder.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
2626
import org.slf4j.Logger;
2727
import org.slf4j.LoggerFactory;
28+
import org.webeid.security.exceptions.JceException;
2829

2930
import javax.cache.Cache;
3031
import java.net.URI;
@@ -72,18 +73,18 @@ public AuthTokenValidatorBuilder withNonceCache(Cache<String, LocalDateTime> cac
7273
}
7374

7475
/**
75-
* Sets the list of trusted user certificate Certificate Authorities.
76-
* In order for the user certificate to be considered valid, the issuer of the user certificate
77-
* must be present in this list.
76+
* Sets the list of trusted user certificate root Certificate Authorities.
77+
* In order for the user certificate to be considered valid, the root certificate of the issuer
78+
* of the user certificate must be present in this list.
7879
* <p>
79-
* At least one trusted Certificate Authority must be provided as mandatory configuration parameter.
80+
* At least one trusted root Certificate Authority must be provided as mandatory configuration parameter.
8081
*
81-
* @param certificates trusted Certificate Authority certificates
82+
* @param certificates trusted root Certificate Authority certificates
8283
* @return the builder instance for method chaining
8384
*/
84-
public AuthTokenValidatorBuilder withTrustedCertificateAuthorities(X509Certificate... certificates) {
85-
Collections.addAll(configuration.getTrustedCACertificates(), certificates);
86-
LOG.debug("Trusted certificate authorities set to {}", configuration.getTrustedCACertificates());
85+
public AuthTokenValidatorBuilder withTrustedRootCertificateAuthorities(X509Certificate... certificates) {
86+
Collections.addAll(configuration.getTrustedRootCACertificates(), certificates);
87+
LOG.debug("Trusted root certificate authorities set to {}", configuration.getTrustedRootCACertificates());
8788
return this;
8889
}
8990

@@ -128,6 +129,19 @@ public AuthTokenValidatorBuilder withOcspRequestTimeout(Duration ocspRequestTime
128129
return this;
129130
}
130131

132+
/**
133+
* Sets the list of OCSP URLs for which the nonce protocol extension will be disabled.
134+
* The OCSP URL is extracted from the user certificate and some OCSP services don't support the nonce extension.
135+
*
136+
* @param urls OCSP URLs for which the nonce protocol extension will be disabled
137+
* @return the builder instance for method chaining
138+
*/
139+
public AuthTokenValidatorBuilder withNonceDisabledOcspUrls(URI... urls) {
140+
Collections.addAll(configuration.getNonceDisabledOcspUrls(), urls);
141+
LOG.debug("OCSP URLs for which the nonce protocol extension is disabled set to {}", configuration.getNonceDisabledOcspUrls());
142+
return this;
143+
}
144+
131145
/**
132146
* Sets the tolerated clock skew of the client computer when verifying the token expiration field {@code exp}.
133147
* <p>
@@ -162,8 +176,9 @@ public AuthTokenValidatorBuilder withSiteCertificateSha256Fingerprint(String cer
162176
* @return the configured authentication token validator object
163177
* @throws NullPointerException when required parameters are null
164178
* @throws IllegalArgumentException when any parameter is invalid
179+
* @throws RuntimeException when JCE configuration is invalid
165180
*/
166-
public AuthTokenValidator build() throws NullPointerException, IllegalArgumentException {
181+
public AuthTokenValidator build() throws NullPointerException, IllegalArgumentException, JceException {
167182
configuration.validate();
168183
return new AuthTokenValidatorImpl(configuration);
169184
}

src/main/java/org/webeid/security/validator/AuthTokenValidatorImpl.java

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,22 @@
2626
import okhttp3.OkHttpClient;
2727
import org.slf4j.Logger;
2828
import org.slf4j.LoggerFactory;
29+
import org.webeid.security.exceptions.JceException;
2930
import org.webeid.security.exceptions.TokenParseException;
3031
import org.webeid.security.exceptions.TokenValidationException;
3132
import org.webeid.security.validator.validators.*;
3233

34+
import java.security.GeneralSecurityException;
35+
import java.security.cert.CertStore;
36+
import java.security.cert.CollectionCertStoreParameters;
37+
import java.security.cert.TrustAnchor;
3338
import java.security.cert.X509Certificate;
39+
import java.util.Set;
3440
import java.util.function.Supplier;
41+
import java.util.stream.Collectors;
3542

3643
/**
37-
* Provides default implementation of {@link AuthTokenValidator}.
44+
* Provides the default implementation of {@link AuthTokenValidator}.
3845
*/
3946
final class AuthTokenValidatorImpl implements AuthTokenValidator {
4047

@@ -51,19 +58,19 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
5158
private final Supplier<OkHttpClient> httpClientSupplier;
5259
private final ValidatorBatch simpleSubjectCertificateValidators;
5360
private final ValidatorBatch tokenBodyValidators;
61+
private final Set<TrustAnchor> trustedRootCACertificateAnchors;
62+
private final CertStore trustedRootCACertificateCertStore;
5463

5564
/**
5665
* @param configuration configuration parameters for the token validator
5766
*/
58-
AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration) {
67+
AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration) throws JceException {
5968
// Copy the configuration object to make AuthTokenValidatorImpl immutable and thread-safe.
6069
this.configuration = configuration.copy();
61-
/*
62-
* Lazy initialization, avoid constructing the OkHttpClient object when certificate revocation check is not enabled.
63-
* Returns a supplier which caches the instance retrieved during the first call to get() and returns
64-
* that value on subsequent calls to get(). The returned supplier is thread-safe.
65-
* The OkHttpClient build() method will be invoked at most once.
66-
*/
70+
// Lazy initialization, avoid constructing the OkHttpClient object when certificate revocation check is not enabled.
71+
// Returns a supplier which caches the instance retrieved during the first call to get() and returns
72+
// that value on subsequent calls to get(). The returned supplier is thread-safe.
73+
// The OkHttpClient build() method will be invoked at most once.
6774
this.httpClientSupplier = Suppliers.memoize(() -> new OkHttpClient.Builder()
6875
.connectTimeout(configuration.getOcspRequestTimeout())
6976
.callTimeout(configuration.getOcspRequestTimeout())
@@ -82,6 +89,20 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
8289
new SiteCertificateFingerprintValidator(configuration.getSiteCertificateSha256Fingerprint())::validateSiteCertificateFingerprint
8390
);
8491

92+
// Create and cache trusted root CA certificate JCA objects for SubjectCertificateTrustedValidator.
93+
trustedRootCACertificateAnchors = configuration.getTrustedRootCACertificates()
94+
.stream()
95+
.map(cert -> new TrustAnchor(cert, null))
96+
.collect(Collectors.toSet());
97+
try {
98+
// We use the default JCE provider as there is no reason to use Bouncy Castle, moreover BC requires
99+
// the validated certificate to be in the certificate store which breaks the clean immutable usage of
100+
// trustedRootCACertificateCertStore in SubjectCertificateTrustedValidator.
101+
trustedRootCACertificateCertStore = CertStore.getInstance("Collection",
102+
new CollectionCertStoreParameters(configuration.getTrustedRootCACertificates()));
103+
} catch (GeneralSecurityException e) {
104+
throw new JceException(e);
105+
}
85106
}
86107

87108
@Override
@@ -128,7 +149,7 @@ public X509Certificate validate(String authToken) throws TokenValidationExceptio
128149
*/
129150
private ValidatorBatch getCertTrustValidators() {
130151
final SubjectCertificateTrustedValidator certTrustedValidator =
131-
new SubjectCertificateTrustedValidator(configuration.getTrustedCACertificates());
152+
new SubjectCertificateTrustedValidator(trustedRootCACertificateAnchors, trustedRootCACertificateCertStore);
132153
return ValidatorBatch.createFrom(
133154
certTrustedValidator::validateCertificateTrusted
134155
).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(),

src/main/java/org/webeid/security/validator/validators/SubjectCertificateNotRevokedValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public void validateCertificateNotRevoked(AuthTokenValidatorData actualTokenData
7777
final OCSPReq request = new OcspRequestBuilder()
7878
.certificate(certificate)
7979
.enableOcspNonce(!ocspNonceDisabled)
80-
.issuer(Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerCertificate()))
80+
.issuer(Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerRootCertificate()))
8181
.build();
8282

8383
LOG.debug("Sending OCSP request");

src/main/java/org/webeid/security/validator/validators/SubjectCertificateTrustedValidator.java

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,32 @@
2424

2525
import org.slf4j.Logger;
2626
import org.slf4j.LoggerFactory;
27+
import org.webeid.security.exceptions.JceException;
2728
import org.webeid.security.exceptions.UserCertificateNotTrustedException;
2829
import org.webeid.security.validator.AuthTokenValidatorData;
2930

30-
import javax.security.auth.x500.X500Principal;
31-
import java.security.GeneralSecurityException;
32-
import java.security.cert.X509Certificate;
33-
import java.util.Collection;
34-
import java.util.Map;
35-
import java.util.function.Function;
36-
import java.util.stream.Collectors;
31+
import java.security.InvalidAlgorithmParameterException;
32+
import java.security.NoSuchAlgorithmException;
33+
import java.security.cert.*;
34+
import java.util.Set;
3735

36+
/**
37+
* Validator that validates that the user certificate from the authentication token is signed by a trusted certificate authority.
38+
* <p>
39+
* We use the default JCE provider as there is no reason to use Bouncy Castle, moreover BC requires the validated certificate
40+
* to be in the certificate store which breaks the clean immutable usage of {@code trustedRootCACertificateCertStore}.
41+
*/
3842
public final class SubjectCertificateTrustedValidator {
3943

4044
private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateTrustedValidator.class);
4145

42-
private final Map<X500Principal, X509Certificate> trustedCACertificates;
43-
private X509Certificate trustedCACertificate;
46+
private final Set<TrustAnchor> trustedRootCACertificateAnchors;
47+
private final CertStore trustedRootCACertificateCertStore;
48+
private X509Certificate subjectCertificateIssuerRootCertificate;
4449

45-
public SubjectCertificateTrustedValidator(Collection<X509Certificate> trustedCACertificates) {
46-
this.trustedCACertificates = trustedCACertificates.stream()
47-
.collect(Collectors.toMap(X509Certificate::getSubjectX500Principal, Function.identity()));
50+
public SubjectCertificateTrustedValidator(Set<TrustAnchor> trustedRootCACertificateAnchors, CertStore trustedRootCACertificateCertStore) {
51+
this.trustedRootCACertificateAnchors = trustedRootCACertificateAnchors;
52+
this.trustedRootCACertificateCertStore = trustedRootCACertificateCertStore;
4853
}
4954

5055
/**
@@ -53,29 +58,33 @@ public SubjectCertificateTrustedValidator(Collection<X509Certificate> trustedCAC
5358
* @param actualTokenData authentication token data that contains the user certificate.
5459
* @throws UserCertificateNotTrustedException when user certificate is not signed by a trusted CA or is valid after CA certificate.
5560
*/
56-
public void validateCertificateTrusted(AuthTokenValidatorData actualTokenData) throws UserCertificateNotTrustedException {
61+
public void validateCertificateTrusted(AuthTokenValidatorData actualTokenData) throws UserCertificateNotTrustedException, JceException {
5762

58-
final X509Certificate userCertificate = actualTokenData.getSubjectCertificate();
59-
final X509Certificate caCertificate = trustedCACertificates.get(userCertificate.getIssuerX500Principal());
63+
final X509Certificate certificate = actualTokenData.getSubjectCertificate();
6064

61-
if (caCertificate == null) {
62-
throw new UserCertificateNotTrustedException("User certificate CA is not in the trusted CA list");
63-
}
65+
final X509CertSelector selector = new X509CertSelector();
66+
selector.setCertificate(certificate);
6467

6568
try {
66-
userCertificate.verify(caCertificate.getPublicKey());
67-
if (userCertificate.getNotAfter().after(caCertificate.getNotAfter())) {
68-
throw new UserCertificateNotTrustedException("Trusted CA certificate expires earlier than the user certificate");
69-
}
70-
this.trustedCACertificate = caCertificate;
71-
LOG.debug("User certificate is signed with a trusted CA certificate");
72-
} catch (GeneralSecurityException e) {
73-
LOG.trace("Error verifying signer's certificate {} against CA certificate {}", userCertificate.getSubjectDN(), caCertificate.getSubjectDN());
69+
final PKIXBuilderParameters pkixBuilderParameters = new PKIXBuilderParameters(trustedRootCACertificateAnchors, selector);
70+
pkixBuilderParameters.setRevocationEnabled(false);
71+
pkixBuilderParameters.addCertStore(trustedRootCACertificateCertStore);
72+
73+
// See the comment in AuthTokenValidatorImpl constructor why we use the default JCE provider.
74+
final CertPathBuilder certPathBuilder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType());
75+
final PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) certPathBuilder.build(pkixBuilderParameters);
76+
77+
subjectCertificateIssuerRootCertificate = result.getTrustAnchor().getTrustedCert();
78+
79+
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
80+
throw new JceException(e);
81+
} catch (CertPathBuilderException e) {
82+
LOG.trace("Error verifying signer's certificate {}: {}", certificate.getSubjectDN(), e);
7483
throw new UserCertificateNotTrustedException();
7584
}
7685
}
7786

78-
public X509Certificate getSubjectCertificateIssuerCertificate() {
79-
return trustedCACertificate;
87+
public X509Certificate getSubjectCertificateIssuerRootCertificate() {
88+
return subjectCertificateIssuerRootCertificate;
8089
}
8190
}

src/test/java/org/webeid/security/testutil/AbstractTestWithCache.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,18 @@ public abstract class AbstractTestWithCache {
4040
private static final String INCORRECT_TEST_NONCE_KEY = CORRECT_TEST_NONCE_KEY + "incorrect";
4141
private static final String TOO_SHORT_TEST_NONCE_KEY = "1234567812345678123456781234567";
4242

43-
private final CacheManager cacheManager = Caching.getCachingProvider(CaffeineCachingProvider.class.getName()).getCacheManager();
44-
private final CompleteConfiguration<String, LocalDateTime> cacheConfig = new MutableConfiguration<String, LocalDateTime>().setTypes(String.class, LocalDateTime.class);
43+
private static final CacheManager cacheManager = Caching.getCachingProvider(CaffeineCachingProvider.class.getName()).getCacheManager();
44+
private static final CompleteConfiguration<String, LocalDateTime> cacheConfig = new MutableConfiguration<String, LocalDateTime>().setTypes(String.class, LocalDateTime.class);
4545

4646
protected Cache<String, LocalDateTime> cache;
4747

48+
public static Cache<String, LocalDateTime> createCache(String cacheName) {
49+
return cacheManager.createCache(cacheName, cacheConfig);
50+
}
51+
4852
@BeforeEach
4953
protected void setup() {
50-
cache = cacheManager.createCache(CACHE_NAME, cacheConfig);
54+
cache = createCache(CACHE_NAME);
5155
}
5256

5357
public void putCorrectNonceToCache() {

src/test/java/org/webeid/security/testutil/AbstractTestWithMockedDateValidatorAndCorrectNonce.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
package org.webeid.security.testutil;
2424

2525
import org.junit.jupiter.api.BeforeEach;
26+
import org.webeid.security.exceptions.JceException;
2627
import org.webeid.security.validator.AuthTokenValidator;
2728

2829
import java.security.cert.CertificateException;
@@ -39,7 +40,7 @@ public void setup() {
3940
super.setup();
4041
try {
4142
validator = getAuthTokenValidator(cache);
42-
} catch (CertificateException e) {
43+
} catch (CertificateException | JceException e) {
4344
throw new RuntimeException(e);
4445
}
4546
}

src/test/java/org/webeid/security/testutil/AbstractTestWithValidator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
package org.webeid.security.testutil;
2424

2525
import org.junit.jupiter.api.BeforeEach;
26+
import org.webeid.security.exceptions.JceException;
2627
import org.webeid.security.validator.AuthTokenValidator;
2728

2829
import java.security.cert.CertificateException;
@@ -39,7 +40,7 @@ protected void setup() {
3940
super.setup();
4041
try {
4142
validator = getAuthTokenValidator(cache);
42-
} catch (CertificateException e) {
43+
} catch (CertificateException | JceException e) {
4344
throw new RuntimeException(e);
4445
}
4546
}

0 commit comments

Comments
 (0)