Skip to content

Commit d7588c8

Browse files
NFC-82 NFC signing support for web-eid example
Signed-off-by: Sander Kondratjev <[email protected]>
1 parent b0f90da commit d7588c8

19 files changed

+472
-170
lines changed

example/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ When the application has started, open the _ngrok_ HTTPS URL in your preferred w
8282
- [Using DigiDoc4j in production mode with the `prod` profile](#using-digidoc4j-in-production-mode-with-the-prod-profile)
8383
+ [Stateful and stateless authentication](#stateful-and-stateless-authentication)
8484
+ [Assuring that the signing and authentication certificate subjects match](#assuring-that-the-signing-and-authentication-certificate-subjects-match)
85+
+ [Requesting the signing certificate in a separate step](#requesting-the-signing-certificate-in-a-separate-step)
8586
* [HTTPS support](#https-support)
8687
+ [How to verify that HTTPS is configured properly](#how-to-verify-that-https-is-configured-properly)
8788
* [Deployment](#deployment)
@@ -119,7 +120,9 @@ The `src/main/java/eu/webeid/example` directory contains the Spring Boot applica
119120
- `WebEidChallengeNonceFilter` for issuing the challenge nonce required by the authentication flow,
120121
- `WebEidMobileAuthInitFilter` for issuing the challenge nonce and generating the deep link with the authentication request, used to initiate the mobile authentication flow,
121122
- `WebEidAjaxLoginProcessingFilter` and `WebEidLoginPageGeneratingFilter` for handling login requests.
122-
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration,
123+
- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration.
124+
- `SigningService`: prepares ASiC-E containers and finalizes signatures.
125+
- `MobileSigningService`: orchestrates the mobile signing flow (builds mobile signing requests/responses) and supports requesting the signing certificate in a separate step when enabled by configuration.
123126
- `web`: Spring Web MVC controller for the welcome page and Spring Web REST controller that provides a digital signing endpoint.
124127

125128
The `src/resources` directory contains the resources used by the application:
@@ -177,6 +180,16 @@ A common alternative to stateful authentication is stateless authentication with
177180

178181
It is usually required to verify that the signing certificate subject matches the authentication certificate subject by assuring that both ID codes match. This check is implemented at the beginning of the `SigningService.prepareContainer()` method.
179182

183+
### Requesting the signing certificate in a separate step
184+
185+
In some deployments, the signing certificate is not reused from the authentication flow. Instead, it is retrieved directly from the user’s ID-card during the signing process itself.
186+
187+
This approach is useful when the signing process is performed without a prior authentication step. For example, in a mobile flow, the user may start signing directly without authenticating beforehand. In such cases, the signing certificate must be requested separately from the user’s ID-card before the signature can be created.
188+
189+
When this mode is enabled in the configuration, the backend issues a separate request for the signing certificate using the `MobileSigningService`. The service communicates with the client to obtain the certificate before the signing container is prepared, ensuring that the correct certificate chain is available for the signature.
190+
191+
This behavior is controlled by the `request-signing-cert` flag in the `application.yaml` configuration files (`application-dev.yaml`, `application-prod.yaml`). When the flag is set to **true**, the application explicitly requests the signing certificate during the signing process, demonstrating the separate signing certificate retrieval flow. When set to **false**, the signing uses the signing certificate that was already obtained during authentication, and no additional request is made.
192+
180193
## HTTPS support
181194

182195
There are two ways of adding HTTPS support to a Spring Boot application:

example/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
<groupId>org.springframework.boot</groupId>
3939
<artifactId>spring-boot-starter-thymeleaf</artifactId>
4040
</dependency>
41+
<dependency>
42+
<groupId>org.springframework.boot</groupId>
43+
<artifactId>spring-boot-starter-validation</artifactId>
44+
</dependency>
4145

4246
<dependency>
4347
<groupId>org.digidoc4j</groupId>

example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import eu.webeid.example.security.WebEidMobileAuthInitFilter;
2929
import eu.webeid.example.security.ui.WebEidLoginPageGeneratingFilter;
3030
import eu.webeid.security.challenge.ChallengeNonceGenerator;
31+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
3132
import org.springframework.context.annotation.Bean;
3233
import org.springframework.context.annotation.Configuration;
3334
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@@ -42,6 +43,7 @@
4243
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
4344

4445
@Configuration
46+
@ConfigurationPropertiesScan
4547
@EnableWebSecurity
4648
@EnableMethodSecurity(securedEnabled = true)
4749
public class ApplicationConfiguration {

example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,19 @@ public ChallengeNonceGenerator generator(ChallengeNonceStore challengeNonceStore
7979
}
8080

8181
@Bean
82-
public AuthTokenValidator validator(YAMLConfig yamlConfig) {
82+
public AuthTokenValidator validator(WebEidAuthTokenProperties authTokenProperties) {
8383
try {
8484
return new AuthTokenValidatorBuilder()
85-
.withSiteOrigin(URI.create(yamlConfig.getLocalOrigin()))
85+
.withSiteOrigin(URI.create(authTokenProperties.validation().localOrigin()))
8686
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromCerFiles())
87-
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromTrustStore(yamlConfig))
88-
.withOcspRequestTimeout(yamlConfig.getOcspRequestTimeout())
87+
.withTrustedCertificateAuthorities(loadTrustedCACertificatesFromTrustStore(authTokenProperties))
88+
.withOcspRequestTimeout(authTokenProperties.validation().ocspRequestTimeout())
8989
.build();
9090
} catch (JceException e) {
9191
throw new RuntimeException("Error building the Web eID auth token validator.", e);
9292
}
9393
}
9494

95-
@Bean
96-
public YAMLConfig yamlConfig() {
97-
return new YAMLConfig();
98-
}
99-
10095
private X509Certificate[] loadTrustedCACertificatesFromCerFiles() {
10196
List<X509Certificate> caCertificates = new ArrayList<>();
10297

@@ -118,7 +113,7 @@ private X509Certificate[] loadTrustedCACertificatesFromCerFiles() {
118113
return caCertificates.toArray(new X509Certificate[0]);
119114
}
120115

121-
private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yamlConfig) {
116+
private X509Certificate[] loadTrustedCACertificatesFromTrustStore(WebEidAuthTokenProperties authTokenProperties) {
122117
List<X509Certificate> caCertificates = new ArrayList<>();
123118

124119
try (InputStream is = ValidationConfiguration.class.getResourceAsStream(CERTS_RESOURCE_PATH + activeProfile + "/" + TRUSTED_CERTIFICATES_JKS)) {
@@ -127,7 +122,7 @@ private X509Certificate[] loadTrustedCACertificatesFromTrustStore(YAMLConfig yam
127122
return new X509Certificate[0];
128123
}
129124
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
130-
keystore.load(is, yamlConfig.getTrustStorePassword().toCharArray());
125+
keystore.load(is, authTokenProperties.validation().trustStorePassword().toCharArray());
131126
Enumeration<String> aliases = keystore.aliases();
132127
while (aliases.hasMoreElements()) {
133128
String alias = aliases.nextElement();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2020-2025 Estonian Information System Authority
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
23+
package eu.webeid.example.config;
24+
25+
import jakarta.validation.constraints.NotBlank;
26+
import jakarta.validation.constraints.NotNull;
27+
import org.springframework.boot.context.properties.ConfigurationProperties;
28+
import org.springframework.boot.context.properties.bind.DefaultValue;
29+
import org.springframework.validation.annotation.Validated;
30+
31+
import java.time.Duration;
32+
33+
@Validated
34+
@ConfigurationProperties(prefix = "web-eid-auth-token")
35+
public record WebEidAuthTokenProperties(WebEidAuthTokenValidation validation) {
36+
37+
public record WebEidAuthTokenValidation(
38+
@NotBlank String localOrigin,
39+
String siteCertHash,
40+
@NotBlank String trustStorePassword,
41+
@DefaultValue("5s") Duration ocspRequestTimeout,
42+
@NotNull Boolean useDigiDoc4jProdConfiguration) {
43+
}
44+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2020-2025 Estonian Information System Authority
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
23+
package eu.webeid.example.config;
24+
25+
import jakarta.validation.constraints.NotBlank;
26+
import org.springframework.boot.context.properties.ConfigurationProperties;
27+
import org.springframework.validation.annotation.Validated;
28+
29+
@Validated
30+
@ConfigurationProperties(prefix = "web-eid-mobile")
31+
public record WebEidMobileProperties(
32+
@NotBlank String baseRequestUri,
33+
boolean requestSigningCert) {
34+
}

example/src/main/java/eu/webeid/example/config/YAMLConfig.java

Lines changed: 0 additions & 89 deletions
This file was deleted.

example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222

2323
package eu.webeid.example.security;
2424

25+
import eu.webeid.security.authtoken.SupportedSignatureAlgorithm;
2526
import eu.webeid.security.certificate.CertificateData;
27+
import org.springframework.lang.Nullable;
2628
import org.springframework.security.core.Authentication;
2729
import org.springframework.security.core.GrantedAuthority;
2830
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
@@ -36,21 +38,21 @@
3638
public class WebEidAuthentication extends PreAuthenticatedAuthenticationToken implements Authentication {
3739

3840
private final String idCode;
41+
private final transient String signingCertificate;
42+
private final transient List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms;
3943

40-
public static Authentication fromCertificate(X509Certificate userCertificate, List<GrantedAuthority> authorities) throws CertificateEncodingException {
41-
final String principalName = getPrincipalNameFromCertificate(userCertificate);
42-
final String idCode = CertificateData.getSubjectIdCode(userCertificate)
43-
.orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject ID code"));
44-
return new WebEidAuthentication(principalName, idCode, authorities);
45-
}
46-
47-
public String getIdCode() {
48-
return idCode;
49-
}
50-
51-
private WebEidAuthentication(String principalName, String idCode, List<GrantedAuthority> authorities) {
44+
private WebEidAuthentication(String principalName, String idCode, String signingCertificate, List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms, List<GrantedAuthority> authorities) {
5245
super(principalName, idCode, authorities);
5346
this.idCode = idCode;
47+
this.signingCertificate = signingCertificate;
48+
this.supportedSignatureAlgorithms = supportedSignatureAlgorithms;
49+
}
50+
51+
public static Authentication fromCertificate(X509Certificate userCertificate, @Nullable String signingCertificate, @Nullable List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms, List<GrantedAuthority> authorities) throws CertificateEncodingException {
52+
final String principalName = getPrincipalNameFromCertificate(userCertificate);
53+
final String idCode = CertificateData.getSubjectIdCode(userCertificate)
54+
.orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject ID code"));
55+
return new WebEidAuthentication(principalName, idCode, signingCertificate, supportedSignatureAlgorithms, authorities);
5456
}
5557

5658
private static String getPrincipalNameFromCertificate(X509Certificate userCertificate) throws CertificateEncodingException {
@@ -62,10 +64,22 @@ private static String getPrincipalNameFromCertificate(X509Certificate userCertif
6264
} else {
6365
// Organization certificates do not have given name and surname fields.
6466
return CertificateData.getSubjectCN(userCertificate)
65-
.orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject CN"));
67+
.orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject CN"));
6668
}
6769
}
6870

71+
public String getIdCode() {
72+
return idCode;
73+
}
74+
75+
public String getSigningCertificate() {
76+
return signingCertificate;
77+
}
78+
79+
public List<SupportedSignatureAlgorithm> getSupportedSignatureAlgorithms() {
80+
return supportedSignatureAlgorithms;
81+
}
82+
6983
@Override
7084
public boolean equals(Object o) {
7185
if (!super.equals(o)) return false;

0 commit comments

Comments
 (0)