Skip to content

Commit be722c1

Browse files
NFC-46 Strategy + Factory for token format validation (V1, V11). Refactoring.
1 parent 8bd6e63 commit be722c1

15 files changed

+440
-408
lines changed

src/main/java/eu/webeid/security/validator/AuthTokenV11Validator.java

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,40 @@
1414

1515
import static eu.webeid.security.util.Strings.isNullOrEmpty;
1616

17-
public class AuthTokenV11Validator {
17+
public final class AuthTokenV11Validator implements AuthTokenValidator {
18+
19+
private static final String SUPPORTED_PREFIX = "web-eid:1.1";
1820

1921
private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
2022
private final Supplier<SubjectCertificateValidatorBatch> certTrustValidatorsSupplier;
23+
private final AuthTokenSignatureValidator authTokenSignatureValidator;
2124

2225
public AuthTokenV11Validator(
23-
SubjectCertificateValidatorBatch simpleSubjectCertificateValidators,
24-
Supplier<SubjectCertificateValidatorBatch> certTrustValidatorsSupplier
26+
SubjectCertificateValidatorBatch simpleSubjectCertificateValidators,
27+
Supplier<SubjectCertificateValidatorBatch> certTrustValidatorsSupplier,
28+
AuthTokenSignatureValidator authTokenSignatureValidator
2529
) {
2630
this.simpleSubjectCertificateValidators = simpleSubjectCertificateValidators;
2731
this.certTrustValidatorsSupplier = certTrustValidatorsSupplier;
32+
this.authTokenSignatureValidator = authTokenSignatureValidator;
33+
}
34+
35+
@Override
36+
public boolean supports(String format) {
37+
return format != null && format.startsWith(SUPPORTED_PREFIX);
38+
}
39+
40+
@Override
41+
public WebEidAuthToken parse(String authToken) throws AuthTokenException {
42+
throw new UnsupportedOperationException("Parsing is handled by AuthTokenValidatorImpl. " + this.getClass().getSimpleName() + " only supports validation.");
2843
}
2944

30-
public void validate(WebEidAuthToken token, X509Certificate subjectCertificate) throws AuthTokenException {
45+
@Override
46+
public X509Certificate validate(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException {
47+
if (token.getFormat() == null || !token.getFormat().startsWith(SUPPORTED_PREFIX)) {
48+
throw new AuthTokenParseException("Only token format '" + SUPPORTED_PREFIX + "' is supported by this validator");
49+
}
50+
3151
if (isNullOrEmpty(token.getUnverifiedSigningCertificate())) {
3252
throw new AuthTokenParseException("'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'");
3353
}
@@ -38,22 +58,31 @@ public void validate(WebEidAuthToken token, X509Certificate subjectCertificate)
3858

3959
validateSupportedSignatureAlgorithms(token.getSupportedSignatureAlgorithms());
4060

41-
final X509Certificate signingCertificate =
42-
CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedSigningCertificate());
61+
final X509Certificate subjectCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedCertificate());
62+
final X509Certificate signingCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedSigningCertificate());
4363

4464
if (!subjectCertificate.getSubjectX500Principal().equals(signingCertificate.getSubjectX500Principal())) {
4565
throw new AuthTokenParseException("Signing certificate subject does not match authentication certificate subject");
4666
}
4767

4868
simpleSubjectCertificateValidators.executeFor(signingCertificate);
4969
certTrustValidatorsSupplier.get().executeFor(signingCertificate);
70+
71+
authTokenSignatureValidator.validate(
72+
token.getAlgorithm(),
73+
token.getSignature(),
74+
signingCertificate.getPublicKey(),
75+
currentChallengeNonce
76+
);
77+
78+
return subjectCertificate;
5079
}
5180

5281
private static void validateSupportedSignatureAlgorithms(List<SupportedSignatureAlgorithm> algorithms) throws AuthTokenParseException {
5382
boolean hasInvalid = algorithms.stream().anyMatch(supportedSignatureAlgorithm ->
54-
!isValidCryptoAlgorithm(supportedSignatureAlgorithm.getCryptoAlgorithm())
55-
|| !isValidHashFunction(supportedSignatureAlgorithm.getHashFunction())
56-
|| !isValidPaddingScheme(supportedSignatureAlgorithm.getPaddingScheme())
83+
!isValidCryptoAlgorithm(supportedSignatureAlgorithm.getCryptoAlgorithm())
84+
|| !isValidHashFunction(supportedSignatureAlgorithm.getHashFunction())
85+
|| !isValidPaddingScheme(supportedSignatureAlgorithm.getPaddingScheme())
5786
);
5887

5988
if (hasInvalid) {
@@ -67,8 +96,8 @@ private static boolean isValidCryptoAlgorithm(String value) {
6796

6897
private static boolean isValidHashFunction(String value) {
6998
return Set.of(
70-
"SHA-224", "SHA-256", "SHA-384", "SHA-512",
71-
"SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512"
99+
"SHA-224", "SHA-256", "SHA-384", "SHA-512",
100+
"SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512"
72101
).contains(value);
73102
}
74103

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package eu.webeid.security.validator;
2+
3+
import eu.webeid.security.authtoken.WebEidAuthToken;
4+
import eu.webeid.security.certificate.CertificateLoader;
5+
import eu.webeid.security.exceptions.AuthTokenException;
6+
import eu.webeid.security.exceptions.AuthTokenParseException;
7+
import eu.webeid.security.validator.certvalidators.SubjectCertificateNotRevokedValidator;
8+
import eu.webeid.security.validator.certvalidators.SubjectCertificateTrustedValidator;
9+
import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
10+
import eu.webeid.security.validator.ocsp.OcspClient;
11+
import eu.webeid.security.validator.ocsp.OcspServiceProvider;
12+
13+
import java.security.cert.CertStore;
14+
import java.security.cert.TrustAnchor;
15+
import java.security.cert.X509Certificate;
16+
import java.util.Set;
17+
18+
public final class AuthTokenV1Validator implements AuthTokenValidator {
19+
20+
private static final String SUPPORTED_TOKEN_FORMAT_VERSION = "web-eid:1";
21+
22+
private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
23+
private final Set<TrustAnchor> trustedCACertificateAnchors;
24+
private final CertStore trustedCACertificateCertStore;
25+
private final AuthTokenSignatureValidator authTokenSignatureValidator;
26+
private final AuthTokenValidationConfiguration configuration;
27+
private final OcspClient ocspClient;
28+
private final OcspServiceProvider ocspServiceProvider;
29+
30+
public AuthTokenV1Validator(
31+
SubjectCertificateValidatorBatch simpleSubjectCertificateValidators,
32+
Set<TrustAnchor> trustedCACertificateAnchors,
33+
CertStore trustedCACertificateCertStore,
34+
AuthTokenSignatureValidator authTokenSignatureValidator,
35+
AuthTokenValidationConfiguration configuration,
36+
OcspClient ocspClient,
37+
OcspServiceProvider ocspServiceProvider
38+
) {
39+
this.simpleSubjectCertificateValidators = simpleSubjectCertificateValidators;
40+
this.trustedCACertificateAnchors = trustedCACertificateAnchors;
41+
this.trustedCACertificateCertStore = trustedCACertificateCertStore;
42+
this.authTokenSignatureValidator = authTokenSignatureValidator;
43+
this.configuration = configuration;
44+
this.ocspClient = ocspClient;
45+
this.ocspServiceProvider = ocspServiceProvider;
46+
}
47+
48+
@Override
49+
public boolean supports(String format) {
50+
return format != null && format.startsWith(SUPPORTED_TOKEN_FORMAT_VERSION);
51+
}
52+
53+
@Override
54+
public WebEidAuthToken parse(String authToken) throws AuthTokenException {
55+
throw new UnsupportedOperationException("Parsing is handled by AuthTokenValidatorImpl. " + this.getClass().getSimpleName() + " only supports validation.");
56+
}
57+
58+
@Override
59+
public X509Certificate validate(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException {
60+
if (token.getFormat() == null || token.getFormat().isBlank()) {
61+
throw new AuthTokenParseException("'format' field is missing");
62+
}
63+
if (!token.getFormat().startsWith(SUPPORTED_TOKEN_FORMAT_VERSION)) {
64+
throw new AuthTokenParseException("Only token format version '" + SUPPORTED_TOKEN_FORMAT_VERSION + "' is currently supported");
65+
}
66+
67+
if (token.getUnverifiedCertificate() == null || token.getUnverifiedCertificate().isEmpty()) {
68+
throw new AuthTokenParseException("'unverifiedCertificate' field is missing, null or empty");
69+
}
70+
71+
final X509Certificate subjectCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedCertificate());
72+
73+
simpleSubjectCertificateValidators.executeFor(subjectCertificate);
74+
getCertTrustValidators().executeFor(subjectCertificate);
75+
76+
// It is guaranteed that if the signature verification succeeds, then the origin and challenge
77+
// have been implicitly and correctly verified without the need to implement any additional checks.
78+
authTokenSignatureValidator.validate(
79+
token.getAlgorithm(),
80+
token.getSignature(),
81+
subjectCertificate.getPublicKey(),
82+
currentChallengeNonce
83+
);
84+
85+
return subjectCertificate;
86+
}
87+
88+
/**
89+
* Creates the certificate trust validators batch.
90+
* As SubjectCertificateTrustedValidator has mutable state that SubjectCertificateNotRevokedValidator depends on,
91+
* they cannot be reused/cached in an instance variable in a multi-threaded environment. Hence, they are
92+
* re-created for each validation run for thread safety.
93+
*
94+
* @return certificate trust validator batch
95+
*/
96+
private SubjectCertificateValidatorBatch getCertTrustValidators() {
97+
return createCertTrustValidators(
98+
configuration,
99+
trustedCACertificateAnchors,
100+
trustedCACertificateCertStore,
101+
ocspClient,
102+
ocspServiceProvider
103+
);
104+
}
105+
106+
public static SubjectCertificateValidatorBatch createCertTrustValidators(
107+
AuthTokenValidationConfiguration configuration,
108+
Set<TrustAnchor> trustedCACertificateAnchors,
109+
CertStore trustedCACertificateCertStore,
110+
OcspClient ocspClient,
111+
OcspServiceProvider ocspServiceProvider
112+
) {
113+
final SubjectCertificateTrustedValidator certTrustedValidator =
114+
new SubjectCertificateTrustedValidator(trustedCACertificateAnchors, trustedCACertificateCertStore);
115+
116+
return SubjectCertificateValidatorBatch.createFrom(
117+
certTrustedValidator::validateCertificateTrusted
118+
).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(),
119+
new SubjectCertificateNotRevokedValidator(
120+
certTrustedValidator,
121+
ocspClient, ocspServiceProvider,
122+
configuration.getAllowedOcspResponseTimeSkew(),
123+
configuration.getMaxOcspResponseThisUpdateAge()
124+
)::validateCertificateNotRevoked
125+
);
126+
}
127+
}

src/main/java/eu/webeid/security/validator/AuthTokenValidator.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
*/
3535
public interface AuthTokenValidator {
3636

37+
/**
38+
* Returns whether this validator supports validation of the given token format.
39+
*
40+
* @param format the format string from the Web eID authentication token (e.g. "web-eid:1.0", "web-eid:1.1")
41+
* @return true if this validator can handle the given format, false otherwise
42+
*/
43+
boolean supports(String format);
44+
3745
/**
3846
* Parses the Web eID authentication token signed by the subject.
3947
*
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package eu.webeid.security.validator;
2+
3+
import eu.webeid.security.exceptions.AuthTokenParseException;
4+
5+
import java.util.List;
6+
7+
public final class AuthTokenValidatorFactory {
8+
private final List<AuthTokenValidator> validators;
9+
10+
public AuthTokenValidatorFactory(List<AuthTokenValidator> validators) {
11+
this.validators = List.copyOf(validators);
12+
}
13+
14+
public AuthTokenValidator requireFor(String format) throws AuthTokenParseException {
15+
return validators.stream()
16+
.filter(v -> v.supports(format))
17+
.findFirst()
18+
.orElseThrow(() -> new AuthTokenParseException("Only token format version 'web-eid:1' is currently supported"));
19+
}
20+
}

0 commit comments

Comments
 (0)