diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 2303e15a..3ab40226 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: web-eid-springboot-example: - image: web-eid-springboot-example:3.1.1 + image: web-eid-springboot-example:3.2.0 restart: always environment: JAVA_TOOL_OPTIONS: '-Dspring.profiles.active=prod' diff --git a/example/pom.xml b/example/pom.xml index 55bb5092..b50adcec 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -5,12 +5,12 @@ org.springframework.boot spring-boot-starter-parent - 3.4.4 + 3.5.3 eu.webeid.example web-eid-springboot-example - 3.1.1 + 3.2.0 web-eid-springboot-example Example Spring Boot application that demonstrates how to use Web eID for authentication and digital signing @@ -18,11 +18,11 @@ 17 - 3.5.2 - 3.1.1 - 6.0.0 - 1.44 - 3.4.5 + 3.5.3 + 3.2.0 + 6.0.1 + 1.44 + 3.4.6 diff --git a/pom.xml b/pom.xml index 04af5316..2816da7c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 authtoken-validation eu.webeid.security - 3.1.1 + 3.2.0 jar authtoken-validation Web eID authentication token validation library for Java @@ -13,16 +13,16 @@ 11 0.12.6 - 1.80 - 2.18.3 + 1.81 + 2.19.1 2.0.17 - 5.12.1 + 5.13.3 3.27.3 - 5.17.0 + 5.18.0 3.5.2 3.3.1 3.11.2 - 3.5.0 + 3.6.0 0.8.12 ${project.basedir}/../jacoco-coverage-report/target/site/jacoco-aggregate/jacoco.xml 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..a1387637 --- /dev/null +++ b/src/main/java/eu/webeid/security/authtoken/SupportedSignatureAlgorithm.java @@ -0,0 +1,31 @@ +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/validator/AuthTokenV11Validator.java b/src/main/java/eu/webeid/security/validator/AuthTokenV11Validator.java new file mode 100644 index 00000000..e14b66f0 --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/AuthTokenV11Validator.java @@ -0,0 +1,107 @@ +package eu.webeid.security.validator; + +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.validator.certvalidators.SubjectCertificateValidatorBatch; + +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import static eu.webeid.security.util.Strings.isNullOrEmpty; + +public final class AuthTokenV11Validator implements AuthTokenValidator { + + private static final String SUPPORTED_PREFIX = "web-eid:1.1"; + + private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators; + private final Supplier certTrustValidatorsSupplier; + private final AuthTokenSignatureValidator authTokenSignatureValidator; + + public AuthTokenV11Validator( + SubjectCertificateValidatorBatch simpleSubjectCertificateValidators, + Supplier certTrustValidatorsSupplier, + AuthTokenSignatureValidator authTokenSignatureValidator + ) { + this.simpleSubjectCertificateValidators = simpleSubjectCertificateValidators; + this.certTrustValidatorsSupplier = certTrustValidatorsSupplier; + this.authTokenSignatureValidator = authTokenSignatureValidator; + } + + @Override + public boolean supports(String format) { + return format != null && format.startsWith(SUPPORTED_PREFIX); + } + + @Override + public WebEidAuthToken parse(String authToken) throws AuthTokenException { + throw new UnsupportedOperationException("Parsing is handled by AuthTokenValidatorImpl. " + this.getClass().getSimpleName() + " only supports validation."); + } + + @Override + public X509Certificate validate(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException { + if (token.getFormat() == null || !token.getFormat().startsWith(SUPPORTED_PREFIX)) { + throw new AuthTokenParseException("Only token format '" + SUPPORTED_PREFIX + "' is supported by this validator"); + } + + if (isNullOrEmpty(token.getUnverifiedSigningCertificate())) { + throw new AuthTokenParseException("'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'"); + } + + if (token.getSupportedSignatureAlgorithms() == null || token.getSupportedSignatureAlgorithms().isEmpty()) { + throw new AuthTokenParseException("'supportedSignatureAlgorithms' field is missing"); + } + + validateSupportedSignatureAlgorithms(token.getSupportedSignatureAlgorithms()); + + final X509Certificate subjectCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedCertificate()); + final X509Certificate signingCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedSigningCertificate()); + + if (!subjectCertificate.getSubjectX500Principal().equals(signingCertificate.getSubjectX500Principal())) { + throw new AuthTokenParseException("Signing certificate subject does not match authentication certificate subject"); + } + + simpleSubjectCertificateValidators.executeFor(signingCertificate); + certTrustValidatorsSupplier.get().executeFor(signingCertificate); + + authTokenSignatureValidator.validate( + token.getAlgorithm(), + token.getSignature(), + signingCertificate.getPublicKey(), + currentChallengeNonce + ); + + return subjectCertificate; + } + + private static void validateSupportedSignatureAlgorithms(List algorithms) throws AuthTokenParseException { + boolean hasInvalid = algorithms.stream().anyMatch(supportedSignatureAlgorithm -> + !isValidCryptoAlgorithm(supportedSignatureAlgorithm.getCryptoAlgorithm()) + || !isValidHashFunction(supportedSignatureAlgorithm.getHashFunction()) + || !isValidPaddingScheme(supportedSignatureAlgorithm.getPaddingScheme()) + ); + + if (hasInvalid) { + throw new AuthTokenParseException("Unsupported signature algorithm"); + } + } + + private static boolean isValidCryptoAlgorithm(String value) { + return "RSA".equals(value) || "ECC".equals(value); + } + + private static boolean isValidHashFunction(String value) { + return Set.of( + "SHA-224", "SHA-256", "SHA-384", "SHA-512", + "SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512" + ).contains(value); + } + + private static boolean isValidPaddingScheme(String value) { + return "NONE".equals(value) || "PKCS1.5".equals(value) || "PSS".equals(value); + } +} diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenV1Validator.java b/src/main/java/eu/webeid/security/validator/AuthTokenV1Validator.java new file mode 100644 index 00000000..7fefe9f8 --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/AuthTokenV1Validator.java @@ -0,0 +1,127 @@ +package eu.webeid.security.validator; + +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.certvalidators.SubjectCertificateNotRevokedValidator; +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 java.security.cert.CertStore; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Set; + +public final class AuthTokenV1Validator implements AuthTokenValidator { + + private static final String SUPPORTED_TOKEN_FORMAT_VERSION = "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 AuthTokenV1Validator( + 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(SUPPORTED_TOKEN_FORMAT_VERSION); + } + + @Override + public WebEidAuthToken parse(String authToken) throws AuthTokenException { + throw new UnsupportedOperationException("Parsing is handled by AuthTokenValidatorImpl. " + this.getClass().getSimpleName() + " only supports validation."); + } + + @Override + public X509Certificate validate(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException { + if (token.getFormat() == null || token.getFormat().isBlank()) { + throw new AuthTokenParseException("'format' field is missing"); + } + if (!token.getFormat().startsWith(SUPPORTED_TOKEN_FORMAT_VERSION)) { + throw new AuthTokenParseException("Only token format version '" + SUPPORTED_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() { + return createCertTrustValidators( + configuration, + trustedCACertificateAnchors, + trustedCACertificateCertStore, + ocspClient, + ocspServiceProvider + ); + } + + public static SubjectCertificateValidatorBatch createCertTrustValidators( + 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/AuthTokenValidator.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java index 3476ea41..d848d8e1 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java @@ -34,7 +34,13 @@ */ public interface AuthTokenValidator { - String CURRENT_TOKEN_FORMAT_VERSION = "web-eid:1"; + /** + * 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); /** * Parses the Web eID authentication token signed by the subject. diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorFactory.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorFactory.java new file mode 100644 index 00000000..3f12f4f0 --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorFactory.java @@ -0,0 +1,20 @@ +package eu.webeid.security.validator; + +import eu.webeid.security.exceptions.AuthTokenParseException; + +import java.util.List; + +public final class AuthTokenValidatorFactory { + private final List validators; + + public AuthTokenValidatorFactory(List validators) { + this.validators = List.copyOf(validators); + } + + public AuthTokenValidator requireFor(String format) throws AuthTokenParseException { + return validators.stream() + .filter(v -> v.supports(format)) + .findFirst() + .orElseThrow(() -> new AuthTokenParseException("Only token format version 'web-eid:1' is currently supported")); + } +} diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java index 14cf3e78..85048c01 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java @@ -25,15 +25,12 @@ 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; @@ -45,6 +42,7 @@ import java.security.cert.CertStore; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -58,45 +56,62 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { 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; + private final AuthTokenValidatorFactory tokenValidatorFactory; - /** - * @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(); + final AuthTokenValidationConfiguration copiedConfig = configuration.copy(); - // Create and cache trusted CA certificate JCA objects for SubjectCertificateTrustedValidator and AiaOcspService. - trustedCACertificateAnchors = CertificateValidator.buildTrustAnchorsFromCertificates(configuration.getTrustedCACertificates()); - trustedCACertificateCertStore = CertificateValidator.buildCertStoreFromCertificates(configuration.getTrustedCACertificates()); + final Set trustedCACertificateAnchors = CertificateValidator.buildTrustAnchorsFromCertificates(copiedConfig.getTrustedCACertificates()); + final CertStore trustedCACertificateCertStore = CertificateValidator.buildCertStoreFromCertificates(copiedConfig.getTrustedCACertificates()); - simpleSubjectCertificateValidators = SubjectCertificateValidatorBatch.createFrom( + final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators = SubjectCertificateValidatorBatch.createFrom( SubjectCertificatePurposeValidator::validateCertificatePurpose, - new SubjectCertificatePolicyValidator(configuration.getDisallowedSubjectCertificatePolicies())::validateCertificatePolicies + new SubjectCertificatePolicyValidator(copiedConfig.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"); + // OcspClient uses built-in HttpClient internally by default. + // A single HttpClient instance is reused for all HTTP calls to utilize connection and thread pools. + if (copiedConfig.isUserCertificateRevocationCheckWithOcspEnabled()) { + Objects.requireNonNull(ocspClient, "OCSP client must not be null when OCSP check is enabled"); ocspServiceProvider = new OcspServiceProvider( - configuration.getDesignatedOcspServiceConfiguration(), - new AiaOcspServiceConfiguration(configuration.getNonceDisabledOcspUrls(), + copiedConfig.getDesignatedOcspServiceConfiguration(), + new AiaOcspServiceConfiguration( + copiedConfig.getNonceDisabledOcspUrls(), trustedCACertificateAnchors, trustedCACertificateCertStore)); } - authTokenSignatureValidator = new AuthTokenSignatureValidator(configuration.getSiteOrigin()); + final AuthTokenSignatureValidator authTokenSignatureValidator = + new AuthTokenSignatureValidator(copiedConfig.getSiteOrigin()); + + this.tokenValidatorFactory = new AuthTokenValidatorFactory(List.of( + new AuthTokenV11Validator( + simpleSubjectCertificateValidators, + () -> AuthTokenV1Validator.createCertTrustValidators( + copiedConfig, + trustedCACertificateAnchors, + trustedCACertificateCertStore, + ocspClient, + ocspServiceProvider + ), + authTokenSignatureValidator + ), + new AuthTokenV1Validator( + simpleSubjectCertificateValidators, + trustedCACertificateAnchors, + trustedCACertificateCertStore, + authTokenSignatureValidator, + copiedConfig, + ocspClient, + ocspServiceProvider + ) + )); + } + + @Override + public boolean supports(String format) { + throw new UnsupportedOperationException("AuthTokenValidatorImpl is not format-specific. Use format-specific validators instead."); } @Override @@ -116,7 +131,7 @@ public WebEidAuthToken parse(String authToken) throws AuthTokenException { public X509Certificate validate(WebEidAuthToken authToken, String currentChallengeNonce) throws AuthTokenException { try { LOG.info("Starting token validation"); - return validateToken(authToken, currentChallengeNonce); + return tokenValidatorFactory.requireFor(authToken.getFormat()).validate(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); @@ -144,50 +159,4 @@ private WebEidAuthToken parseToken(String authToken) throws AuthTokenParseExcept 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/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java b/src/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java index fd8896c3..6ea0e95b 100644 --- a/src/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java +++ b/src/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java @@ -22,6 +22,8 @@ package eu.webeid.security.testutil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.BeforeEach; import eu.webeid.security.authtoken.WebEidAuthToken; import eu.webeid.security.exceptions.AuthTokenException; @@ -30,11 +32,9 @@ 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_NFC_AUTH_TOKEN = "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," + + "\"unverifiedSigningCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," + + "\"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 validNfcAuthToken; @BeforeEach protected void setup() { try { validator = AuthTokenValidators.getAuthTokenValidator(); validAuthToken = validator.parse(VALID_AUTH_TOKEN); + validNfcAuthToken = validator.parse(VALID_NFC_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_NFC_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..37c80e01 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenAlgorithmTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenAlgorithmTest.java @@ -59,4 +59,65 @@ void whenAlgorithmInvalid_thenParsingFails() throws AuthTokenException { .hasMessage("Unsupported signature algorithm"); } + @Test + void whenNfcTokenMissingSupportedAlgorithms_thenValidationFails() throws Exception { + final WebEidAuthToken token = removeJsonField(); + + assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE)) + .isInstanceOf(AuthTokenParseException.class) + .hasMessageContaining("'supportedSignatureAlgorithms' field is missing"); + } + + @Test + void whenNfcTokenHasInvalidCryptoAlgorithm_thenValidationFails() throws Exception { + final WebEidAuthToken token = replaceTokenField( + VALID_NFC_AUTH_TOKEN, + "\"cryptoAlgorithm\":\"RSA\"", + "\"cryptoAlgorithm\":\"INVALID\"" + ); + + assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE)) + .isInstanceOf(AuthTokenParseException.class) + .hasMessage("Unsupported signature algorithm"); + } + + @Test + void whenNfcTokenHasInvalidHashFunction_thenValidationFails() throws Exception { + final WebEidAuthToken token = replaceTokenField( + VALID_NFC_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 whenNfcTokenHasInvalidPaddingScheme_thenValidationFails() throws Exception { + final WebEidAuthToken token = replaceTokenField( + VALID_NFC_AUTH_TOKEN, + "\"paddingScheme\":\"PKCS1.5\"", + "\"paddingScheme\":\"BAD_PADDING\"" + ); + + assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE)) + .isInstanceOf(AuthTokenParseException.class) + .hasMessage("Unsupported signature algorithm"); + } + + @Test + void whenNfcTokenHasEmptySupportedAlgorithms_thenValidationFails() throws Exception { + final WebEidAuthToken token = replaceTokenField( + VALID_NFC_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/AuthTokenCertificateTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java index 14ed0666..8f0b3499 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java @@ -58,6 +58,14 @@ class AuthTokenCertificateTest extends AbstractTestWithValidator { "\"signature\":\"arx164xRiwhIQDINe0J+ZxJWZFOQTx0PBtOaWaxAe7gofEIHRIbV1w0sOCYBJnvmvMem9hU4nc2+iJx2x8poYck4Z6eI3GwtiksIec3XQ9ZIk1n/XchXnmPn3GYV+HzJ\"," + "\"format\":\"web-eid:1\"}"; + private static final String NFC_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 MISSING_KEY_USAGE_CERT = "MIICxjCCAa6gAwIBAgIJANTbd26vS6fmMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNVBAMTCndlYi1laWQuZXUwHhcNMjAwOTI0MTIyNDMzWhcNMzAwOTIyMTIyNDMzWjAVMRMwEQYDVQQDEwp3ZWItZWlkLmV1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAza5qBFu5fvs47rx3o9yzBVfIxHjMotID8ppkwWVen/uFxlqsRVi+XnWkggW+K8X45inAnBAVi1rIw7GQNdacSHglyvQfwM64AallmD0+K+QgbqxcO9fvRvlAeISENBc2bGgqTIytPEON5ZmazzbOZjqY3M1QcPlPZOeUm6M9ZcZFhsxpiB4gwZUic9tnCz9eujd6k6DzNVfSRaJcpGA5hJ9aKH4vXS3x7anewna+USEXkRb4Il5zSlZR0i1yrVA1YNOxCG/+GgWvXfvXwdQ0z9BpGwNEyc0mRDNx+umaTukz9t+7/qTcB2JLTuiwM9Gqg5sDDnzPlcZSa7GnIU0MLQIDAQABoxkwFzAVBgNVHREEDjAMggp3ZWItZWlkLmV1MA0GCSqGSIb3DQEBBQUAA4IBAQAYGkBhTlet47uw3JYunYo6dj4nGWSGV4x6LYjCp5QlAmGd28HpC1RFB3ba+inwW8SP69kEOcB0sJQAZ/tV90oCATNsy/Whg/TtiHISL2pr1dyBoKDRWbgTp8jjzcp2Bj9nL14aqpj1t4K1lcoYETX41yVmyyJu6VFs80M5T3yikm2giAhszjChnjyoT2kaEKoua9EUK9SS27pVltgbbvtmeTp3ZPHtBfiDOATL6E03RZ5WfMLRefI796a+RcznnudzQHhMSwcjLpMDgIWpUU4OU7RiwrU+S3MrvgzCjkWh2MGu/OGLB+d3JZoW+eCvigoshmAsbJCMLbh4N78BCPqk"; private static final String WRONG_PURPOSE_CERT = "MIIEBDCCA2WgAwIBAgIQGIgoZxFL7VZbyFH7MAVEkTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE4MTAxODA5MjcyM1oXDTIzMTAxNzIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT3SZB34CUGYhQyLsLd9b2ihv35q7NT47Id9ugLIdgg3NSFDccH6rV16D2m8DKfuD2mn3V6QdaaZnbWF4YdDK1W0C9kLNsB70ob//y39pugMftepmQpJcBGPqug81tf5jujggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPhJx7ro54+N8r2ByiZXzZyWBbjFMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsG/wUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgFi5XSCFGgsc8SKLWwMBWS0nu/20FjEqh6OGvsI4iPctNDkinsxcYgARdfqPsNnDX+KjALKPEKZCLKRixGL2kPLMgJCAQFXP9gstThxlj/1Q5YFb7KWhPWFiKgQEi9JdvxJQNXLkWV9onEh96mRFgv4IJJpGazuoSMZtzNpyBxmM0dwnxOf"; private static final String WRONG_POLICY_CERT = "MIIEATCCA2OgAwIBAgIQOWkBWXNDJm1byFd3XsWkvjAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE4MTAxODA5NTA0N1oXDTIzMTAxNzIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR5k1lXzvSeI9O/1s1pZvjhEW8nItJoG0EBFxmLEY6S7ki1vF2Q3TEDx6dNztI1Xtx96cs8r4zYTwdiQoDg7k3diUuR9nTWGxQEMO1FDo4Y9fAmiPGWT++GuOVoZQY3XxijggHBMIIBvTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBFBgNVHSAEPjA8MDAGCSsGAQQBzh8BAzAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5lZS9DUFMwCAYGBACPegECMB8GA1UdEQQYMBaBFDM4MDAxMDg1NzE4QGVlc3RpLmVlMB0GA1UdDgQWBBTkLL00CRAVTDEpocmV+W4m2CbmwDBhBggrBgEFBQcBAwRVMFMwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwHwYDVR0jBBgwFoAUwISZKcROnzsCNPaZ4QpWAAgpPnswcwYIKwYBBQUHAQEEZzBlMCwGCCsGAQUFBzABhiBodHRwOi8vYWlhLmRlbW8uc2suZWUvZXN0ZWlkMjAxODA1BggrBgEFBQcwAoYpaHR0cDovL2Muc2suZWUvVGVzdF9vZl9FU1RFSUQyMDE4LmRlci5jcnQwCgYIKoZIzj0EAwQDgYsAMIGHAkIB9VLJjHbS2bYudRatkEeMFJAMKbJ4bAVdh0KlFxWASexF5ywpGl43WSpB6QAXzNEBMe1FIWiOIud44iexNWO1jgACQQ1+M+taZ4hyWqSNW5DCIiUP7Yu4WvH3SUjEqQHbOQshyMh5EM1pVcvOn/ZgOxLt6ETv9avnhVMw2zTd1b8u4EFk"; @@ -67,6 +75,7 @@ class AuthTokenCertificateTest extends AbstractTestWithValidator { private static final String EXPIRED_RSA_CERT = "MIIE3DCCA8SgAwIBAgIQbLnhZj25xUtSW9CfBn46KjANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEAwwORVNURUlELVNLIDIwMTExGDAWBgkqhkiG9w0BCQEWCXBraUBzay5lZTAeFw0xMzEwMTQxMTA4MTVaFw0xODEwMTEyMDU5NTlaMIGPMQswCQYDVQQGEwJFRTEPMA0GA1UECgwGRVNURUlEMRcwFQYDVQQLDA5hdXRoZW50aWNhdGlvbjEgMB4GA1UEAwwXS0lUVCxIRUlLS0ksMzc3MTIzMDAyNTUxDTALBgNVBAQMBEtJVFQxDzANBgNVBCoMBkhFSUtLSTEUMBIGA1UEBRMLMzc3MTIzMDAyNTUwggEjMA0GCSqGSIb3DQEBAQUAA4IBEAAwggELAoIBAQBopcNoApF/o+YyVcHaonVhCbUYfUhDtoP2VDOKXNytBNIFO5uEL86mMOcfTURfOssrpvQBVgKWgQ0wjhq09qkfPJM9NbPz0VytcsGARKSNcPh1BKgnUnfd0M6SwSl1rFl2zvbDBfZTMDtQbROS4eV1wBXwa8XeHqQmTOZK/4mv+6fj0q/LzPmxUHP/LJbyjm07MAVzTAGFvanICPdTY9YQUyNCtp+r8RxjNEk/FjVDi9zgER7Tg/v/VEnjUdZG4pLZXnV+4EsBcH2Y/XoPq3Ou0ts3IG02iz83UFR0o3TYQnHnW9fMwToJRQzS3Bnd+NZee+yZZNKOUvxmn8f4dsDdAgR3CME3o4IBWzCCAVcwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBLAwUQYDVR0gBEowSDBGBgsrBgEEAc4fAQEDAzA3MBIGCCsGAQUFBwICMAYaBG5vbmUwIQYIKwYBBQUHAgEWFWh0dHA6Ly93d3cuc2suZWUvY3BzLzAfBgNVHREEGDAWgRRoZWlra2kua2l0dEBlZXN0aS5lZTAdBgNVHQ4EFgQUC8nhz1ziuRJnO6hJIBYthupzYkYwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMCIGCCsGAQUFBwEDBBYwFDAIBgYEAI5GAQEwCAYGBACORgEEMB8GA1UdIwQYMBaAFHtq8lVQXLjZegiHQa76ois9W1d2MEAGA1UdHwQ5MDcwNaAzoDGGL2h0dHA6Ly93d3cuc2suZWUvcmVwb3NpdG9yeS9jcmxzL2VzdGVpZDIwMTEuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBV7ohEG05MXcxHEeXOb2hNuLrVVT2dhOVwp21M13sOsG9l/GN8KEUR4JQcJo4zEK49zHCaA1qKg+4/mubWfMKz5eUS9njs7cuit2FjlTJUrm7ye9dKndGiv5o4T4ycvbUF4NJ6AvxXZLolfLTaF6Ge/c15Jz1WmBv0x+0C00d0qWkE3VVjwqYxUw9gJlWfbLLxqsT1pUXaf9JcsxdKXkhKKr9eQ7r00PwbARkKyeU/ylHGfOQlZeGXfyWxX1q1ZALicwJe6/UbQTqQeLn5Mviw/49H2rLb9BImFIJ30QYBlj9SGSHSZ5k11XPRaw2GfLrgoBqOjMUyKhfRxqJwb/xL"; private static final String EXPIRED_ECDSA_CERT = "MIIF0TCCA7mgAwIBAgIQMBVFXroEt3hZ8FHcKKE65TANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFzAVBgNVBAMMDkVTVEVJRC1TSyAyMDE1MB4XDTE3MTAyNTA4NTcwMFoXDTIxMDIxMDIxNTk1OVowgYsxCzAJBgNVBAYTAkVFMQ8wDQYDVQQKDAZFU1RFSUQxFzAVBgNVBAsMDmF1dGhlbnRpY2F0aW9uMR4wHAYDVQQDDBVUT09NLE1BUlQsMzc2MDIwNDAzMzQxDTALBgNVBAQMBFRPT00xDTALBgNVBCoMBE1BUlQxFDASBgNVBAUTCzM3NjAyMDQwMzM0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAExS1YQQBDLVvOi0a2GA5Y34AXODpx0AL8eKDOB7BjwBc/FAyVExhfb6O+lT5Tnaec3GnT4JNRyeV8d82L8cyOgFn4PWc+5cjFdmcZjJbtCvgyBOQQ831tteIDL2XSrvZEo4ICBDCCAgAwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCA4gwUwYDVR0gBEwwSjA+BgkrBgEEAc4fAQEwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvcmVwb3NpdG9vcml1bS9DUFMwCAYGBACPegECMB8GA1UdEQQYMBaBFG1hcnQudG9vbS4zQGVlc3RpLmVlMB0GA1UdDgQWBBSzneoLqtqbvHvJ19cjhp2XR5ovQTAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwHwYDVR0jBBgwFoAUs6uIvJnVYqSFKgjNtB1yO4NyR1EwYQYIKwYBBQUHAQMEVTBTMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wagYIKwYBBQUHAQEEXjBcMCcGCCsGAQUFBzABhhtodHRwOi8vYWlhLnNrLmVlL2VzdGVpZDIwMTUwMQYIKwYBBQUHMAKGJWh0dHA6Ly9jLnNrLmVlL0VTVEVJRC1TS18yMDE1LmRlci5jcnQwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL3d3dy5zay5lZS9jcmxzL2VzdGVpZC9lc3RlaWQyMDE1LmNybDANBgkqhkiG9w0BAQsFAAOCAgEAOXTvktUXqPgaK/uxzgH0xSEYClBAWIQaNgpqY5lwsQtgQnpfKlsADqMZxCp7UuuMvQmpDbBxv1kIr0oG1uUXrUtPw81XOH1ClwPPXWpg9VRTAetNbHTBbHDyzuXQMNeDmrntChs+BteletejGD+aYG39HGMlrMbGQZOgvQrpYHMDek0ckCPEsZRXqUP0g7Ie7uBQhz5At7l4EDAeOW8xGoI6t+Ke4GedccXKef60w2ZIIDzvOFHPTc6POCsIlFtF/nCKwVi7GoQKjbUbM5OdBLZ0jyLq2LvzZuT86Jo8wObziuSzApGlBexHAqLrR83q+/Xl61yPnFf3w2kAfS9kBjeunzTH7Jm3pNT3Zq9JRLvEDqtpOPqr4zm9nG6OSghFU6tySkpQ5HiakGpMcnt5o5KuXhQ+Dg317tdXPyQkSiuJ9NfEBW0ijrwO12SVRzYo/jRl4ZQUkAEEUSMEsC6gTsZypPdIsLDVoQWTytHDU89s1xJDn4HulPl12dFnrhlLeX4RxOjDxppZxdjBU0FoJoDB0qwEAN2TMAPJWh+Pp9mFuS/u0dht9sKvAkpx+o0Z7v7QMz03XlzCHOLTIK+f81Rjokl8f+wiog5Ojj0wZkDe6DuQC9L5uDey3PJHv3naVovhs7jrEJu+yrsLue/OHhAgWRh2S75/wlVPHPEE44k="; private static final String REVOKED_CERT = "MIIERDCCA6agAwIBAgIQSs8/WoDixVxbKRhNnF/GEzAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE4MDYxOTE0NTA1M1oXDTIwMDEwMjIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR/jopNG3KL0ZQUvO4OGSvcaqUtFDm3azOtsM2VRp666r0d36Zh0Zx/xej8f+SzEfWvvDT1HQLo3USiSbYn1FyNHTNxifV+Zvf6StXJAkdu24d1UvKbf+LylglO/yS7o4ijggIEMIICADAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5F/AQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFEQA6/1GXJtp+6czUzorhEJ7B95pMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezB/BggrBgEFBQcBAQRzMHEwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MEEGCCsGAQUFBzAChjVodHRwczovL3NrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VTVEVJRDIwMTguZGVyLmNydDAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vYy5zay5lZS90ZXN0X2VzdGVpZDIwMTguY3JsMAoGCCqGSM49BAMEA4GLADCBhwJBcmcfLC+HcSJ6BuRrDGL+K+7BAW8BfAiiWWAuBV4ebLkbbAWmkc9dSKgr4BEGEt90xDTQ85yW4SjGulFXu9C3yQsCQgETaXTs3Hp6vDAcQYL8Bx4BO3DwJbDuD4BUJyT0+9HQiFCQmTQ4xrNjeaeOwRWyMOM9z5ORMeJCiQUyil1x4YPIbg=="; + private static final String DIFFERENT_CERT = "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"; private MockedStatic mockedClock; @@ -300,4 +309,52 @@ void whenCertificateCaIsNotPartOfTrustChain_thenValidationFails() throws Excepti .isInstanceOf(CertificateNotTrustedException.class); } + @Test + void whenValidNfcToken_thenValidationSucceeds() { + mockDate("2023-10-01", mockedClock); + assertThatCode(() -> validator + .validate(validNfcAuthToken, VALID_CHALLENGE_NONCE)) + .doesNotThrowAnyException(); + } + + @Test + void whenNfcSigningCertificateFieldIsMissing_thenValidationFails() throws Exception { + final WebEidAuthToken token = replaceTokenField(NFC_AUTH_TOKEN, "\"unverifiedSigningCertificate\":\"X5C\",", ""); + assertThatThrownBy(() -> validator + .validate(token, VALID_CHALLENGE_NONCE)) + .isInstanceOf(AuthTokenParseException.class) + .hasMessage("'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'"); + } + + @Test + void whenNfcSigningCertificateIsNotBase64_thenValidationFails() throws Exception { + final WebEidAuthToken token = replaceTokenField(NFC_AUTH_TOKEN, "X5C", "This is not a certificate"); + assertThatThrownBy(() -> validator + .validate(token, VALID_CHALLENGE_NONCE)) + .isInstanceOf(CertificateDecodingException.class) + .cause() + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Illegal base64 character"); + } + + @Test + void whenNfcSigningCertificateIsNotACertificate_thenValidationFails() throws Exception { + final WebEidAuthToken token = replaceTokenField(NFC_AUTH_TOKEN, "X5C", "VGhpcyBpcyBub3QgYSBjZXJ0aWZpY2F0ZQ"); + assertThatThrownBy(() -> validator + .validate(token, VALID_CHALLENGE_NONCE)) + .isInstanceOf(CertificateDecodingException.class) + .cause() + .isInstanceOf(CertificateException.class) + .hasMessage("Could not parse certificate: java.io.IOException: Empty input"); + } + + @Test + void whenNfcSigningCertificateSubjectDoesNotMatch_thenValidationFails() throws Exception { + final WebEidAuthToken token = replaceTokenField(NFC_AUTH_TOKEN, "X5C", DIFFERENT_CERT); + assertThatThrownBy(() -> validator + .validate(token, VALID_CHALLENGE_NONCE)) + .isInstanceOf(AuthTokenParseException.class) + .hasMessage("Signing certificate subject does not match authentication certificate subject"); + } + } diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java index 3f596858..738d304a 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 NFC_AUTH_TOKEN_WRONG_CERT = "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQH9NeN14jo0ReaircrN2YvDAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIwMDMxMjEyMjgxMloXDTI1MDMxMjIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARVeP+9l3b1mm3fMHPeCFLbD7esXI8lDc+soWCBoMnZGo3d2Rg/mzKCIWJtw+JhcN7RwFFH9cwZ8Gni4C3QFYBIIJ2GdjX2KQfEkDvRsnKw6ZZmJQ+HC4ZFew3r8gauhfejggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFOfk7lPOq6rb9IbFZF1q97kJ4s2iMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgEQRbzFOSHIcmIEKczhN8xuteYgN2zEXZSJdP0q1iH1RR2AzZ8Ddz6SKRn/bZSzjcd4b7h3AyOEQr2hcidYkxT7sAJCAMPtOUryqp2WbTEUoOpbWrKqp8GjaAiVpBGDn/Xdu5M2Z6dvwZHnFGgRrZXtyUbcAgRW7MQJ0s/9GCVro3iqUzNN\"," + + "\"unverifiedSigningCertificate\":\"MIIEBDCCA2WgAwIBAgIQH9NeN14jo0ReaircrN2YvDAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIwMDMxMjEyMjgxMloXDTI1MDMxMjIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARVeP+9l3b1mm3fMHPeCFLbD7esXI8lDc+soWCBoMnZGo3d2Rg/mzKCIWJtw+JhcN7RwFFH9cwZ8Gni4C3QFYBIIJ2GdjX2KQfEkDvRsnKw6ZZmJQ+HC4ZFew3r8gauhfejggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFOfk7lPOq6rb9IbFZF1q97kJ4s2iMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgEQRbzFOSHIcmIEKczhN8xuteYgN2zEXZSJdP0q1iH1RR2AzZ8Ddz6SKRn/bZSzjcd4b7h3AyOEQr2hcidYkxT7sAJCAMPtOUryqp2WbTEUoOpbWrKqp8GjaAiVpBGDn/Xdu5M2Z6dvwZHnFGgRrZXtyUbcAgRW7MQJ0s/9GCVro3iqUzNN\"," + + "\"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 whenValidNfcTokenAndNonce_thenValidationSucceeds() throws Exception { + final X509Certificate result = validator.validate(validNfcAuthToken, VALID_CHALLENGE_NONCE); + + assertThat(CertificateData.getSubjectCN(result).orElseThrow()) + .isEqualTo("JÕEORG\\,JAAK-KRISTJAN\\,38001085718"); + assertThat(CertificateData.getSubjectIdCode(result).orElseThrow()) + .isEqualTo("PNOEE-38001085718"); + } + + @Test + void whenNfcTokenWithWrongChallengeNonce_thenValidationFails() { + final String invalidChallengeNonce = "12345678123456781234567812345678912356789124"; + assertThatThrownBy(() -> validator + .validate(validNfcAuthToken, invalidChallengeNonce)) + .isInstanceOf(AuthTokenSignatureValidationException.class); + } + + @Test + void whenNfcTokenWithWrongOrigin_thenValidationFails() throws Exception { + final AuthTokenValidator validatorWithWrongOrigin = + AuthTokenValidators.getAuthTokenValidator("https://wrong-origin.com"); + + assertThatThrownBy(() -> validatorWithWrongOrigin + .validate(validNfcAuthToken, VALID_CHALLENGE_NONCE)) + .isInstanceOf(AuthTokenSignatureValidationException.class); + } + + @Test + void whenNfcTokenWithWrongCert_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(NFC_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..5be2f3be 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java @@ -45,6 +45,14 @@ class AuthTokenSignatureValidatorTest { "\"signature\":\"xsjXsQvVYXWcdV0YPhxLthJxtf0//R8p9WFFlYJGRARrl1ruyoAUwl0xeHgeZOKeJtwiCYCNWJzCG3VM3ydgt92bKhhk1u0JXIPVqvOkmDY72OCN4q73Y8iGSPVTgjk93TgquHlodf7YcqZNhutwNNf3oldHEWJD5zmkdwdpBFXgeOwTAdFwGljDQZbHr3h1Dr+apUDuloS0WuIzUuu8YXN2b8lh8FCTlF0G0DEjhHd/MGx8dbe3UTLHmD7K9DXv4zLJs6EF9i2v/C10SIBQDkPBSVPqMxCDPECjbEPi2+ds94eU7ThOhOQlFFtJ4KjQNTUa2crSixH7cYZF2rNNmA==\"," + "\"format\":\"web-eid:1.0\"}"; + private static final String VALID_NFC_RS256_AUTH_TOKEN = "{\"algorithm\":\"RS256\"," + + "\"unverifiedCertificate\":\"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\"," + + "\"unverifiedSigningCertificate\":\"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\"," + + "\"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 whenValidRS256NfcSignature_thenSucceeds() throws Exception { + final AuthTokenSignatureValidator signatureValidator = + new AuthTokenSignatureValidator(URI.create("https://ria.ee")); + + final WebEidAuthToken authToken = OBJECT_READER.readValue(VALID_NFC_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/AuthTokenV11ValidatorTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenV11ValidatorTest.java new file mode 100644 index 00000000..fb7cbe6c --- /dev/null +++ b/src/test/java/eu/webeid/security/validator/AuthTokenV11ValidatorTest.java @@ -0,0 +1,77 @@ +package eu.webeid.security.validator; + +import eu.webeid.security.authtoken.WebEidAuthToken; +import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch; +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 java.util.function.Supplier; + +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; + +import eu.webeid.security.exceptions.AuthTokenParseException; + +class AuthTokenV11ValidatorTest { + + private AuthTokenV11Validator validator; + + @BeforeEach + void setUp() { + SubjectCertificateValidatorBatch scvb = mock(SubjectCertificateValidatorBatch.class); + Supplier trustSupplier = () -> mock(SubjectCertificateValidatorBatch.class); + AuthTokenSignatureValidator signatureValidator = mock(AuthTokenSignatureValidator.class); + validator = new AuthTokenV11Validator(scvb, trustSupplier, signatureValidator); + } + + @ParameterizedTest + @ValueSource(strings = { "web-eid:1.1", "web-eid:1.1.0", "web-eid:1.10" }) + void supports_acceptsV11AndPrefixedVariants(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 supports_rejectsOthers(String format) { + assertThat(validator.supports(format)).isFalse(); + } + + @Test + void validate_throwsIfFormatIsNotV11() { + WebEidAuthToken token = mock(WebEidAuthToken.class); + when(token.getFormat()).thenReturn("web-eid:1"); + + assertThatThrownBy(() -> validator.validate(token, "nonce")) + .isInstanceOf(AuthTokenParseException.class) + .hasMessageContaining("Only token format 'web-eid:1.1' is supported"); + } + + @Test + void validate_throwsIfUnverifiedSigningCertificateIsMissing() { + WebEidAuthToken token = mock(WebEidAuthToken.class); + when(token.getFormat()).thenReturn("web-eid:1.1"); + when(token.getUnverifiedSigningCertificate()).thenReturn(null); + + assertThatThrownBy(() -> validator.validate(token, "nonce")) + .isInstanceOf(AuthTokenParseException.class) + .hasMessageContaining("unverifiedSigningCertificate"); + } + + @Test + void validate_throwsIfSupportedSignatureAlgorithmsMissing() { + WebEidAuthToken token = mock(WebEidAuthToken.class); + when(token.getFormat()).thenReturn("web-eid:1.1"); + when(token.getUnverifiedSigningCertificate()).thenReturn("abc"); + when(token.getSupportedSignatureAlgorithms()).thenReturn(null); + + assertThatThrownBy(() -> validator.validate(token, "nonce")) + .isInstanceOf(AuthTokenParseException.class) + .hasMessageContaining("supportedSignatureAlgorithms"); + } +} diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenV1ValidatorTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenV1ValidatorTest.java new file mode 100644 index 00000000..b62039ef --- /dev/null +++ b/src/test/java/eu/webeid/security/validator/AuthTokenV1ValidatorTest.java @@ -0,0 +1,71 @@ +package eu.webeid.security.validator; + +import eu.webeid.security.authtoken.WebEidAuthToken; +import eu.webeid.security.exceptions.AuthTokenParseException; +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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.api.Test; + +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 AuthTokenV1ValidatorTest { + + private final SubjectCertificateValidatorBatch scvb = mock(SubjectCertificateValidatorBatch.class); + private final AuthTokenSignatureValidator signatureValidator = mock(AuthTokenSignatureValidator.class); + private final AuthTokenValidationConfiguration config = mock(AuthTokenValidationConfiguration.class); + + private final AuthTokenV1Validator validator = new AuthTokenV1Validator( + 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 supports_acceptsAllMajor1Variants(String format) { + assertThat(validator.supports(format)).isTrue(); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { "web-eid", "web-eid:0.9", "web-eid:2", "webauthn:1" }) + void supports_rejectsNullEmptyAndOtherMajors(String format) { + assertThat(validator.supports(format)).isFalse(); + } + + @Test + void validate_throwsWhenFormatMissing() { + WebEidAuthToken token = mock(WebEidAuthToken.class); + when(token.getFormat()).thenReturn(null); + + assertThatThrownBy(() -> validator.validate(token, "nonce")) + .isInstanceOf(AuthTokenParseException.class) + .hasMessageContaining("'format' field is missing"); + } + + + @Test + void validate_throwsWhenUnverifiedCertificateMissing() { + 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/AuthTokenValidatorFactoryTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenValidatorFactoryTest.java new file mode 100644 index 00000000..27d6e1c4 --- /dev/null +++ b/src/test/java/eu/webeid/security/validator/AuthTokenValidatorFactoryTest.java @@ -0,0 +1,55 @@ +package eu.webeid.security.validator; + +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 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; + +import java.util.List; + +class AuthTokenValidatorFactoryTest { + + @ParameterizedTest + @ValueSource(strings = { "web-eid:0.9", "web-eid:2", "foo", "1", "web-eid" }) + void requireFor_throwsWhenUnsupported(String format) { + AuthTokenValidatorFactory factory = new AuthTokenValidatorFactory(List.of()); + assertThatThrownBy(() -> factory.requireFor(format)) + .isInstanceOf(AuthTokenParseException.class) + .hasMessageContaining("Only token format version 'web-eid:1' is currently supported"); + } + + @Test + void requireFor_returnsV11WhenMultipleMatchFirstIsV11() throws AuthTokenParseException { + AuthTokenValidator v11 = mock(AuthTokenValidator.class); + when(v11.supports("web-eid:1.1")).thenReturn(true); + + AuthTokenValidator v1 = mock(AuthTokenValidator.class); + when(v1.supports("web-eid:1")).thenReturn(true); + + AuthTokenValidatorFactory factory = new AuthTokenValidatorFactory(List.of(v11, v1)); + + AuthTokenValidator chosen = factory.requireFor("web-eid:1.1"); + + assertThat(chosen).isSameAs(v11); + } + + @Test + void requireFor_returnsV1ForBaseV1() throws AuthTokenParseException { + AuthTokenValidator v11 = mock(AuthTokenValidator.class); + when(v11.supports("web-eid:1.1")).thenReturn(true); + + AuthTokenValidator v1 = mock(AuthTokenValidator.class); + when(v1.supports("web-eid:1")).thenReturn(true); + + AuthTokenValidatorFactory factory = new AuthTokenValidatorFactory(List.of(v11, v1)); + + AuthTokenValidator chosen = factory.requireFor("web-eid:1"); + + assertThat(chosen).isSameAs(v1); + } +} diff --git a/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java b/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java index 2594d86e..771c3018 100644 --- a/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java +++ b/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java @@ -342,11 +342,11 @@ private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedVal return getSubjectCertificateNotRevokedValidator(getMockClient(response), getAiaOcspServiceProvider()); } - private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidator(OcspServiceProvider ocspServiceProvider) throws JceException { + private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidator(OcspServiceProvider ocspServiceProvider) { return getSubjectCertificateNotRevokedValidator(ocspClient, ocspServiceProvider); } - private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidator(OcspClient client, OcspServiceProvider ocspServiceProvider) throws JceException { + private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidator(OcspClient client, OcspServiceProvider ocspServiceProvider) { return new SubjectCertificateNotRevokedValidator(trustedValidator, client, ocspServiceProvider, CONFIGURATION.getAllowedOcspResponseTimeSkew(), CONFIGURATION.getMaxOcspResponseThisUpdateAge()); } @@ -357,6 +357,7 @@ private static void setSubjectCertificateIssuerCertificate(SubjectCertificateTru } private HttpResponse getMockedResponse(byte[] bodyContent) throws URISyntaxException { + @SuppressWarnings("unchecked") final HttpResponse mockResponse = mock(HttpResponse.class); final HttpHeaders headers = HttpHeaders.of(