trustedCACertificateAnchors,
+ CertStore trustedCACertificateCertStore,
+ AuthTokenSignatureValidator authTokenSignatureValidator,
+ AuthTokenValidationConfiguration configuration,
+ OcspClient ocspClient,
+ OcspServiceProvider ocspServiceProvider
+ ) {
+ this.simpleSubjectCertificateValidators = simpleSubjectCertificateValidators;
+ this.trustedCACertificateAnchors = trustedCACertificateAnchors;
+ this.trustedCACertificateCertStore = trustedCACertificateCertStore;
+ this.authTokenSignatureValidator = authTokenSignatureValidator;
+ this.configuration = configuration;
+ this.ocspClient = ocspClient;
+ this.ocspServiceProvider = ocspServiceProvider;
+ }
+
+ @Override
+ public boolean supports(String format) {
+ return format != null && format.startsWith(getSupportedFormatPrefix());
+ }
+
+ protected String getSupportedFormatPrefix() {
+ return V1_SUPPORTED_TOKEN_FORMAT_PREFIX;
+ }
+
+ @Override
+ public X509Certificate validate(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException {
+ if (token.getUnverifiedCertificate() == null || token.getUnverifiedCertificate().isEmpty()) {
+ throw new AuthTokenParseException("'unverifiedCertificate' field is missing, null or empty");
+ }
+
+ final X509Certificate subjectCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedCertificate());
+
+ simpleSubjectCertificateValidators.executeFor(subjectCertificate);
+
+ SubjectCertificateValidatorBatch.forTrustValidation(
+ configuration,
+ trustedCACertificateAnchors,
+ trustedCACertificateCertStore,
+ ocspClient,
+ ocspServiceProvider
+ ).executeFor(subjectCertificate);
+
+ // It is guaranteed that if the signature verification succeeds, then the origin and challenge
+ // have been implicitly and correctly verified without the need to implement any additional checks.
+ authTokenSignatureValidator.validate(
+ token.getAlgorithm(),
+ token.getSignature(),
+ subjectCertificate.getPublicKey(),
+ currentChallengeNonce
+ );
+
+ return subjectCertificate;
+ }
+}
diff --git a/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidator.java b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidator.java
new file mode 100644
index 00000000..61279e6e
--- /dev/null
+++ b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2020-2025 Estonian Information System Authority
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package eu.webeid.security.validator.versionvalidators;
+
+import eu.webeid.security.authtoken.WebEidAuthToken;
+import eu.webeid.security.certificate.CertificateData;
+import eu.webeid.security.exceptions.AuthTokenException;
+import eu.webeid.security.util.Strings;
+
+import java.security.cert.X509Certificate;
+
+public interface AuthTokenVersionValidator {
+ /**
+ * Returns whether this validator supports validation of the given token format.
+ *
+ * @param format the format string from the Web eID authentication token (e.g. "web-eid:1.0", "web-eid:1.1")
+ * @return true if this validator can handle the given format, false otherwise
+ */
+ boolean supports(String format);
+
+ /**
+ * Validates the Web eID authentication token signed by the subject and returns
+ * the subject certificate that can be used for retrieving information about the subject.
+ *
+ * See {@link CertificateData} and {@link Strings} for convenience methods for retrieving user
+ * information from the certificate.
+ *
+ * @param authToken the Web eID authentication token
+ * @param currentChallengeNonce the challenge nonce that is associated with the authentication token
+ * @return validated subject certificate
+ * @throws AuthTokenException when validation fails
+ */
+ X509Certificate validate(WebEidAuthToken authToken, String currentChallengeNonce) throws AuthTokenException;
+}
diff --git a/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidatorFactory.java b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidatorFactory.java
new file mode 100644
index 00000000..9d6be378
--- /dev/null
+++ b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidatorFactory.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2020-2025 Estonian Information System Authority
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package eu.webeid.security.validator.versionvalidators;
+
+import eu.webeid.security.certificate.CertificateValidator;
+import eu.webeid.security.exceptions.AuthTokenParseException;
+import eu.webeid.security.exceptions.JceException;
+import eu.webeid.security.validator.AuthTokenSignatureValidator;
+import eu.webeid.security.validator.AuthTokenValidationConfiguration;
+import eu.webeid.security.validator.certvalidators.SubjectCertificatePolicyValidator;
+import eu.webeid.security.validator.certvalidators.SubjectCertificatePurposeValidator;
+import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
+import eu.webeid.security.validator.ocsp.OcspClient;
+import eu.webeid.security.validator.ocsp.OcspServiceProvider;
+import eu.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration;
+
+import java.security.cert.CertStore;
+import java.security.cert.TrustAnchor;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public final class AuthTokenVersionValidatorFactory {
+ private final List validators;
+
+ AuthTokenVersionValidatorFactory(List validators) {
+ this.validators = validators;
+ }
+
+ boolean supports(String format) {
+ return validators.stream().anyMatch(v -> v.supports(format));
+ }
+
+ public AuthTokenVersionValidator getValidatorFor(String format) throws AuthTokenParseException {
+ return validators.stream()
+ .filter(v -> v.supports(format))
+ .findFirst()
+ .orElseThrow(() -> new AuthTokenParseException(
+ "Token format version '" + format + "' is currently not supported"));
+ }
+
+ public static AuthTokenVersionValidatorFactory create(AuthTokenValidationConfiguration configuration, OcspClient ocspClient) throws JceException {
+ // Copy the configuration object to make AuthTokenVersionValidatorFactory immutable and thread-safe.
+ final AuthTokenValidationConfiguration validationConfig = configuration.copy();
+
+ // Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator and AiaOcspService.
+ final Set trustedCACertificateAnchors = CertificateValidator.buildTrustAnchorsFromCertificates(validationConfig.getTrustedCACertificates());
+ final CertStore trustedCACertificateCertStore = CertificateValidator.buildCertStoreFromCertificates(validationConfig.getTrustedCACertificates());
+
+ final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators = SubjectCertificateValidatorBatch.createFrom(
+ SubjectCertificatePurposeValidator::validateCertificatePurpose,
+ new SubjectCertificatePolicyValidator(validationConfig.getDisallowedSubjectCertificatePolicies())::validateCertificatePolicies
+ );
+
+ // OcspClient uses built-in HttpClient internally by default.
+ // A single HttpClient instance is reused for all HTTP calls to utilize connection and thread pools.
+ // The OCSP client may be provided by the API consumer.
+ OcspServiceProvider ocspServiceProvider = null;
+ if (validationConfig.isUserCertificateRevocationCheckWithOcspEnabled()) {
+ Objects.requireNonNull(ocspClient, "OCSP client must not be null when OCSP check is enabled");
+ ocspServiceProvider = new OcspServiceProvider(
+ validationConfig.getDesignatedOcspServiceConfiguration(),
+ new AiaOcspServiceConfiguration(
+ validationConfig.getNonceDisabledOcspUrls(),
+ trustedCACertificateAnchors,
+ trustedCACertificateCertStore));
+ }
+
+ final AuthTokenSignatureValidator authTokenSignatureValidator =
+ new AuthTokenSignatureValidator(validationConfig.getSiteOrigin());
+
+ return new AuthTokenVersionValidatorFactory(List.of(
+ new AuthTokenVersion11Validator(
+ simpleSubjectCertificateValidators,
+ trustedCACertificateAnchors,
+ trustedCACertificateCertStore,
+ authTokenSignatureValidator,
+ validationConfig,
+ ocspClient,
+ ocspServiceProvider
+ ),
+ new AuthTokenVersion1Validator(
+ simpleSubjectCertificateValidators,
+ trustedCACertificateAnchors,
+ trustedCACertificateCertStore,
+ authTokenSignatureValidator,
+ validationConfig,
+ ocspClient,
+ ocspServiceProvider
+ )
+ ));
+ }
+}
diff --git a/src/test/java/eu/webeid/security/challenge/ChallengeNonceGeneratorTest.java b/src/test/java/eu/webeid/security/challenge/ChallengeNonceGeneratorTest.java
index 5790e0d2..2a9ff366 100644
--- a/src/test/java/eu/webeid/security/challenge/ChallengeNonceGeneratorTest.java
+++ b/src/test/java/eu/webeid/security/challenge/ChallengeNonceGeneratorTest.java
@@ -23,13 +23,15 @@
package eu.webeid.security.challenge;
import eu.webeid.security.exceptions.AuthTokenException;
-import org.junit.jupiter.api.Test;
import eu.webeid.security.exceptions.ChallengeNonceExpiredException;
import eu.webeid.security.exceptions.ChallengeNonceNotFoundException;
+import org.junit.jupiter.api.Test;
import java.time.Duration;
-import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
class ChallengeNonceGeneratorTest {
diff --git a/src/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java b/src/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java
index fd8896c3..ad5d400b 100644
--- a/src/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java
+++ b/src/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java
@@ -22,19 +22,19 @@
package eu.webeid.security.testutil;
-import org.junit.jupiter.api.BeforeEach;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import eu.webeid.security.authtoken.WebEidAuthToken;
import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.security.validator.AuthTokenValidator;
+import org.junit.jupiter.api.BeforeEach;
import java.io.IOException;
import java.security.cert.CertificateException;
-import static eu.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidator;
-
public abstract class AbstractTestWithValidator {
- /*
+ /*
* notBefore Time UTCTime 2021-07-22 12:43:08 UTC
* notAfter Time UTCTime 2026-07-09 21:59:59 UTC
*/
@@ -43,16 +43,26 @@ public abstract class AbstractTestWithValidator {
"\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.5.0+0\"," +
"\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," +
"\"format\":\"web-eid:1.0\"}";
+
+ public static final String VALID_V11_AUTH_TOKEN = "{\"algorithm\":\"ES384\"," +
+ "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," +
+ "\"unverifiedSigningCertificate\":\"MIID6zCCA02gAwIBAgIQT7j6zk6pmVRcyspLo5SqejAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUzMVoXDTI5MDUwMjEwNDUzMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASkwENR8GmCpEs6OshDWDfIiKvGuyNMOD2rjIQW321AnZD3oIsqD0svBMNEJJj9Dlvq/47TYDObIa12KAU5IuOBfJs2lrFdSXZjaM+a5TWT3O2JTM36YDH2GcMe/eisepejggGrMIIBpzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBIBgNVHSAEQTA/MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBTVX3s48Spy/Es2TcXgkRvwUn2YcjCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwEwYGBACORgEGMAkGBwQAjkYBBgEwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgGBr+Jbo1GeqgWdIwgMo7SA29AP38JxNm2HWq2Qb+kIHpusAK574Co1K5D4+Mk7/ITTuXQaET5WphHoN7tdAciTaQJBAn0zBigYyVPYSTO68HM6hmlwTwi/KlJDdXW/2NsMjSqofFFJXpGvpxk2CTqSRCjcavxLPnkasTbNROYSJcmM8Xc=\"," +
+ "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," +
+ "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," +
+ "\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," +
+ "\"format\":\"web-eid:1.1\"}";
public static final String VALID_CHALLENGE_NONCE = "12345678123456781234567812345678912356789123";
protected AuthTokenValidator validator;
protected WebEidAuthToken validAuthToken;
+ protected WebEidAuthToken validV11AuthToken;
@BeforeEach
protected void setup() {
try {
validator = AuthTokenValidators.getAuthTokenValidator();
validAuthToken = validator.parse(VALID_AUTH_TOKEN);
+ validV11AuthToken = validator.parse(VALID_V11_AUTH_TOKEN);
} catch (CertificateException | IOException | AuthTokenException e) {
throw new RuntimeException(e);
}
@@ -62,4 +72,11 @@ protected WebEidAuthToken replaceTokenField(String token, String field, String v
final String tokenWithReplacedAlgorithm = token.replace(field, value);
return validator.parse(tokenWithReplacedAlgorithm);
}
+
+ protected WebEidAuthToken removeJsonField() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode node = (ObjectNode) mapper.readTree(AbstractTestWithValidator.VALID_V11_AUTH_TOKEN);
+ node.remove("supportedSignatureAlgorithms");
+ return validator.parse(mapper.writeValueAsString(node));
+ }
}
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenAlgorithmTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenAlgorithmTest.java
index 3f1e6a78..1c2d61f5 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenAlgorithmTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenAlgorithmTest.java
@@ -22,11 +22,11 @@
package eu.webeid.security.validator;
-import org.junit.jupiter.api.Test;
import eu.webeid.security.authtoken.WebEidAuthToken;
-import eu.webeid.security.exceptions.AuthTokenParseException;
import eu.webeid.security.exceptions.AuthTokenException;
+import eu.webeid.security.exceptions.AuthTokenParseException;
import eu.webeid.security.testutil.AbstractTestWithValidator;
+import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -59,4 +59,65 @@ void whenAlgorithmInvalid_thenParsingFails() throws AuthTokenException {
.hasMessage("Unsupported signature algorithm");
}
+ @Test
+ void whenV11TokenMissingSupportedAlgorithms_thenValidationFails() throws Exception {
+ final WebEidAuthToken token = removeJsonField();
+
+ assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessageContaining("'supportedSignatureAlgorithms' field is missing");
+ }
+
+ @Test
+ void whenV11TokenHasInvalidCryptoAlgorithm_thenValidationFails() throws Exception {
+ final WebEidAuthToken token = replaceTokenField(
+ VALID_V11_AUTH_TOKEN,
+ "\"cryptoAlgorithm\":\"RSA\"",
+ "\"cryptoAlgorithm\":\"INVALID\""
+ );
+
+ assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("Unsupported signature algorithm");
+ }
+
+ @Test
+ void whenV11TokenHasInvalidHashFunction_thenValidationFails() throws Exception {
+ final WebEidAuthToken token = replaceTokenField(
+ VALID_V11_AUTH_TOKEN,
+ "\"hashFunction\":\"SHA-256\"",
+ "\"hashFunction\":\"NOT_A_HASH\""
+ );
+
+ assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("Unsupported signature algorithm");
+ }
+
+ @Test
+ void whenV11TokenHasInvalidPaddingScheme_thenValidationFails() throws Exception {
+ final WebEidAuthToken token = replaceTokenField(
+ VALID_V11_AUTH_TOKEN,
+ "\"paddingScheme\":\"PKCS1.5\"",
+ "\"paddingScheme\":\"BAD_PADDING\""
+ );
+
+ assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("Unsupported signature algorithm");
+ }
+
+ @Test
+ void whenV11TokenHasEmptySupportedAlgorithms_thenValidationFails() throws Exception {
+ final WebEidAuthToken token = replaceTokenField(
+ VALID_V11_AUTH_TOKEN,
+ "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]",
+ "\"supportedSignatureAlgorithms\":[]"
+ );
+
+ assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("'supportedSignatureAlgorithms' field is missing");
+ }
+
}
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateBelgianIdCardTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateBelgianIdCardTest.java
index c354b47d..2a5b7770 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateBelgianIdCardTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateBelgianIdCardTest.java
@@ -22,22 +22,23 @@
package eu.webeid.security.validator;
-import static eu.webeid.security.testutil.DateMocker.mockDate;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.mockito.Mockito.mockStatic;
-
import eu.webeid.security.authtoken.WebEidAuthToken;
import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.security.testutil.AbstractTestWithValidator;
import eu.webeid.security.testutil.AuthTokenValidators;
import eu.webeid.security.util.DateAndTime;
-import java.io.IOException;
-import java.security.cert.CertificateException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
+import java.io.IOException;
+import java.security.cert.CertificateException;
+
+import static eu.webeid.security.testutil.DateMocker.mockDate;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.Mockito.mockStatic;
+
class AuthTokenCertificateBelgianIdCardTest extends AbstractTestWithValidator {
private static final String BELGIAN_TEST_ID_CARD_AUTH_TOKEN_ECC =
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateFinnishIdCardTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateFinnishIdCardTest.java
index c4fe11d2..786aaade 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateFinnishIdCardTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateFinnishIdCardTest.java
@@ -22,22 +22,23 @@
package eu.webeid.security.validator;
-import static eu.webeid.security.testutil.DateMocker.mockDate;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.mockito.Mockito.mockStatic;
-
import eu.webeid.security.authtoken.WebEidAuthToken;
import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.security.testutil.AbstractTestWithValidator;
import eu.webeid.security.testutil.AuthTokenValidators;
import eu.webeid.security.util.DateAndTime;
-import java.io.IOException;
-import java.security.cert.CertificateException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
+import java.io.IOException;
+import java.security.cert.CertificateException;
+
+import static eu.webeid.security.testutil.DateMocker.mockDate;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.Mockito.mockStatic;
+
class AuthTokenCertificateFinnishIdCardTest extends AbstractTestWithValidator {
private static final String FINNISH_TEST_ID_CARD_BACKMAN_JUHANI_AUTH_TOKEN =
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java
index 14ed0666..994d1811 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java
@@ -70,7 +70,6 @@ class AuthTokenCertificateTest extends AbstractTestWithValidator {
private MockedStatic mockedClock;
-
@Override
@BeforeEach
protected void setup() {
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java
index 3f596858..2da650be 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java
@@ -46,6 +46,14 @@ class AuthTokenSignatureTest extends AbstractTestWithValidator {
"\"signature\":\"arx164xRiwhIQDINe0J+ZxJWZFOQTx0PBtOaWaxAe7gofEIHRIbV1w0sOCYBJnvmvMem9hU4nc2+iJx2x8poYck4Z6eI3GwtiksIec3XQ9ZIk1n/XchXnmPn3GYV+HzJ\"," +
"\"format\":\"web-eid:1.0\"}";
+ static final String V11_AUTH_TOKEN_WRONG_CERT = "{\"algorithm\":\"ES384\"," +
+ "\"unverifiedCertificate\":\"MIIEDDCCA26gAwIBAgIQM8UTDe8zVKtcysotoMgBlzAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUwMVoXDTI5MDUwMjEwNDUwMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARh/M6SBatkyMHjTmRgIF1MTqZpVIfqHZD6MrQUHdlykVSLNBmloFjoXbQbSe0l+sgKUPSZWb48IGPC7Mrudt5vLvnKy31qZ5a+2Ceg87NrVzdNCWF2oQrwXw63HieIBMmjggHMMIIByDAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwKAYDVR0RBCEwH4EdamFhay1rcmlzdGphbi5qb2VvcmdAZWVzdGkuZWUwHQYDVR0OBBYEFOSW4XJH0oDJAh2nEqFGhrlF9zXQMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQQjG3AnPzJdtmoaNI59T8vcjsNjVB5XLfUXiBguizma9I6dFqhHiTtfqo2aWpd+dcL8iz/3Dn03C0ruPLnJVt24lAkIB8M6KO+RcVJqXz8KXMUGstjK+1iIE0hd+2JtNmIJcqgNT7sj8f4NZfsix5JuUpY1j4msWG3k0h79U2bWcR8NQZdU=\"," +
+ "\"unverifiedSigningCertificate\":\"MIID6zCCA02gAwIBAgIQT7j6zk6pmVRcyspLo5SqejAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUzMVoXDTI5MDUwMjEwNDUzMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASkwENR8GmCpEs6OshDWDfIiKvGuyNMOD2rjIQW321AnZD3oIsqD0svBMNEJJj9Dlvq/47TYDObIa12KAU5IuOBfJs2lrFdSXZjaM+a5TWT3O2JTM36YDH2GcMe/eisepejggGrMIIBpzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBIBgNVHSAEQTA/MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBTVX3s48Spy/Es2TcXgkRvwUn2YcjCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwEwYGBACORgEGMAkGBwQAjkYBBgEwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgGBr+Jbo1GeqgWdIwgMo7SA29AP38JxNm2HWq2Qb+kIHpusAK574Co1K5D4+Mk7/ITTuXQaET5WphHoN7tdAciTaQJBAn0zBigYyVPYSTO68HM6hmlwTwi/KlJDdXW/2NsMjSqofFFJXpGvpxk2CTqSRCjcavxLPnkasTbNROYSJcmM8Xc=\"," +
+ "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," +
+ "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," +
+ "\"signature\":\"arx164xRiwhIQDINe0J+ZxJWZFOQTx0PBtOaWaxAe7gofEIHRIbV1w0sOCYBJnvmvMem9hU4nc2+iJx2x8poYck4Z6eI3GwtiksIec3XQ9ZIk1n/XchXnmPn3GYV+HzJ\"," +
+ "\"format\":\"web-eid:1.1\"}";
+
@Test
void whenValidTokenAndNonce_thenValidationSucceeds() throws Exception {
final X509Certificate result = validator.validate(validAuthToken, VALID_CHALLENGE_NONCE);
@@ -91,4 +99,43 @@ void whenTokenWithWrongCert_thenValidationFails() throws Exception {
}
}
+ @Test
+ void whenValidV11TokenAndNonce_thenValidationSucceeds() throws Exception {
+ final X509Certificate result = validator.validate(validV11AuthToken, VALID_CHALLENGE_NONCE);
+
+ assertThat(CertificateData.getSubjectCN(result).orElseThrow())
+ .isEqualTo("JÕEORG\\,JAAK-KRISTJAN\\,38001085718");
+ assertThat(CertificateData.getSubjectIdCode(result).orElseThrow())
+ .isEqualTo("PNOEE-38001085718");
+ }
+
+ @Test
+ void whenV11TokenWithWrongChallengeNonce_thenValidationFails() {
+ final String invalidChallengeNonce = "12345678123456781234567812345678912356789124";
+ assertThatThrownBy(() -> validator
+ .validate(validV11AuthToken, invalidChallengeNonce))
+ .isInstanceOf(AuthTokenSignatureValidationException.class);
+ }
+
+ @Test
+ void whenV11TokenWithWrongOrigin_thenValidationFails() throws Exception {
+ final AuthTokenValidator validatorWithWrongOrigin =
+ AuthTokenValidators.getAuthTokenValidator("https://wrong-origin.com");
+
+ assertThatThrownBy(() -> validatorWithWrongOrigin
+ .validate(validV11AuthToken, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenSignatureValidationException.class);
+ }
+
+ @Test
+ void whenV11TokenWithWrongCert_thenValidationFails() throws Exception {
+ try (final var mockedClock = mockStatic(DateAndTime.DefaultClock.class)) {
+ mockDate("2024-08-01", mockedClock);
+ final AuthTokenValidator validator = AuthTokenValidators.getAuthTokenValidator();
+ final WebEidAuthToken token = validator.parse(V11_AUTH_TOKEN_WRONG_CERT);
+ assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenSignatureValidationException.class);
+ }
+ }
+
}
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java
index fc7edd0c..1205dab0 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java
@@ -24,16 +24,16 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
+import eu.webeid.security.authtoken.WebEidAuthToken;
import eu.webeid.security.certificate.CertificateLoader;
import org.junit.jupiter.api.Test;
-import eu.webeid.security.authtoken.WebEidAuthToken;
import java.net.URI;
import java.security.cert.X509Certificate;
-import static org.assertj.core.api.Assertions.assertThatCode;
import static eu.webeid.security.validator.AuthTokenSignatureTest.VALID_AUTH_TOKEN;
import static eu.webeid.security.validator.AuthTokenSignatureTest.VALID_CHALLENGE_NONCE;
+import static org.assertj.core.api.Assertions.assertThatCode;
class AuthTokenSignatureValidatorTest {
@@ -45,6 +45,14 @@ class AuthTokenSignatureValidatorTest {
"\"signature\":\"xsjXsQvVYXWcdV0YPhxLthJxtf0//R8p9WFFlYJGRARrl1ruyoAUwl0xeHgeZOKeJtwiCYCNWJzCG3VM3ydgt92bKhhk1u0JXIPVqvOkmDY72OCN4q73Y8iGSPVTgjk93TgquHlodf7YcqZNhutwNNf3oldHEWJD5zmkdwdpBFXgeOwTAdFwGljDQZbHr3h1Dr+apUDuloS0WuIzUuu8YXN2b8lh8FCTlF0G0DEjhHd/MGx8dbe3UTLHmD7K9DXv4zLJs6EF9i2v/C10SIBQDkPBSVPqMxCDPECjbEPi2+ds94eU7ThOhOQlFFtJ4KjQNTUa2crSixH7cYZF2rNNmA==\"," +
"\"format\":\"web-eid:1.0\"}";
+ private static final String VALID_V11_RS256_AUTH_TOKEN = "{\"algorithm\":\"RS256\"," +
+ "\"unverifiedCertificate\":\"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\"," +
+ "\"unverifiedSigningCertificate\":\"X5C\"," +
+ "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," +
+ "\"issuerApp\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," +
+ "\"signature\":\"xsjXsQvVYXWcdV0YPhxLthJxtf0//R8p9WFFlYJGRARrl1ruyoAUwl0xeHgeZOKeJtwiCYCNWJzCG3VM3ydgt92bKhhk1u0JXIPVqvOkmDY72OCN4q73Y8iGSPVTgjk93TgquHlodf7YcqZNhutwNNf3oldHEWJD5zmkdwdpBFXgeOwTAdFwGljDQZbHr3h1Dr+apUDuloS0WuIzUuu8YXN2b8lh8FCTlF0G0DEjhHd/MGx8dbe3UTLHmD7K9DXv4zLJs6EF9i2v/C10SIBQDkPBSVPqMxCDPECjbEPi2+ds94eU7ThOhOQlFFtJ4KjQNTUa2crSixH7cYZF2rNNmA==\"," +
+ "\"format\":\"web-eid:1.1\"}";
+
@Test
void whenValidES384Signature_thenSucceeds() throws Exception {
final AuthTokenSignatureValidator signatureValidator =
@@ -71,4 +79,17 @@ void whenValidRS256Signature_thenSucceeds() throws Exception {
.doesNotThrowAnyException();
}
+ @Test
+ void whenValidRS256V11Signature_thenSucceeds() throws Exception {
+ final AuthTokenSignatureValidator signatureValidator =
+ new AuthTokenSignatureValidator(URI.create("https://ria.ee"));
+
+ final WebEidAuthToken authToken = OBJECT_READER.readValue(VALID_V11_RS256_AUTH_TOKEN);
+ final X509Certificate x509Certificate = CertificateLoader.decodeCertificateFromBase64(authToken.getUnverifiedCertificate());
+
+ assertThatCode(() -> signatureValidator
+ .validate("RS256", authToken.getSignature(), x509Certificate.getPublicKey(), VALID_CHALLENGE_NONCE))
+ .doesNotThrowAnyException();
+ }
+
}
diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenStructureTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenStructureTest.java
index be79c0cb..4b38f822 100644
--- a/src/test/java/eu/webeid/security/validator/AuthTokenStructureTest.java
+++ b/src/test/java/eu/webeid/security/validator/AuthTokenStructureTest.java
@@ -23,7 +23,6 @@
package eu.webeid.security.validator;
import eu.webeid.security.authtoken.WebEidAuthToken;
-import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.security.exceptions.AuthTokenParseException;
import eu.webeid.security.testutil.AbstractTestWithValidator;
import org.junit.jupiter.api.Test;
@@ -65,11 +64,10 @@ void whenTokenTooLong_thenParsingFails() {
}
@Test
- void whenUnknownTokenVersion_thenParsingFails() throws AuthTokenException {
- final WebEidAuthToken token = replaceTokenField(VALID_AUTH_TOKEN, "web-eid:1", "invalid");
- assertThatThrownBy(() -> validator
- .validate(token, ""))
+ void whenUnknownTokenVersion_thenParsingFails() throws Exception {
+ WebEidAuthToken token = replaceTokenField(VALID_AUTH_TOKEN, "web-eid:1", "invalid");
+ assertThatThrownBy(() -> validator.validate(token, "nonce"))
.isInstanceOf(AuthTokenParseException.class)
- .hasMessage("Only token format version 'web-eid:1' is currently supported");
+ .hasMessage("Token format version 'invalid.0' is currently not supported");
}
}
diff --git a/src/test/java/eu/webeid/security/validator/ocsp/OcspServiceProviderTest.java b/src/test/java/eu/webeid/security/validator/ocsp/OcspServiceProviderTest.java
index 00337fd9..c038f2a2 100644
--- a/src/test/java/eu/webeid/security/validator/ocsp/OcspServiceProviderTest.java
+++ b/src/test/java/eu/webeid/security/validator/ocsp/OcspServiceProviderTest.java
@@ -22,18 +22,24 @@
package eu.webeid.security.validator.ocsp;
-import org.bouncycastle.cert.X509CertificateHolder;
-import org.junit.jupiter.api.Test;
import eu.webeid.security.exceptions.OCSPCertificateException;
import eu.webeid.security.validator.ocsp.service.OcspService;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.junit.jupiter.api.Test;
import java.net.URI;
import java.util.Date;
-import static org.assertj.core.api.Assertions.*;
-import static eu.webeid.security.testutil.Certificates.*;
+import static eu.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert;
+import static eu.webeid.security.testutil.Certificates.getMariliisEsteid2015Cert;
+import static eu.webeid.security.testutil.Certificates.getTestEsteid2015CA;
+import static eu.webeid.security.testutil.Certificates.getTestEsteid2018CA;
+import static eu.webeid.security.testutil.Certificates.getTestSkOcspResponder2020;
import static eu.webeid.security.testutil.OcspServiceMaker.getAiaOcspServiceProvider;
import static eu.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceProvider;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class OcspServiceProviderTest {
diff --git a/src/test/java/eu/webeid/security/validator/ocsp/OcspUrlTest.java b/src/test/java/eu/webeid/security/validator/ocsp/OcspUrlTest.java
index 95b5759e..d6f4b7b6 100644
--- a/src/test/java/eu/webeid/security/validator/ocsp/OcspUrlTest.java
+++ b/src/test/java/eu/webeid/security/validator/ocsp/OcspUrlTest.java
@@ -26,11 +26,11 @@
import java.security.cert.X509Certificate;
+import static eu.webeid.security.validator.ocsp.OcspUrl.getOcspUri;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import static eu.webeid.security.validator.ocsp.OcspUrl.getOcspUri;
class OcspUrlTest {
diff --git a/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenV11CertificateTest.java b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenV11CertificateTest.java
new file mode 100644
index 00000000..eb97b5d6
--- /dev/null
+++ b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenV11CertificateTest.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (c) 2020-2025 Estonian Information System Authority
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package eu.webeid.security.validator.versionvalidators;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import eu.webeid.security.authtoken.WebEidAuthToken;
+import eu.webeid.security.certificate.CertificateLoader;
+import eu.webeid.security.exceptions.AuthTokenParseException;
+import eu.webeid.security.exceptions.CertificateDecodingException;
+import eu.webeid.security.testutil.AbstractTestWithValidator;
+import eu.webeid.security.util.DateAndTime;
+import eu.webeid.security.validator.AuthTokenSignatureValidator;
+import eu.webeid.security.validator.AuthTokenValidationConfiguration;
+import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
+import eu.webeid.security.validator.ocsp.OcspClient;
+import eu.webeid.security.validator.ocsp.OcspServiceProvider;
+import org.bouncycastle.asn1.x509.Extension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.security.cert.CertStore;
+import java.security.cert.CertificateException;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Set;
+
+import static eu.webeid.security.testutil.DateMocker.mockDate;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+class AuthTokenV11CertificateTest extends AbstractTestWithValidator {
+
+ private static final String V11_AUTH_TOKEN = "{\"algorithm\":\"ES384\"," +
+ "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," +
+ "\"unverifiedSigningCertificate\":\"X5C\"," +
+ "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," +
+ "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," +
+ "\"signature\":\"xsjXsQvVYXWcdV0YPhxLthJxtf0//R8p9WFFlYJGRARrl1ruyoAUwl0xeHgeZOKeJtwiCYCNWJzCG3VM3ydgt92bKhhk1u0JXIPVqvOkmDY72OCN4q73Y8iGSPVTgjk93TgquHlodf7YcqZNhutwNNf3oldHEWJD5zmkdwdpBFXgeOwTAdFwGljDQZbHr3h1Dr+apUDuloS0WuIzUuu8YXN2b8lh8FCTlF0G0DEjhHd/MGx8dbe3UTLHmD7K9DXv4zLJs6EF9i2v/C10SIBQDkPBSVPqMxCDPECjbEPi2+ds94eU7ThOhOQlFFtJ4KjQNTUa2crSixH7cYZF2rNNmA==\"," +
+ "\"format\":\"web-eid:1.1\"}";
+
+ private static final String DIFFERENT_CERT = "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";
+
+ private MockedStatic mockedClock;
+ private static final ObjectReader OBJECT_READER = new ObjectMapper().readerFor(WebEidAuthToken.class);
+ private SubjectCertificateValidatorBatch scvb;
+ private Set trustedCACertificateAnchors;
+ private CertStore trustedCACertificateCertStore;
+ private AuthTokenSignatureValidator signatureValidator;
+ private AuthTokenValidationConfiguration configuration;
+ private OcspClient ocspClient;
+ private OcspServiceProvider ocspServiceProvider;
+
+ @Override
+ @BeforeEach
+ protected void setup() {
+ super.setup();
+ mockedClock = mockStatic(DateAndTime.DefaultClock.class);
+ // Ensure that the certificates do not expire.
+ mockDate("2021-08-01", mockedClock);
+ scvb = mock(SubjectCertificateValidatorBatch.class);
+ trustedCACertificateAnchors = Collections.emptySet();
+ trustedCACertificateCertStore = mock(CertStore.class);
+ signatureValidator = mock(AuthTokenSignatureValidator.class);
+ configuration = mock(AuthTokenValidationConfiguration.class);
+ ocspClient = mock(OcspClient.class);
+ ocspServiceProvider = mock(OcspServiceProvider.class);
+ }
+
+ @AfterEach
+ void tearDown() {
+ mockedClock.close();
+ }
+
+ @Test
+ void whenValidV11Token_thenValidationSucceeds() {
+ mockDate("2023-10-01", mockedClock);
+ assertThatCode(() -> validator
+ .validate(validV11AuthToken, VALID_CHALLENGE_NONCE))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void whenV11SigningCertificateFieldIsMissing_thenValidationFails() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode node = (ObjectNode) mapper.readTree(V11_AUTH_TOKEN);
+ node.remove("unverifiedSigningCertificate");
+ WebEidAuthToken token = OBJECT_READER.readValue(node.toString());
+
+ AuthTokenVersion11Validator spyValidator = spyAuthTokenVersion11Validator();
+ doReturn(mock(X509Certificate.class)).when(spyValidator).validateV1(any(), any());
+
+ assertThatThrownBy(() -> spyValidator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'");
+ }
+
+ @Test
+ void whenV11SigningCertificateIsNotBase64_thenValidationFails() throws Exception {
+ AuthTokenVersion11Validator spyValidator = spyAuthTokenVersion11Validator();
+ X509Certificate mockSubjectCert = CertificateLoader.decodeCertificateFromBase64(OBJECT_READER.readValue(V11_AUTH_TOKEN, WebEidAuthToken.class).getUnverifiedCertificate());
+ doReturn(mockSubjectCert).when(spyValidator).validateV1(any(), any());
+ WebEidAuthToken token = getWebEidAuthToken("This is not a certificate");
+
+ assertThatThrownBy(() -> spyValidator
+ .validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(CertificateDecodingException.class)
+ .cause()
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Illegal base64 character");
+ }
+
+ @Test
+ void whenV11SigningCertificateIsNotACertificate_thenValidationFails() throws Exception {
+ AuthTokenVersion11Validator spyValidator = spyAuthTokenVersion11Validator();
+ X509Certificate mockSubjectCert = CertificateLoader.decodeCertificateFromBase64(OBJECT_READER.readValue(V11_AUTH_TOKEN, WebEidAuthToken.class).getUnverifiedCertificate());
+ doReturn(mockSubjectCert).when(spyValidator).validateV1(any(), any());
+ WebEidAuthToken token = getWebEidAuthToken("VGhpcyBpcyBub3QgYSBjZXJ0aWZpY2F0ZQ");
+
+ assertThatThrownBy(() -> spyValidator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(CertificateDecodingException.class)
+ .cause()
+ .isInstanceOf(CertificateException.class)
+ .hasMessage("Could not parse certificate: java.io.IOException: Empty input");
+ }
+
+ @Test
+ void whenV11SigningCertificateSubjectDoesNotMatch_thenValidationFails() throws Exception {
+ AuthTokenVersion11Validator spyValidator = spyAuthTokenVersion11Validator();
+ X509Certificate mockSubjectCert = CertificateLoader.decodeCertificateFromBase64(OBJECT_READER.readValue(V11_AUTH_TOKEN, WebEidAuthToken.class).getUnverifiedCertificate());
+ doReturn(mockSubjectCert).when(spyValidator).validateV1(any(), any());
+ WebEidAuthToken token = getWebEidAuthToken(DIFFERENT_CERT);
+
+ assertThatThrownBy(() -> spyValidator.validate(token, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("Signing certificate subject does not match authentication certificate subject");
+ }
+
+ @Test
+ void whenV11SigningCertificateNotIssuedBySameAuthority_thenValidationFails() throws Exception {
+ AuthTokenVersion11Validator spyValidator = spyAuthTokenVersion11Validator();
+ WebEidAuthToken parsedToken = OBJECT_READER.readValue(V11_AUTH_TOKEN, WebEidAuthToken.class);
+ X509Certificate realSubjectCert = CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate());
+ doReturn(realSubjectCert).when(spyValidator).validateV1(any(), any());
+
+ X509Certificate mockSigningCert = mock(X509Certificate.class);
+ when(mockSigningCert.getSubjectX500Principal()).thenReturn(realSubjectCert.getSubjectX500Principal());
+
+ byte[] realAki = realSubjectCert.getExtensionValue(Extension.authorityKeyIdentifier.getId());
+ byte[] differentAki = realAki.clone();
+ if (differentAki.length > 0) {
+ differentAki[differentAki.length - 1] ^= (byte) 0xFF;
+ }
+ when(mockSigningCert.getExtensionValue(Extension.authorityKeyIdentifier.getId())).thenReturn(differentAki);
+
+ try (MockedStatic mocked = mockStatic(CertificateLoader.class)) {
+ mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate()))
+ .thenReturn(realSubjectCert);
+ mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedSigningCertificate()))
+ .thenReturn(mockSigningCert);
+
+ assertThatThrownBy(() -> spyValidator.validate(parsedToken, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("Signing certificate is not issued by the same issuing authority as the authentication certificate");
+ }
+ }
+
+ @Test
+ void whenV11SigningCertificateHasNoAuthorityKeyIdentifier_thenValidationFails() throws Exception {
+ AuthTokenVersion11Validator spyValidator = spyAuthTokenVersion11Validator();
+ WebEidAuthToken parsedToken = OBJECT_READER.readValue(V11_AUTH_TOKEN, WebEidAuthToken.class);
+ X509Certificate realSubjectCert = CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate());
+ doReturn(realSubjectCert).when(spyValidator).validateV1(any(), any());
+
+ X509Certificate mockSigningCert = mock(X509Certificate.class);
+ when(mockSigningCert.getSubjectX500Principal()).thenReturn(realSubjectCert.getSubjectX500Principal());
+ when(mockSigningCert.getExtensionValue(Extension.authorityKeyIdentifier.getId())).thenReturn(null);
+
+ try (MockedStatic mocked = mockStatic(CertificateLoader.class)) {
+ mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate()))
+ .thenReturn(realSubjectCert);
+ mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedSigningCertificate()))
+ .thenReturn(mockSigningCert);
+
+ assertThatThrownBy(() -> spyValidator.validate(parsedToken, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("Signing certificate is not issued by the same issuing authority as the authentication certificate");
+ }
+ }
+
+ @Test
+ void whenV11SigningCertificateNotSuitableForSigning_thenValidationFails() throws Exception {
+ AuthTokenVersion11Validator spyValidator = spyAuthTokenVersion11Validator();
+ WebEidAuthToken parsedToken = OBJECT_READER.readValue(V11_AUTH_TOKEN, WebEidAuthToken.class);
+ X509Certificate realSubjectCert = CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate());
+ doReturn(realSubjectCert).when(spyValidator).validateV1(any(), any());
+
+ X509Certificate signingCert = mock(X509Certificate.class);
+ when(signingCert.getSubjectX500Principal()).thenReturn(realSubjectCert.getSubjectX500Principal());
+ when(signingCert.getIssuerX500Principal()).thenReturn(realSubjectCert.getIssuerX500Principal());
+ when(signingCert.getExtensionValue(Extension.authorityKeyIdentifier.getId()))
+ .thenReturn(realSubjectCert.getExtensionValue(Extension.authorityKeyIdentifier.getId()));
+ when(signingCert.getKeyUsage()).thenReturn(new boolean[]{true, false});
+
+ try (MockedStatic mocked = mockStatic(CertificateLoader.class)) {
+ mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedCertificate()))
+ .thenReturn(realSubjectCert);
+ mocked.when(() -> CertificateLoader.decodeCertificateFromBase64(parsedToken.getUnverifiedSigningCertificate()))
+ .thenReturn(signingCert);
+
+ assertThatThrownBy(() -> spyValidator.validate(parsedToken, VALID_CHALLENGE_NONCE))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("Signing certificate key usage extension missing or does not contain non-repudiation bit required for digital signatures");
+ }
+ }
+
+ private AuthTokenVersion11Validator spyAuthTokenVersion11Validator() {
+ return Mockito.spy(new AuthTokenVersion11Validator(
+ scvb,
+ trustedCACertificateAnchors,
+ trustedCACertificateCertStore,
+ signatureValidator,
+ configuration,
+ ocspClient,
+ ocspServiceProvider
+ ));
+ }
+
+ private static WebEidAuthToken getWebEidAuthToken(String cert) throws JsonProcessingException {
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode node = (ObjectNode) mapper.readTree(V11_AUTH_TOKEN);
+ node.put("unverifiedSigningCertificate", cert);
+ return OBJECT_READER.readValue(node.toString());
+ }
+
+}
diff --git a/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java
new file mode 100644
index 00000000..c311363b
--- /dev/null
+++ b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2020-2025 Estonian Information System Authority
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package eu.webeid.security.validator.versionvalidators;
+
+import eu.webeid.security.authtoken.WebEidAuthToken;
+import eu.webeid.security.certificate.CertificateLoader;
+import eu.webeid.security.exceptions.AuthTokenParseException;
+import eu.webeid.security.validator.AuthTokenSignatureValidator;
+import eu.webeid.security.validator.AuthTokenValidationConfiguration;
+import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
+import eu.webeid.security.validator.ocsp.OcspClient;
+import eu.webeid.security.validator.ocsp.OcspServiceProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.security.cert.CertStore;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+class AuthTokenVersion11ValidatorTest {
+
+ private AuthTokenVersion11Validator validator;
+
+ @BeforeEach
+ void setUp() {
+ SubjectCertificateValidatorBatch scvb = mock(SubjectCertificateValidatorBatch.class);
+ Set trustAnchors = Collections.emptySet();
+ CertStore certStore = mock(CertStore.class);
+ AuthTokenSignatureValidator signatureValidator = mock(AuthTokenSignatureValidator.class);
+ AuthTokenValidationConfiguration config = mock(AuthTokenValidationConfiguration.class);
+ OcspClient ocspClient = mock(OcspClient.class);
+ OcspServiceProvider ocspServiceProvider = mock(OcspServiceProvider.class);
+
+ validator = new AuthTokenVersion11Validator(
+ scvb,
+ trustAnchors,
+ certStore,
+ signatureValidator,
+ config,
+ ocspClient,
+ ocspServiceProvider
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"web-eid:1.1", "web-eid:1.1.0", "web-eid:1.10"})
+ void whenFormatIsV11OrPrefixedVariant_thenSupportsReturnsTrue(String format) {
+ assertThat(validator.supports(format)).isTrue();
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:2", "webauthn:1.1"})
+ void whenFormatIsNullEmptyOrNotV11_thenSupportsReturnsFalse(String format) {
+ assertThat(validator.supports(format)).isFalse();
+ }
+
+ @Test
+ void whenUnverifiedSigningCertificateMissing_thenValidationFails() throws Exception {
+ WebEidAuthToken token = mock(WebEidAuthToken.class);
+ when(token.getFormat()).thenReturn("web-eid:1.1");
+ when(token.getUnverifiedSigningCertificate()).thenReturn(null);
+
+ AuthTokenVersion11Validator spyValidator = Mockito.spy(validator);
+ doReturn(mock(X509Certificate.class)).when(spyValidator).validateV1(any(), any());
+
+ assertThatThrownBy(() -> spyValidator.validate(token, "nonce"))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'");
+ }
+
+ @Test
+ void whenSupportedSignatureAlgorithmsMissing_thenValidationFails() throws Exception {
+ WebEidAuthToken token = mock(WebEidAuthToken.class);
+ when(token.getFormat()).thenReturn("web-eid:1.1");
+ when(token.getUnverifiedSigningCertificate()).thenReturn("abc");
+ when(token.getSupportedSignatureAlgorithms()).thenReturn(null);
+
+ AuthTokenVersion11Validator spyValidator = Mockito.spy(validator);
+ doReturn(mock(X509Certificate.class)).when(spyValidator).validateV1(any(), any());
+
+ try (MockedStatic mocked = mockStatic(CertificateLoader.class)) {
+ mocked.when(() -> CertificateLoader.decodeCertificateFromBase64("abc"))
+ .thenReturn(mock(X509Certificate.class));
+
+ assertThatThrownBy(() -> spyValidator.validate(token, "nonce"))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("'supportedSignatureAlgorithms' field is missing");
+ }
+ }
+}
diff --git a/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java
new file mode 100644
index 00000000..44af1342
--- /dev/null
+++ b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2020-2025 Estonian Information System Authority
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package eu.webeid.security.validator.versionvalidators;
+
+import eu.webeid.security.authtoken.WebEidAuthToken;
+import eu.webeid.security.exceptions.AuthTokenParseException;
+import eu.webeid.security.validator.AuthTokenSignatureValidator;
+import eu.webeid.security.validator.AuthTokenValidationConfiguration;
+import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
+import eu.webeid.security.validator.ocsp.OcspClient;
+import eu.webeid.security.validator.ocsp.OcspServiceProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.security.cert.CertStore;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class AuthTokenVersion1ValidatorTest {
+
+ private final SubjectCertificateValidatorBatch scvb = mock(SubjectCertificateValidatorBatch.class);
+ private final AuthTokenSignatureValidator signatureValidator = mock(AuthTokenSignatureValidator.class);
+ private final AuthTokenValidationConfiguration config = mock(AuthTokenValidationConfiguration.class);
+
+ private final AuthTokenVersion1Validator validator = new AuthTokenVersion1Validator(
+ scvb,
+ Set.of(),
+ mock(CertStore.class),
+ signatureValidator,
+ config,
+ mock(OcspClient.class),
+ mock(OcspServiceProvider.class)
+ );
+
+ @ParameterizedTest
+ @ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:1.1", "web-eid:1.10"})
+ void whenFormatIsAnyMajorV1Variant_thenSupportsReturnsTrue(String format) {
+ assertThat(validator.supports(format)).isTrue();
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = {"web-eid", "web-eid:0.9", "web-eid:2", "webauthn:1"})
+ void whenFormatIsNullEmptyOrNotV1_thenSupportsReturnsFalse(String format) {
+ assertThat(validator.supports(format)).isFalse();
+ }
+
+ @Test
+ void whenUnverifiedCertificateMissing_thenValidationFails() {
+ WebEidAuthToken token = mock(WebEidAuthToken.class);
+ when(token.getFormat()).thenReturn("web-eid:1");
+ when(token.getUnverifiedCertificate()).thenReturn(null);
+
+ assertThatThrownBy(() -> validator.validate(token, "nonce"))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessageContaining("'unverifiedCertificate' field is missing");
+ }
+}
diff --git a/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidatorFactoryTest.java b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidatorFactoryTest.java
new file mode 100644
index 00000000..a02b16e2
--- /dev/null
+++ b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidatorFactoryTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2020-2025 Estonian Information System Authority
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package eu.webeid.security.validator.versionvalidators;
+
+import eu.webeid.security.exceptions.AuthTokenParseException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class AuthTokenVersionValidatorFactoryTest {
+
+ @Test
+ void whenValidatorSupportsFormat_thenSupportsReturnsTrue() {
+ AuthTokenVersionValidator v11 = mock(AuthTokenVersionValidator.class);
+ when(v11.supports("web-eid:1.1")).thenReturn(true);
+
+ AuthTokenVersionValidatorFactory factory = new AuthTokenVersionValidatorFactory(List.of(v11));
+
+ assertThat(factory.supports("web-eid:1.1")).isTrue();
+ }
+
+ @Test
+ void whenValidatorDoesNotSupportFormat_thenSupportsReturnsFalse() {
+ AuthTokenVersionValidator v11 = mock(AuthTokenVersionValidator.class);
+ when(v11.supports("web-eid:1.1")).thenReturn(false);
+
+ AuthTokenVersionValidatorFactory factory = new AuthTokenVersionValidatorFactory(List.of(v11));
+
+ assertThat(factory.supports("web-eid:2")).isFalse();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"web-eid:0.9", "web-eid:2", "foo", "1", "web-eid"})
+ void whenUnsupportedFormat_thenGetValidatorForThrows(String format) {
+ AuthTokenVersionValidatorFactory factory = new AuthTokenVersionValidatorFactory(List.of());
+ assertThatThrownBy(() -> factory.getValidatorFor(format))
+ .isInstanceOf(AuthTokenParseException.class)
+ .hasMessage("Token format version '" + format + "' is currently not supported");
+ }
+
+ @Test
+ void whenMultipleValidatorsAndFirstIsV11_thenGetValidatorForReturnsV11() throws AuthTokenParseException {
+ AuthTokenVersionValidator v11 = mock(AuthTokenVersionValidator.class);
+ when(v11.supports("web-eid:1.1")).thenReturn(true);
+
+ AuthTokenVersionValidator v1 = mock(AuthTokenVersionValidator.class);
+ when(v1.supports("web-eid:1")).thenReturn(true);
+
+ AuthTokenVersionValidatorFactory factory = new AuthTokenVersionValidatorFactory(List.of(v11, v1));
+
+ AuthTokenVersionValidator chosen = factory.getValidatorFor("web-eid:1.1");
+
+ assertThat(chosen).isSameAs(v11);
+ }
+
+ @Test
+ void whenFormatIsBaseV1_thenGetValidatorForReturnsV1() throws AuthTokenParseException {
+ AuthTokenVersionValidator v11 = mock(AuthTokenVersionValidator.class);
+ when(v11.supports("web-eid:1.1")).thenReturn(true);
+
+ AuthTokenVersionValidator v1 = mock(AuthTokenVersionValidator.class);
+ when(v1.supports("web-eid:1")).thenReturn(true);
+
+ AuthTokenVersionValidatorFactory factory = new AuthTokenVersionValidatorFactory(List.of(v11, v1));
+
+ AuthTokenVersionValidator chosen = factory.getValidatorFor("web-eid:1");
+
+ assertThat(chosen).isSameAs(v1);
+ }
+}