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(