Skip to content

Commit f5248e4

Browse files
NFC-46 Add license header. Remove public from factory. Extract certificate validation steps into helper methods. Update test naming. Update error message. Add tests for Authority Key Identifier validation: mismatch between subject and signing cert, and missing AKI extension. Auth and sing cert for V11_AUTH_TOKEN_WRONG_CERT. Bring back comments.
1 parent 771955a commit f5248e4

File tree

9 files changed

+154
-52
lines changed

9 files changed

+154
-52
lines changed

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

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
1+
/*
2+
* Copyright (c) 2020-2025 Estonian Information System Authority
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
123
package eu.webeid.security.validator;
224

325
import eu.webeid.security.authtoken.SupportedSignatureAlgorithm;
426
import eu.webeid.security.authtoken.WebEidAuthToken;
527
import eu.webeid.security.certificate.CertificateLoader;
628
import eu.webeid.security.exceptions.AuthTokenException;
729
import eu.webeid.security.exceptions.AuthTokenParseException;
30+
import eu.webeid.security.exceptions.CertificateDecodingException;
831
import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
932
import eu.webeid.security.validator.ocsp.OcspClient;
1033
import eu.webeid.security.validator.ocsp.OcspServiceProvider;
@@ -64,52 +87,71 @@ public boolean supports(String format) {
6487
public X509Certificate validate(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException {
6588
final X509Certificate subjectCertificate = validateV1(token, currentChallengeNonce);
6689

90+
validateFormat(token);
91+
final X509Certificate signingCertificate = validateSigningCertificateExists(token);
92+
validateSupportedSignatureAlgorithms(token.getSupportedSignatureAlgorithms());
93+
validateSameSubject(subjectCertificate, signingCertificate);
94+
validateSameIssuer(subjectCertificate, signingCertificate);
95+
validateKeyUsage(signingCertificate);
96+
97+
return subjectCertificate;
98+
}
99+
100+
private static void validateSupportedSignatureAlgorithms(List<SupportedSignatureAlgorithm> algorithms) throws AuthTokenParseException {
101+
if (algorithms == null || algorithms.isEmpty()) {
102+
throw new AuthTokenParseException("'supportedSignatureAlgorithms' field is missing");
103+
}
104+
105+
boolean hasInvalid = algorithms.stream().anyMatch(supportedSignatureAlgorithm ->
106+
!SUPPORTED_CRYPTO_ALGORITHMS.contains(supportedSignatureAlgorithm.getCryptoAlgorithm()) ||
107+
!SUPPORTED_HASH_FUNCTIONS.contains(supportedSignatureAlgorithm.getHashFunction()) ||
108+
!SUPPORTED_PADDING_SCHEMES.contains(supportedSignatureAlgorithm.getPaddingScheme())
109+
);
110+
111+
if (hasInvalid) {
112+
throw new AuthTokenParseException("Unsupported signature algorithm");
113+
}
114+
}
115+
116+
private static void validateFormat(WebEidAuthToken token) throws AuthTokenParseException {
67117
if (token.getFormat() == null || !token.getFormat().startsWith(SUPPORTED_PREFIX)) {
68118
throw new AuthTokenParseException("Only token format '" + SUPPORTED_PREFIX + "' is supported by this validator");
69119
}
120+
}
70121

122+
private static X509Certificate validateSigningCertificateExists(WebEidAuthToken token) throws AuthTokenParseException, CertificateDecodingException {
71123
if (isNullOrEmpty(token.getUnverifiedSigningCertificate())) {
72124
throw new AuthTokenParseException("'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'");
73125
}
126+
return CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedSigningCertificate());
127+
}
74128

75-
validateSupportedSignatureAlgorithms(token.getSupportedSignatureAlgorithms());
76-
77-
final X509Certificate signingCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedSigningCertificate());
78-
79-
if (!subjectAndSigningCertificateSubjectsMatch(subjectCertificate.getSubjectX500Principal(), signingCertificate.getSubjectX500Principal())) {
129+
private static void validateSameSubject(X509Certificate subjectCertificate, X509Certificate signingCertificate)
130+
throws AuthTokenParseException {
131+
if (!subjectAndSigningCertificateSubjectsMatch(
132+
subjectCertificate.getSubjectX500Principal(),
133+
signingCertificate.getSubjectX500Principal())) {
80134
throw new AuthTokenParseException("Signing certificate subject does not match authentication certificate subject");
81135
}
136+
}
82137

138+
private static void validateSameIssuer(X509Certificate subjectCertificate, X509Certificate signingCertificate)
139+
throws AuthTokenParseException {
83140
byte[] subjectCertificateAuthorityKeyIdentifier = getAuthorityKeyIdentifier(subjectCertificate);
84141
byte[] signingCertificateAuthorityKeyIdentifier = getAuthorityKeyIdentifier(signingCertificate);
85142

86143
if (subjectCertificateAuthorityKeyIdentifier.length == 0
87144
|| signingCertificateAuthorityKeyIdentifier.length == 0
88145
|| !Arrays.equals(subjectCertificateAuthorityKeyIdentifier, signingCertificateAuthorityKeyIdentifier)) {
89-
throw new AuthTokenParseException("Signing certificate is not issued by the same issuing authority as the authentication certificate");
146+
throw new AuthTokenParseException(
147+
"Signing certificate is not issued by the same issuing authority as the authentication certificate");
90148
}
149+
}
91150

151+
private static void validateKeyUsage(X509Certificate signingCertificate) throws AuthTokenParseException {
92152
boolean[] keyUsage = signingCertificate.getKeyUsage();
93153
if (keyUsage == null || keyUsage.length <= KEY_USAGE_NON_REPUDIATION || !keyUsage[KEY_USAGE_NON_REPUDIATION]) {
94-
throw new AuthTokenParseException("Signing certificate not suitable for signing");
95-
}
96-
97-
return subjectCertificate;
98-
}
99-
100-
private static void validateSupportedSignatureAlgorithms(List<SupportedSignatureAlgorithm> algorithms) throws AuthTokenParseException {
101-
if (algorithms == null || algorithms.isEmpty()) {
102-
throw new AuthTokenParseException("'supportedSignatureAlgorithms' field is missing");
103-
}
104-
105-
boolean hasInvalid = algorithms.stream().anyMatch(supportedSignatureAlgorithm ->
106-
!SUPPORTED_CRYPTO_ALGORITHMS.contains(supportedSignatureAlgorithm.getCryptoAlgorithm()) ||
107-
!SUPPORTED_HASH_FUNCTIONS.contains(supportedSignatureAlgorithm.getHashFunction()) ||
108-
!SUPPORTED_PADDING_SCHEMES.contains(supportedSignatureAlgorithm.getPaddingScheme())
109-
);
110-
111-
if (hasInvalid) {
112-
throw new AuthTokenParseException("Unsupported signature algorithm");
154+
throw new AuthTokenParseException("Signing certificate key usage extension missing or does not contain non-repudiation bit required for digital signatures");
113155
}
114156
}
115157

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
/*
2+
* Copyright (c) 2020-2025 Estonian Information System Authority
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
123
package eu.webeid.security.validator;
224

325
import eu.webeid.security.authtoken.WebEidAuthToken;

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

55
import java.util.List;
66

7-
public final class AuthTokenValidatorFactory {
7+
final class AuthTokenValidatorFactory {
88
private final List<AuthTokenValidator> validators;
99

10-
public AuthTokenValidatorFactory(List<AuthTokenValidator> validators) {
10+
AuthTokenValidatorFactory(List<AuthTokenValidator> validators) {
1111
this.validators = List.copyOf(validators);
1212
}
1313

14-
public boolean supports(String format) {
14+
boolean supports(String format) {
1515
return validators.stream().anyMatch(v -> v.supports(format));
1616
}
1717

18-
public AuthTokenValidator requireFor(String format) throws AuthTokenParseException {
18+
AuthTokenValidator requireFor(String format) throws AuthTokenParseException {
1919
return validators.stream()
2020
.filter(v -> v.supports(format))
2121
.findFirst()

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
5252
private final AuthTokenValidatorFactory tokenValidatorFactory;
5353

5454
AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration, OcspClient ocspClient) throws JceException {
55+
// Copy the configuration object to make AuthTokenValidatorImpl immutable and thread-safe.
5556
final AuthTokenValidationConfiguration validationConfig = configuration.copy();
5657

58+
// Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator and AiaOcspService.
5759
final Set<TrustAnchor> trustedCACertificateAnchors = CertificateValidator.buildTrustAnchorsFromCertificates(validationConfig.getTrustedCACertificates());
5860
final CertStore trustedCACertificateCertStore = CertificateValidator.buildCertStoreFromCertificates(validationConfig.getTrustedCACertificates());
5961

@@ -64,6 +66,7 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
6466

6567
// OcspClient uses built-in HttpClient internally by default.
6668
// A single HttpClient instance is reused for all HTTP calls to utilize connection and thread pools.
69+
// The OCSP client may be provided by the API consumer.
6770
if (validationConfig.isUserCertificateRevocationCheckWithOcspEnabled()) {
6871
Objects.requireNonNull(ocspClient, "OCSP client must not be null when OCSP check is enabled");
6972
ocspServiceProvider = new OcspServiceProvider(

src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,36 @@ void whenV11SigningCertificateNotIssuedBySameAuthority_thenValidationFails() thr
415415

416416
X509Certificate mockSigningCert = mock(X509Certificate.class);
417417
when(mockSigningCert.getSubjectX500Principal()).thenReturn(realSubjectCert.getSubjectX500Principal());
418-
when(mockSigningCert.getIssuerX500Principal()).thenReturn(
419-
new javax.security.auth.x500.X500Principal("CN=Other Test CA, O=Other Org, C=EE")
420-
);
418+
419+
byte[] realAki = realSubjectCert.getExtensionValue(Extension.authorityKeyIdentifier.getId());
420+
byte[] differentAki = realAki.clone();
421+
if (differentAki.length > 0) {
422+
differentAki[differentAki.length - 1] ^= (byte) 0xFF;
423+
}
424+
when(mockSigningCert.getExtensionValue(Extension.authorityKeyIdentifier.getId())).thenReturn(differentAki);
425+
426+
try (MockedStatic<CertificateLoader> mocked = mockStatic(CertificateLoader.class)) {
427+
mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate()))
428+
.thenReturn(realSubjectCert);
429+
mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedSigningCertificate()))
430+
.thenReturn(mockSigningCert);
431+
432+
assertThatThrownBy(() -> spyValidator.validate(parsedToken, VALID_CHALLENGE_NONCE))
433+
.isInstanceOf(AuthTokenParseException.class)
434+
.hasMessage("Signing certificate is not issued by the same issuing authority as the authentication certificate");
435+
}
436+
}
437+
438+
@Test
439+
void whenV11SigningCertificateHasNoAuthorityKeyIdentifier_thenValidationFails() throws Exception {
440+
AuthTokenV11Validator spyValidator = spyAuthTokenV11Validator();
441+
WebEidAuthToken parsedToken = OBJECT_READER.readValue(V11_AUTH_TOKEN, WebEidAuthToken.class);
442+
X509Certificate realSubjectCert = CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate());
443+
doReturn(realSubjectCert).when(spyValidator).validateV1(any(), any());
444+
445+
X509Certificate mockSigningCert = mock(X509Certificate.class);
446+
when(mockSigningCert.getSubjectX500Principal()).thenReturn(realSubjectCert.getSubjectX500Principal());
447+
when(mockSigningCert.getExtensionValue(Extension.authorityKeyIdentifier.getId())).thenReturn(null);
421448

422449
try (MockedStatic<CertificateLoader> mocked = mockStatic(CertificateLoader.class)) {
423450
mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate()))
@@ -453,7 +480,7 @@ void whenV11SigningCertificateNotSuitableForSigning_thenValidationFails() throws
453480

454481
assertThatThrownBy(() -> spyValidator.validate(parsedToken, VALID_CHALLENGE_NONCE))
455482
.isInstanceOf(AuthTokenParseException.class)
456-
.hasMessage("Signing certificate not suitable for signing");
483+
.hasMessage("Signing certificate key usage extension missing or does not contain non-repudiation bit required for digital signatures");
457484
}
458485
}
459486

0 commit comments

Comments
 (0)