diff --git a/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java index 05eed61d..a37d4c39 100644 --- a/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java +++ b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java @@ -56,7 +56,8 @@ public SecurityFilterChain webEidPluginAndMobileSecurityFilterChain( AuthenticationConfiguration authConfig, ChallengeNonceGenerator challengeNonceGenerator, ITemplateEngine templateEngine, - WebEidMobileProperties webEidMobileProperties + WebEidMobileProperties webEidMobileProperties, + WebEidAuthTokenProperties webEidAuthTokenProperties ) throws Exception { return http .authorizeHttpRequests(auth -> auth @@ -65,7 +66,7 @@ public SecurityFilterChain webEidPluginAndMobileSecurityFilterChain( .anyRequest().authenticated() ) .authenticationProvider(webEidAuthenticationProvider) - .addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator, webEidMobileProperties), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new WebEidMobileAuthInitFilter("/auth/mobile/init", "/auth/mobile/login", challengeNonceGenerator, webEidMobileProperties, webEidAuthTokenProperties), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new WebEidChallengeNonceFilter("/auth/challenge", challengeNonceGenerator), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new WebEidLoginPageGeneratingFilter("/auth/mobile/login", "/auth/login", templateEngine), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class) diff --git a/example/src/main/java/eu/webeid/example/security/WebEidMobileAuthInitFilter.java b/example/src/main/java/eu/webeid/example/security/WebEidMobileAuthInitFilter.java index 036d734e..c152b5c3 100644 --- a/example/src/main/java/eu/webeid/example/security/WebEidMobileAuthInitFilter.java +++ b/example/src/main/java/eu/webeid/example/security/WebEidMobileAuthInitFilter.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import eu.webeid.example.config.WebEidAuthTokenProperties; import eu.webeid.example.config.WebEidMobileProperties; import eu.webeid.security.challenge.ChallengeNonceGenerator; import jakarta.servlet.FilterChain; @@ -51,12 +52,15 @@ public final class WebEidMobileAuthInitFilter extends OncePerRequestFilter { private final ChallengeNonceGenerator nonceGenerator; private final String mobileLoginPath; private final WebEidMobileProperties webEidMobileProperties; + private final WebEidAuthTokenProperties webEidAuthTokenProperties; - public WebEidMobileAuthInitFilter(String path, String mobileLoginPath, ChallengeNonceGenerator nonceGenerator, WebEidMobileProperties webEidMobileProperties) { + public WebEidMobileAuthInitFilter(String path, String mobileLoginPath, ChallengeNonceGenerator nonceGenerator, + WebEidMobileProperties webEidMobileProperties, WebEidAuthTokenProperties webEidAuthTokenProperties) { this.requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, path); this.nonceGenerator = nonceGenerator; this.mobileLoginPath = mobileLoginPath; this.webEidMobileProperties = webEidMobileProperties; + this.webEidAuthTokenProperties = webEidAuthTokenProperties; } @Override @@ -70,8 +74,11 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, var challenge = nonceGenerator.generateAndStoreNonce(); - String loginUri = ServletUriComponentsBuilder.fromCurrentContextPath() - .path(mobileLoginPath).build().toUriString(); + String loginUri = UriComponentsBuilder + .fromUriString(webEidAuthTokenProperties.validation().localOrigin()) + .path(mobileLoginPath) + .build() + .toUriString(); String payloadJson = OBJECT_WRITER.writeValueAsString( new AuthPayload(challenge.getBase64EncodedNonce(), loginUri, diff --git a/example/src/main/java/eu/webeid/example/service/MobileSigningService.java b/example/src/main/java/eu/webeid/example/service/MobileSigningService.java index 2b865e1c..b453b38f 100644 --- a/example/src/main/java/eu/webeid/example/service/MobileSigningService.java +++ b/example/src/main/java/eu/webeid/example/service/MobileSigningService.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import eu.webeid.example.config.WebEidAuthTokenProperties; import eu.webeid.example.config.WebEidMobileProperties; import eu.webeid.example.security.WebEidAuthentication; import eu.webeid.example.service.dto.CertificateDTO; @@ -54,10 +55,13 @@ public class MobileSigningService { private static final ObjectWriter OBJECT_WRITER = new ObjectMapper().writerFor(RequestObject.class); private final SigningService signingService; private final WebEidMobileProperties webEidMobileProperties; + private final WebEidAuthTokenProperties webEidAuthTokenProperties; - public MobileSigningService(SigningService signingService, WebEidMobileProperties webEidMobileProperties) { + public MobileSigningService(SigningService signingService, WebEidMobileProperties webEidMobileProperties, + WebEidAuthTokenProperties webEidAuthTokenProperties) { this.signingService = signingService; this.webEidMobileProperties = webEidMobileProperties; + this.webEidAuthTokenProperties = webEidAuthTokenProperties; } public MobileInitRequest initCertificateOrSigningRequest(WebEidAuthentication authentication) throws IOException, CertificateException, NoSuchAlgorithmException { @@ -75,7 +79,8 @@ public MobileInitRequest initCertificateOrSigningRequest(WebEidAuthentication au public MobileInitRequest initSigningRequest(WebEidAuthentication authentication, CertificateDTO certificateDTO) throws IOException, CertificateException, NoSuchAlgorithmException { Objects.requireNonNull(authentication, "authentication must not be null"); Objects.requireNonNull(certificateDTO, "certificateDTO must not be null"); - final String responseUri = ServletUriComponentsBuilder.fromCurrentContextPath() + final String responseUri = UriComponentsBuilder + .fromUriString(webEidAuthTokenProperties.validation().localOrigin()) .path(SIGNATURE_RESPONSE_PATH) .build() .toUriString(); @@ -95,7 +100,8 @@ public MobileInitRequest initSigningRequest(WebEidAuthentication authentication, } private MobileInitRequest initCertificateRequest() throws IOException { - final String responseUri = ServletUriComponentsBuilder.fromCurrentContextPath() + final String responseUri = UriComponentsBuilder + .fromUriString(webEidAuthTokenProperties.validation().localOrigin()) .path(CERTIFICATE_RESPONSE_PATH) .build() .toUriString(); diff --git a/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java index 46ee0c55..e94450d0 100644 --- a/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java +++ b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java @@ -41,9 +41,13 @@ import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; import javax.security.auth.x500.X500Principal; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; import java.security.cert.CertStore; import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateFactory; import java.security.cert.CertificateNotYetValidException; +import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -55,7 +59,7 @@ class AuthTokenVersion11Validator extends AuthTokenVersion1Validator implements AuthTokenVersionValidator { - private static final String V11_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1.1"; + private static final Set V11_SUPPORTED_TOKEN_FORMAT = Set.of("web-eid:1.1"); private static final Set SUPPORTED_SIGNING_CRYPTO_ALGORITHMS = Set.of("ECC", "RSA"); private static final Set SUPPORTED_SIGNING_PADDING_SCHEMES = Set.of("NONE", "PKCS1.5", "PSS"); private static final Set SUPPORTED_SIGNING_HASH_FUNCTIONS = Set.of( @@ -64,6 +68,9 @@ class AuthTokenVersion11Validator extends AuthTokenVersion1Validator implements ); private static final int KEY_USAGE_NON_REPUDIATION = 1; + private final Set trustedCACertificateAnchors; + private final CertStore trustedCACertificateCertStore; + public AuthTokenVersion11Validator( SubjectCertificateValidatorBatch simpleSubjectCertificateValidators, Set trustedCACertificateAnchors, @@ -82,11 +89,13 @@ public AuthTokenVersion11Validator( ocspClient, ocspServiceProvider ); + this.trustedCACertificateAnchors = trustedCACertificateAnchors; + this.trustedCACertificateCertStore = trustedCACertificateCertStore; } @Override - protected String getSupportedFormatPrefix() { - return V11_SUPPORTED_TOKEN_FORMAT_PREFIX; + public boolean supports(String format) { + return format != null && V11_SUPPORTED_TOKEN_FORMAT.contains(format); } @Override @@ -98,6 +107,7 @@ public X509Certificate validate(WebEidAuthToken token, String currentChallengeNo validateSameIssuer(subjectCertificate, signingCertificate); validateSigningCertificateValidity(signingCertificate); validateKeyUsage(signingCertificate); + validateSigningCertificateChain(signingCertificate); } return subjectCertificate; @@ -184,6 +194,24 @@ private static void validateKeyUsage(X509Certificate signingCertificate) } } + private void validateSigningCertificateChain(X509Certificate signingCertificate) + throws AuthTokenParseException { + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + CertPath certPath = certificateFactory.generateCertPath(List.of(signingCertificate)); + + PKIXParameters parameters = new PKIXParameters(trustedCACertificateAnchors); + parameters.addCertStore(trustedCACertificateCertStore); + parameters.setRevocationEnabled(false); + + CertPathValidator validator = CertPathValidator.getInstance("PKIX"); + validator.validate(certPath, parameters); + } catch (Exception e) { + throw new AuthTokenParseException("Signing certificate chain validation failed", e); + } + } + private static boolean subjectAndSigningCertificateSubjectsMatch( X500Principal authenticationCertificateSubject, X500Principal signingCertificateSubject) { diff --git a/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java index 851c6ab8..6bbe7d1c 100644 --- a/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java +++ b/src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java @@ -40,7 +40,7 @@ class AuthTokenVersion1Validator implements AuthTokenVersionValidator { private static final String V1_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1"; - + private static final Set SUPPORTED_TOKEN_FORMATS = Set.of(V1_SUPPORTED_TOKEN_FORMAT_PREFIX, "web-eid:1.0"); private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators; private final Set trustedCACertificateAnchors; private final CertStore trustedCACertificateCertStore; @@ -69,11 +69,7 @@ public AuthTokenVersion1Validator( @Override public boolean supports(String format) { - return format != null && format.startsWith(getSupportedFormatPrefix()); - } - - protected String getSupportedFormatPrefix() { - return V1_SUPPORTED_TOKEN_FORMAT_PREFIX; + return format != null && SUPPORTED_TOKEN_FORMATS.contains(format); } @Override diff --git a/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java index 26296256..aab39965 100644 --- a/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java +++ b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java @@ -22,6 +22,7 @@ package eu.webeid.security.validator.versionvalidators; +import eu.webeid.security.authtoken.SupportedSignatureAlgorithm; import eu.webeid.security.authtoken.WebEidAuthToken; import eu.webeid.security.certificate.CertificateLoader; import eu.webeid.security.authtoken.UnverifiedSigningCertificate; @@ -31,6 +32,7 @@ import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch; import eu.webeid.security.validator.ocsp.OcspClient; import eu.webeid.security.validator.ocsp.OcspServiceProvider; +import org.bouncycastle.asn1.x509.Extension; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -39,6 +41,7 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; +import javax.security.auth.x500.X500Principal; import java.security.cert.CertStore; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; @@ -79,14 +82,14 @@ void setUp() { } @ParameterizedTest - @ValueSource(strings = {"web-eid:1.1", "web-eid:1.1.0", "web-eid:1.10"}) + @ValueSource(strings = {"web-eid:1.1"}) void whenFormatIsV11OrPrefixedVariant_thenSupportsReturnsTrue(String format) { assertThat(validator.supports(format)).isTrue(); } @ParameterizedTest @NullAndEmptySource - @ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:2", "webauthn:1.1"}) + @ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:1.1.0", "web-eid:1.10", "web-eid:2", "webauthn:1.1"}) void whenFormatIsNullEmptyOrNotV11_thenSupportsReturnsFalse(String format) { assertThat(validator.supports(format)).isFalse(); } @@ -156,4 +159,51 @@ void whenSupportedSignatureAlgorithmsMissing_thenValidationFails() throws Except .hasMessage("'supportedSignatureAlgorithms' field is missing"); } } + + @Test + void whenSigningCertificateChainValidationFails_thenValidationFails() throws Exception { + WebEidAuthToken token = mock(WebEidAuthToken.class); + when(token.getFormat()).thenReturn("web-eid:1.1"); + + SupportedSignatureAlgorithm algorithm = new SupportedSignatureAlgorithm(); + algorithm.setCryptoAlgorithm("RSA"); + algorithm.setHashFunction("SHA-256"); + algorithm.setPaddingScheme("PKCS1.5"); + + UnverifiedSigningCertificate certificate = new UnverifiedSigningCertificate(); + certificate.setCertificate("abc"); + certificate.setSupportedSignatureAlgorithms(Collections.singletonList(algorithm)); + + when(token.getUnverifiedSigningCertificates()).thenReturn(Collections.singletonList(certificate)); + + X500Principal subject = new X500Principal("CN=TEST"); + byte[] authorityKeyIdentifier = new byte[] { + 0x04, 0x18, 0x30, 0x16, (byte) 0x80, 0x14, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 + }; + + X509Certificate subjectCertificate = mock(X509Certificate.class); + when(subjectCertificate.getSubjectX500Principal()).thenReturn(subject); + when(subjectCertificate.getExtensionValue(Extension.authorityKeyIdentifier.getId())) + .thenReturn(authorityKeyIdentifier); + + X509Certificate signingCertificate = mock(X509Certificate.class); + when(signingCertificate.getSubjectX500Principal()).thenReturn(subject); + when(signingCertificate.getExtensionValue(Extension.authorityKeyIdentifier.getId())) + .thenReturn(authorityKeyIdentifier); + when(signingCertificate.getKeyUsage()).thenReturn(new boolean[] {false, true}); + + AuthTokenVersion11Validator spyValidator = Mockito.spy(validator); + doReturn(subjectCertificate).when(spyValidator).validateV1(any(), any()); + + try (MockedStatic mocked = mockStatic(CertificateLoader.class)) { + mocked.when(() -> CertificateLoader.decodeCertificateFromBase64("abc")) + .thenReturn(signingCertificate); + + assertThatThrownBy(() -> spyValidator.validate(token, "nonce")) + .isInstanceOf(AuthTokenParseException.class) + .hasMessage("Signing certificate chain validation failed"); + } + } } diff --git a/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java index a8884426..c39b13ca 100644 --- a/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java +++ b/src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java @@ -61,14 +61,14 @@ class AuthTokenVersion1ValidatorTest { ); @ParameterizedTest - @ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:1.1", "web-eid:1.10"}) - void whenFormatIsAnyMajorV1Variant_thenSupportsReturnsTrue(String format) { + @ValueSource(strings = {"web-eid:1", "web-eid:1.0"}) + void whenFormatIsV1OrV10_thenSupportsReturnsTrue(String format) { assertThat(validator.supports(format)).isTrue(); } @ParameterizedTest @NullAndEmptySource - @ValueSource(strings = {"web-eid", "web-eid:0.9", "web-eid:2", "webauthn:1"}) + @ValueSource(strings = {"web-eid", "web-eid:1.1", "web-eid:1.10", "web-eid:0.9", "web-eid:2", "webauthn:1"}) void whenFormatIsNullEmptyOrNotV1_thenSupportsReturnsFalse(String format) { assertThat(validator.supports(format)).isFalse(); }