From 67e25bd099a020be253a0334999df900d6345f75 Mon Sep 17 00:00:00 2001 From: "Sander.Kondratjev" Date: Fri, 20 Jun 2025 10:30:58 +0300 Subject: [PATCH] NFC-46 Add web-eid-1.1 token support Signed-off-by: Sander Kondratjev --- .../SupportedSignatureAlgorithm.java | 53 ++++ .../security/authtoken/WebEidAuthToken.java | 20 ++ .../challenge/ChallengeNonceStore.java | 2 +- .../AuthTokenValidationConfiguration.java | 10 +- .../validator/AuthTokenValidator.java | 2 - .../validator/AuthTokenValidatorBuilder.java | 2 +- .../validator/AuthTokenValidatorImpl.java | 193 ------------- .../validator/AuthTokenValidatorManager.java | 107 +++++++ .../SubjectCertificatePolicyValidator.java | 4 +- .../SubjectCertificatePurposeValidator.java | 6 +- .../SubjectCertificateValidatorBatch.java | 36 +++ .../ocsp/service/DesignatedOcspService.java | 4 +- .../DesignatedOcspServiceConfiguration.java | 2 +- .../validator/ocsp/service/OcspService.java | 2 +- .../AuthTokenVersion11Validator.java | 190 +++++++++++++ .../AuthTokenVersion1Validator.java | 108 +++++++ .../AuthTokenVersionValidator.java | 54 ++++ .../AuthTokenVersionValidatorFactory.java | 113 ++++++++ .../ChallengeNonceGeneratorTest.java | 6 +- .../testutil/AbstractTestWithValidator.java | 25 +- .../validator/AuthTokenAlgorithmTest.java | 65 ++++- ...AuthTokenCertificateBelgianIdCardTest.java | 13 +- ...AuthTokenCertificateFinnishIdCardTest.java | 13 +- .../validator/AuthTokenCertificateTest.java | 1 - .../validator/AuthTokenSignatureTest.java | 47 +++ .../AuthTokenSignatureValidatorTest.java | 25 +- .../validator/AuthTokenStructureTest.java | 10 +- .../ocsp/OcspServiceProviderTest.java | 14 +- .../security/validator/ocsp/OcspUrlTest.java | 2 +- .../AuthTokenV11CertificateTest.java | 267 ++++++++++++++++++ .../AuthTokenVersion11ValidatorTest.java | 126 +++++++++ .../AuthTokenVersion1ValidatorTest.java | 84 ++++++ .../AuthTokenVersionValidatorFactoryTest.java | 97 +++++++ 33 files changed, 1458 insertions(+), 245 deletions(-) create mode 100644 src/main/java/eu/webeid/security/authtoken/SupportedSignatureAlgorithm.java delete mode 100644 src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java create mode 100644 src/main/java/eu/webeid/security/validator/AuthTokenValidatorManager.java create mode 100644 src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java create mode 100644 src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java create mode 100644 src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidator.java create mode 100644 src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidatorFactory.java create mode 100644 src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenV11CertificateTest.java create mode 100644 src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java create mode 100644 src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java create mode 100644 src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersionValidatorFactoryTest.java diff --git a/src/main/java/eu/webeid/security/authtoken/SupportedSignatureAlgorithm.java b/src/main/java/eu/webeid/security/authtoken/SupportedSignatureAlgorithm.java new file mode 100644 index 00000000..4bc8263f --- /dev/null +++ b/src/main/java/eu/webeid/security/authtoken/SupportedSignatureAlgorithm.java @@ -0,0 +1,53 @@ +/* + * 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.authtoken; + +public class SupportedSignatureAlgorithm { + private String cryptoAlgorithm; + private String hashFunction; + private String paddingScheme; + + public String getCryptoAlgorithm() { + return cryptoAlgorithm; + } + + public void setCryptoAlgorithm(String cryptoAlgorithm) { + this.cryptoAlgorithm = cryptoAlgorithm; + } + + public String getHashFunction() { + return hashFunction; + } + + public void setHashFunction(String hashFunction) { + this.hashFunction = hashFunction; + } + + public String getPaddingScheme() { + return paddingScheme; + } + + public void setPaddingScheme(String paddingScheme) { + this.paddingScheme = paddingScheme; + } +} diff --git a/src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java b/src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java index 77d80bc4..3ba0b8a8 100644 --- a/src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java +++ b/src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java @@ -24,6 +24,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + @JsonIgnoreProperties(ignoreUnknown = true) public class WebEidAuthToken { @@ -32,6 +34,9 @@ public class WebEidAuthToken { private String algorithm; private String format; + private String unverifiedSigningCertificate; + private List supportedSignatureAlgorithms; + public String getUnverifiedCertificate() { return unverifiedCertificate; } @@ -64,4 +69,19 @@ public void setFormat(String format) { this.format = format; } + public String getUnverifiedSigningCertificate() { + return unverifiedSigningCertificate; + } + + public void setUnverifiedSigningCertificate(String unverifiedSigningCertificate) { + this.unverifiedSigningCertificate = unverifiedSigningCertificate; + } + + public List getSupportedSignatureAlgorithms() { + return supportedSignatureAlgorithms; + } + + public void setSupportedSignatureAlgorithms(List supportedSignatureAlgorithms) { + this.supportedSignatureAlgorithms = supportedSignatureAlgorithms; + } } diff --git a/src/main/java/eu/webeid/security/challenge/ChallengeNonceStore.java b/src/main/java/eu/webeid/security/challenge/ChallengeNonceStore.java index 2631cf86..5b5f8d77 100644 --- a/src/main/java/eu/webeid/security/challenge/ChallengeNonceStore.java +++ b/src/main/java/eu/webeid/security/challenge/ChallengeNonceStore.java @@ -22,8 +22,8 @@ package eu.webeid.security.challenge; -import eu.webeid.security.exceptions.ChallengeNonceExpiredException; import eu.webeid.security.exceptions.AuthTokenException; +import eu.webeid.security.exceptions.ChallengeNonceExpiredException; import eu.webeid.security.exceptions.ChallengeNonceNotFoundException; import static eu.webeid.security.util.DateAndTime.utcNow; diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java index 6b943bd2..a48c1266 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java @@ -40,7 +40,7 @@ import static eu.webeid.security.util.DateAndTime.requirePositiveDuration; /** - * Stores configuration parameters for {@link AuthTokenValidatorImpl}. + * Stores configuration parameters for {@link AuthTokenValidatorManager}. */ public final class AuthTokenValidationConfiguration { @@ -79,15 +79,15 @@ void setSiteOrigin(URI siteOrigin) { this.siteOrigin = siteOrigin; } - URI getSiteOrigin() { + public URI getSiteOrigin() { return siteOrigin; } - Collection getTrustedCACertificates() { + public Collection getTrustedCACertificates() { return trustedCACertificates; } - boolean isUserCertificateRevocationCheckWithOcspEnabled() { + public boolean isUserCertificateRevocationCheckWithOcspEnabled() { return isUserCertificateRevocationCheckWithOcspEnabled; } @@ -152,7 +152,7 @@ void validate() { requirePositiveDuration(maxOcspResponseThisUpdateAge, "Max OCSP response thisUpdate age"); } - AuthTokenValidationConfiguration copy() { + public AuthTokenValidationConfiguration copy() { return new AuthTokenValidationConfiguration(this); } diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java index 3476ea41..588d9d7a 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java @@ -34,8 +34,6 @@ */ public interface AuthTokenValidator { - String CURRENT_TOKEN_FORMAT_VERSION = "web-eid:1"; - /** * Parses the Web eID authentication token signed by the subject. * diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java index 9122ee67..2e7ae1ab 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java @@ -214,7 +214,7 @@ public AuthTokenValidator build() throws NullPointerException, IllegalArgumentEx if (configuration.isUserCertificateRevocationCheckWithOcspEnabled() && ocspClient == null) { ocspClient = OcspClientImpl.build(configuration.getOcspRequestTimeout()); } - return new AuthTokenValidatorImpl(configuration, ocspClient); + return new AuthTokenValidatorManager(configuration, ocspClient); } } diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java deleted file mode 100644 index 14cf3e78..00000000 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * 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; - -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 eu.webeid.security.certificate.CertificateValidator; -import eu.webeid.security.exceptions.AuthTokenException; -import eu.webeid.security.exceptions.AuthTokenParseException; -import eu.webeid.security.exceptions.JceException; -import eu.webeid.security.validator.certvalidators.SubjectCertificateNotRevokedValidator; -import eu.webeid.security.validator.certvalidators.SubjectCertificatePolicyValidator; -import eu.webeid.security.validator.certvalidators.SubjectCertificatePurposeValidator; -import eu.webeid.security.validator.certvalidators.SubjectCertificateTrustedValidator; -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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.security.cert.CertStore; -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.Objects; -import java.util.Set; - -/** - * Provides the default implementation of {@link AuthTokenValidator}. - */ -final class AuthTokenValidatorImpl implements AuthTokenValidator { - - private static final int TOKEN_MIN_LENGTH = 100; - private static final int TOKEN_MAX_LENGTH = 10000; - private static final Logger LOG = LoggerFactory.getLogger(AuthTokenValidatorImpl.class); - - private static final ObjectReader OBJECT_READER = new ObjectMapper().readerFor(WebEidAuthToken.class); - - private final AuthTokenValidationConfiguration configuration; - private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators; - private final Set trustedCACertificateAnchors; - private final CertStore trustedCACertificateCertStore; - // OcspClient uses built-in HttpClient internally by default. - // A single HttpClient instance is reused for all HTTP calls to utilize connection and thread pools. - private OcspClient ocspClient; - private OcspServiceProvider ocspServiceProvider; - private final AuthTokenSignatureValidator authTokenSignatureValidator; - - /** - * @param configuration configuration parameters for the token validator - * @param ocspClient client for communicating with the OCSP service - */ - AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration, OcspClient ocspClient) throws JceException { - // Copy the configuration object to make AuthTokenValidatorImpl immutable and thread-safe. - this.configuration = configuration.copy(); - - // Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator and AiaOcspService. - trustedCACertificateAnchors = CertificateValidator.buildTrustAnchorsFromCertificates(configuration.getTrustedCACertificates()); - trustedCACertificateCertStore = CertificateValidator.buildCertStoreFromCertificates(configuration.getTrustedCACertificates()); - - simpleSubjectCertificateValidators = SubjectCertificateValidatorBatch.createFrom( - SubjectCertificatePurposeValidator::validateCertificatePurpose, - new SubjectCertificatePolicyValidator(configuration.getDisallowedSubjectCertificatePolicies())::validateCertificatePolicies - ); - - if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) { - // The OCSP client may be provided by the API consumer. - this.ocspClient = Objects.requireNonNull(ocspClient, "OCSP client must not be null when OCSP check is enabled"); - ocspServiceProvider = new OcspServiceProvider( - configuration.getDesignatedOcspServiceConfiguration(), - new AiaOcspServiceConfiguration(configuration.getNonceDisabledOcspUrls(), - trustedCACertificateAnchors, - trustedCACertificateCertStore)); - } - - authTokenSignatureValidator = new AuthTokenSignatureValidator(configuration.getSiteOrigin()); - } - - @Override - public WebEidAuthToken parse(String authToken) throws AuthTokenException { - try { - LOG.info("Starting token parsing"); - validateTokenLength(authToken); - return parseToken(authToken); - } catch (Exception e) { - // Generally "log and rethrow" is an anti-pattern, but it fits with the surrounding logging style. - LOG.warn("Token parsing was interrupted:", e); - throw e; - } - } - - @Override - public X509Certificate validate(WebEidAuthToken authToken, String currentChallengeNonce) throws AuthTokenException { - try { - LOG.info("Starting token validation"); - return validateToken(authToken, currentChallengeNonce); - } catch (Exception e) { - // Generally "log and rethrow" is an anti-pattern, but it fits with the surrounding logging style. - LOG.warn("Token validation was interrupted:", e); - throw e; - } - } - - private void validateTokenLength(String authToken) throws AuthTokenParseException { - if (authToken == null || authToken.length() < TOKEN_MIN_LENGTH) { - throw new AuthTokenParseException("Auth token is null or too short"); - } - if (authToken.length() > TOKEN_MAX_LENGTH) { - throw new AuthTokenParseException("Auth token is too long"); - } - } - - private WebEidAuthToken parseToken(String authToken) throws AuthTokenParseException { - try { - final WebEidAuthToken token = OBJECT_READER.readValue(authToken); - if (token == null) { - throw new AuthTokenParseException("Web eID authentication token is null"); - } - return token; - } catch (IOException e) { - throw new AuthTokenParseException("Error parsing Web eID authentication token", e); - } - } - - private X509Certificate validateToken(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException { - if (token.getFormat() == null || !token.getFormat().startsWith(CURRENT_TOKEN_FORMAT_VERSION)) { - throw new AuthTokenParseException("Only token format version '" + CURRENT_TOKEN_FORMAT_VERSION + - "' is currently supported"); - } - 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); - getCertTrustValidators().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; - } - - /** - * Creates the certificate trust validators batch. - * As SubjectCertificateTrustedValidator has mutable state that SubjectCertificateNotRevokedValidator depends on, - * they cannot be reused/cached in an instance variable in a multi-threaded environment. Hence, they are - * re-created for each validation run for thread safety. - * - * @return certificate trust validator batch - */ - private SubjectCertificateValidatorBatch getCertTrustValidators() { - final SubjectCertificateTrustedValidator certTrustedValidator = - new SubjectCertificateTrustedValidator(trustedCACertificateAnchors, trustedCACertificateCertStore); - return SubjectCertificateValidatorBatch.createFrom( - certTrustedValidator::validateCertificateTrusted - ).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(), - new SubjectCertificateNotRevokedValidator(certTrustedValidator, - ocspClient, ocspServiceProvider, - configuration.getAllowedOcspResponseTimeSkew(), - configuration.getMaxOcspResponseThisUpdateAge() - )::validateCertificateNotRevoked - ); - } - -} diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorManager.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorManager.java new file mode 100644 index 00000000..887161ee --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorManager.java @@ -0,0 +1,107 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import eu.webeid.security.authtoken.WebEidAuthToken; +import eu.webeid.security.exceptions.AuthTokenException; +import eu.webeid.security.exceptions.AuthTokenParseException; +import eu.webeid.security.exceptions.JceException; +import eu.webeid.security.validator.ocsp.OcspClient; +import eu.webeid.security.validator.versionvalidators.AuthTokenVersionValidatorFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.cert.X509Certificate; + +/** + * Provides the default implementation of {@link AuthTokenValidator}. + */ +final class AuthTokenValidatorManager implements AuthTokenValidator { + + private static final Logger LOG = LoggerFactory.getLogger(AuthTokenValidatorManager.class); + + private final AuthTokenVersionValidatorFactory tokenValidatorFactory; + + // Use human-readable meaningful names for token length limits. + private final int TOKEN_MIN_LENGTH = 100; + private final int TOKEN_MAX_LENGTH = 10000; + + private final ObjectReader TOKEN_READER = new ObjectMapper().readerFor(WebEidAuthToken.class); + + AuthTokenValidatorManager(AuthTokenValidationConfiguration configuration, OcspClient ocspClient) + throws JceException { + this.tokenValidatorFactory = AuthTokenVersionValidatorFactory.create(configuration, ocspClient); + } + + @Override + public WebEidAuthToken parse(String authToken) throws AuthTokenException { + try { + LOG.info("Starting token parsing"); + validateTokenLength(authToken); + return parseToken(authToken); + } catch (Exception e) { + // Generally "log and rethrow" is an antipattern, but it fits with the surrounding logging style. + LOG.warn("Token parsing was interrupted:", e); + throw e; + } + } + + @Override + public X509Certificate validate(WebEidAuthToken authToken, String currentChallengeNonce) throws AuthTokenException { + try { + LOG.info("Starting token validation"); + return tokenValidatorFactory + .getValidatorFor(authToken.getFormat()) + .validate(authToken, currentChallengeNonce); + } catch (Exception e) { + // Generally "log and rethrow" is an antipattern, but it fits with the surrounding logging style. + LOG.warn("Token validation was interrupted:", e); + throw e; + } + } + + private void validateTokenLength(String authToken) throws AuthTokenParseException { + if (authToken == null || authToken.length() < TOKEN_MIN_LENGTH) { + throw new AuthTokenParseException("Auth token is null or too short"); + } + if (authToken.length() > TOKEN_MAX_LENGTH) { + throw new AuthTokenParseException("Auth token is too long"); + } + } + + private WebEidAuthToken parseToken(String authToken) throws AuthTokenParseException { + try { + final WebEidAuthToken token = TOKEN_READER.readValue(authToken); + if (token == null) { + throw new AuthTokenParseException("Web eID authentication token is null"); + } + return token; + } catch (IOException e) { + throw new AuthTokenParseException("Error parsing Web eID authentication token", e); + } + } + +} diff --git a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java index 8f584c7e..39fdfceb 100644 --- a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java +++ b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePolicyValidator.java @@ -23,13 +23,13 @@ package eu.webeid.security.validator.certvalidators; import eu.webeid.security.exceptions.AuthTokenException; +import eu.webeid.security.exceptions.UserCertificateDisallowedPolicyException; +import eu.webeid.security.exceptions.UserCertificateParseException; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.x509.CertificatePolicies; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.PolicyInformation; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; -import eu.webeid.security.exceptions.UserCertificateDisallowedPolicyException; -import eu.webeid.security.exceptions.UserCertificateParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePurposeValidator.java b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePurposeValidator.java index 8332f740..457f3a0f 100644 --- a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePurposeValidator.java +++ b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificatePurposeValidator.java @@ -22,12 +22,12 @@ package eu.webeid.security.validator.certvalidators; -import eu.webeid.security.exceptions.UserCertificateWrongPurposeException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import eu.webeid.security.exceptions.AuthTokenException; import eu.webeid.security.exceptions.UserCertificateMissingPurposeException; import eu.webeid.security.exceptions.UserCertificateParseException; +import eu.webeid.security.exceptions.UserCertificateWrongPurposeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; diff --git a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateValidatorBatch.java b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateValidatorBatch.java index 264135e0..7fd3bc36 100644 --- a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateValidatorBatch.java +++ b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateValidatorBatch.java @@ -23,11 +23,17 @@ package eu.webeid.security.validator.certvalidators; import eu.webeid.security.exceptions.AuthTokenException; +import eu.webeid.security.validator.AuthTokenValidationConfiguration; +import eu.webeid.security.validator.ocsp.OcspClient; +import eu.webeid.security.validator.ocsp.OcspServiceProvider; +import java.security.cert.CertStore; +import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; public final class SubjectCertificateValidatorBatch { @@ -55,4 +61,34 @@ public SubjectCertificateValidatorBatch addOptional(boolean condition, SubjectCe private SubjectCertificateValidatorBatch(List validatorList) { this.validatorList = validatorList; } + + /** + * Creates the certificate trust validators batch. + * As SubjectCertificateTrustedValidator has mutable state that SubjectCertificateNotRevokedValidator depends on, + * they cannot be reused/cached in an instance variable in a multi-threaded environment. Hence, they are + * re-created for each validation run for thread safety. + * + * @return certificate trust validator batch + */ + public static SubjectCertificateValidatorBatch forTrustValidation( + AuthTokenValidationConfiguration configuration, + Set trustedCACertificateAnchors, + CertStore trustedCACertificateCertStore, + OcspClient ocspClient, + OcspServiceProvider ocspServiceProvider) { + + final SubjectCertificateTrustedValidator certTrustedValidator = + new SubjectCertificateTrustedValidator(trustedCACertificateAnchors, trustedCACertificateCertStore); + + return SubjectCertificateValidatorBatch.createFrom( + certTrustedValidator::validateCertificateTrusted + ).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(), + new SubjectCertificateNotRevokedValidator( + certTrustedValidator, + ocspClient, ocspServiceProvider, + configuration.getAllowedOcspResponseTimeSkew(), + configuration.getMaxOcspResponseThisUpdateAge() + )::validateCertificateNotRevoked + ); + } } diff --git a/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspService.java b/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspService.java index bafba269..37974d5f 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspService.java +++ b/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspService.java @@ -22,10 +22,10 @@ package eu.webeid.security.validator.ocsp.service; +import eu.webeid.security.exceptions.AuthTokenException; +import eu.webeid.security.exceptions.OCSPCertificateException; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import eu.webeid.security.exceptions.OCSPCertificateException; -import eu.webeid.security.exceptions.AuthTokenException; import java.net.URI; import java.security.cert.CertificateEncodingException; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java b/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java index 0bc03193..9378ed50 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java +++ b/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java @@ -22,10 +22,10 @@ package eu.webeid.security.validator.ocsp.service; +import eu.webeid.security.exceptions.OCSPCertificateException; import eu.webeid.security.validator.ocsp.OcspResponseValidator; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; -import eu.webeid.security.exceptions.OCSPCertificateException; import java.net.URI; import java.security.cert.CertificateEncodingException; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/service/OcspService.java b/src/main/java/eu/webeid/security/validator/ocsp/service/OcspService.java index 97bbdf2c..b551071b 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/service/OcspService.java +++ b/src/main/java/eu/webeid/security/validator/ocsp/service/OcspService.java @@ -22,8 +22,8 @@ package eu.webeid.security.validator.ocsp.service; -import org.bouncycastle.cert.X509CertificateHolder; import eu.webeid.security.exceptions.AuthTokenException; +import org.bouncycastle.cert.X509CertificateHolder; import java.net.URI; import java.util.Date; diff --git a/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java new file mode 100644 index 00000000..8770abb1 --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java @@ -0,0 +1,190 @@ +/* + * 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.SupportedSignatureAlgorithm; +import eu.webeid.security.authtoken.WebEidAuthToken; +import eu.webeid.security.certificate.CertificateLoader; +import eu.webeid.security.exceptions.AuthTokenException; +import eu.webeid.security.exceptions.AuthTokenParseException; +import eu.webeid.security.exceptions.CertificateDecodingException; +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.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; + +import javax.security.auth.x500.X500Principal; +import java.security.cert.CertStore; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static eu.webeid.security.util.Strings.isNullOrEmpty; + +class AuthTokenVersion11Validator extends AuthTokenVersion1Validator implements AuthTokenVersionValidator { + + private static final String V11_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1.1"; + private static final Set SUPPORTED_SIGNING_CRYPTO_ALGORITHMS = Set.of("ECC", "RSA"); + private static final Set SUPPORTED_SIGNING_PADDING_SCHEMES = Set.of("NONE", "PKCS1.5", "PSS"); + private static final Set SUPPORTED_SIGNING_HASH_FUNCTIONS = Set.of( + "SHA-224", "SHA-256", "SHA-384", "SHA-512", + "SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512" + ); + private static final int KEY_USAGE_NON_REPUDIATION = 1; + + public AuthTokenVersion11Validator( + SubjectCertificateValidatorBatch simpleSubjectCertificateValidators, + Set trustedCACertificateAnchors, + CertStore trustedCACertificateCertStore, + AuthTokenSignatureValidator authTokenSignatureValidator, + AuthTokenValidationConfiguration configuration, + OcspClient ocspClient, + OcspServiceProvider ocspServiceProvider + ) { + super( + simpleSubjectCertificateValidators, + trustedCACertificateAnchors, + trustedCACertificateCertStore, + authTokenSignatureValidator, + configuration, + ocspClient, + ocspServiceProvider + ); + } + + @Override + protected String getSupportedFormatPrefix() { + return V11_SUPPORTED_TOKEN_FORMAT_PREFIX; + } + + @Override + public X509Certificate validate(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException { + final X509Certificate subjectCertificate = validateV1(token, currentChallengeNonce); + final X509Certificate signingCertificate = validateSigningCertificateExists(token); + validateSupportedSignatureAlgorithms(token.getSupportedSignatureAlgorithms()); + validateSameSubject(subjectCertificate, signingCertificate); + validateSameIssuer(subjectCertificate, signingCertificate); + validateSigningCertificateValidity(signingCertificate); + validateKeyUsage(signingCertificate); + + return subjectCertificate; + } + + private static void validateSupportedSignatureAlgorithms(List algorithms) throws AuthTokenParseException { + if (algorithms == null || algorithms.isEmpty()) { + throw new AuthTokenParseException("'supportedSignatureAlgorithms' field is missing"); + } + + boolean hasInvalid = algorithms.stream().anyMatch(supportedSignatureAlgorithm -> + !SUPPORTED_SIGNING_CRYPTO_ALGORITHMS.contains(supportedSignatureAlgorithm.getCryptoAlgorithm()) || + !SUPPORTED_SIGNING_HASH_FUNCTIONS.contains(supportedSignatureAlgorithm.getHashFunction()) || + !SUPPORTED_SIGNING_PADDING_SCHEMES.contains(supportedSignatureAlgorithm.getPaddingScheme()) + ); + + if (hasInvalid) { + throw new AuthTokenParseException("Unsupported signature algorithm"); + } + } + + private static X509Certificate validateSigningCertificateExists(WebEidAuthToken token) throws AuthTokenParseException, CertificateDecodingException { + if (isNullOrEmpty(token.getUnverifiedSigningCertificate())) { + throw new AuthTokenParseException("'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'"); + } + return CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedSigningCertificate()); + } + + private static void validateSameSubject(X509Certificate subjectCertificate, X509Certificate signingCertificate) + throws AuthTokenParseException { + if (!subjectAndSigningCertificateSubjectsMatch( + subjectCertificate.getSubjectX500Principal(), + signingCertificate.getSubjectX500Principal())) { + throw new AuthTokenParseException("Signing certificate subject does not match authentication certificate subject"); + } + } + + private static void validateSameIssuer(X509Certificate subjectCertificate, X509Certificate signingCertificate) + throws AuthTokenParseException { + byte[] subjectCertificateAuthorityKeyIdentifier = getAuthorityKeyIdentifier(subjectCertificate); + byte[] signingCertificateAuthorityKeyIdentifier = getAuthorityKeyIdentifier(signingCertificate); + + if (subjectCertificateAuthorityKeyIdentifier.length == 0 + || signingCertificateAuthorityKeyIdentifier.length == 0 + || !Arrays.equals(subjectCertificateAuthorityKeyIdentifier, signingCertificateAuthorityKeyIdentifier)) { + throw new AuthTokenParseException( + "Signing certificate is not issued by the same issuing authority as the authentication certificate"); + } + } + + private static void validateSigningCertificateValidity(X509Certificate signingCertificate) + throws AuthTokenParseException { + try { + signingCertificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException e) { + throw new AuthTokenParseException("Signing certificate is not valid: " + e.getMessage(), e); + } + } + + private static void validateKeyUsage(X509Certificate signingCertificate) + throws AuthTokenParseException { + boolean[] keyUsage = signingCertificate.getKeyUsage(); + if (keyUsage == null || keyUsage.length <= KEY_USAGE_NON_REPUDIATION || !keyUsage[KEY_USAGE_NON_REPUDIATION]) { + throw new AuthTokenParseException("Signing certificate key usage extension missing or does not contain non-repudiation bit required for digital signatures"); + } + } + + private static boolean subjectAndSigningCertificateSubjectsMatch( + X500Principal authenticationCertificateSubject, + X500Principal signingCertificateSubject) { + X500Name authName = X500Name.getInstance(RFC4519Style.INSTANCE, authenticationCertificateSubject.getEncoded()); + X500Name signName = X500Name.getInstance(RFC4519Style.INSTANCE, signingCertificateSubject.getEncoded()); + return authName.equals(signName); + } + + private static byte[] getAuthorityKeyIdentifier(X509Certificate certificate) throws AuthTokenParseException { + try { + byte[] authorityKeyIdentifierExtension = certificate.getExtensionValue(Extension.authorityKeyIdentifier.getId()); + if (authorityKeyIdentifierExtension == null) { + return new byte[0]; + } + AuthorityKeyIdentifier authorityKeyIdentifier = + AuthorityKeyIdentifier.getInstance(JcaX509ExtensionUtils.parseExtensionValue(authorityKeyIdentifierExtension)); + return authorityKeyIdentifier.getKeyIdentifier(); + } catch (Exception e) { + throw new AuthTokenParseException("Failed to parse Authority Key Identifier", e); + } + } + + protected X509Certificate validateV1(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException { + return super.validate(token, currentChallengeNonce); + } +} diff --git a/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java new file mode 100644 index 00000000..7ce63762 --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java @@ -0,0 +1,108 @@ +/* + * 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.AuthTokenException; +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 java.security.cert.CertStore; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Set; + +class AuthTokenVersion1Validator implements AuthTokenVersionValidator { + + private static final String V1_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1"; + + private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators; + private final Set trustedCACertificateAnchors; + private final CertStore trustedCACertificateCertStore; + private final AuthTokenSignatureValidator authTokenSignatureValidator; + private final AuthTokenValidationConfiguration configuration; + private final OcspClient ocspClient; + private final OcspServiceProvider ocspServiceProvider; + + public AuthTokenVersion1Validator( + SubjectCertificateValidatorBatch simpleSubjectCertificateValidators, + Set 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); + } +}