Skip to content

Commit 36b2e90

Browse files
authored
Merge branch 'master' into legacy-signature-algorithms
2 parents 6fb11b2 + 6447d43 commit 36b2e90

7 files changed

Lines changed: 283 additions & 21 deletions

File tree

MIGRATION_GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ It is recommended to start using device-link authentication flows from Smart-ID
6565
3. Replace showing verification code with showing device link or QR-code. Recommended to use device link for same device and QR-code for cross-device authentication.
6666
- [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second.
6767
4. Querying session status can be done in parallel while displaying device content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller.
68-
5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseValidator`. It will validate required fields, certificate and signature value in sessions status, and it will also handler errors.
68+
5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `DeviceLinkAuthenticationResponseValidator` or `NotificationAuthenticationResponseValidator` (depending on the flow). They will validate required fields, certificate and signature value in sessions status, and they will also handle errors.
6969
6. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2.
7070

7171
## Migrating signing

README.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -231,16 +231,16 @@ Anonymous authentication is a new feature in Smart-ID API v3.1. It allows to aut
231231
RP can learn the user's identity only after the user has authenticated themselves.
232232

233233
```java
234-
// For security reasons a new hash value must be created for each new authentication request
235-
String rpChallenge = RpChallengeGenerator.generate();
234+
// For security reasons a new RP challenge must be created for each new authentication request
235+
RpChallenge rpChallenge = RpChallengeGenerator.generate();
236236
// Store generated rpChallenge only on backend side. Do not expose it to the client side.
237237
// Used for validating authentication sessions status OK response
238238

239239
// Set up builder
240240
DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient
241241
.createDeviceLinkAuthentication()
242242
// to use anonymous authentication, do not set semantics identifier or document number
243-
.withRpChallenge(rpChallenge)
243+
.withRpChallenge(rpChallenge.toBase64EncodedValue())
244244
.withSignatureAlgorithm(AuthenticationSignatureAlgorithm.RSASSA_PSS)
245245
.withHashAlgorithm(HashAlgorithm.SHA3_512)
246246
.withInteractions(Collections.singletonList(
@@ -805,15 +805,16 @@ if ("RUNNING".equalsIgnoreCase(sessionStatus.getState())) {
805805
### Validating session status response
806806

807807
It's important to validate the session status response to ensure that the returned signature or authentication result is valid.
808-
For validating authentication session status response, use the `AuthenticationResponseValidator`.
808+
For validating authentication session status response, use `DeviceLinkAuthenticationResponseValidator` for device link flows
809+
and `NotificationAuthenticationResponseValidator` for notification-based flows.
809810
For validating signature session status response, use the `SignatureResponseValidator`.
810811
NB! Integrators must validate signature value against expected signature value.
811812

812813
#### Set up CertificateValidator
813814

814815
CertificateValidator will check if the certificate is not expired and is trusted
815816
by constructing certificate chain with trust anchors and intermediate CA certificates provided in the TrustedCACertStore.
816-
Will be used by AuthenticationResponseValidator and SignatureResponseValidator.
817+
Will be used by DeviceLinkAuthenticationResponseValidator, NotificationAuthenticationResponseValidator, CertificateChoiceResponseValidator and SignatureResponseValidator.
817818

818819
```java
819820
// Set up TrustedCACertStore
@@ -848,11 +849,12 @@ CertificateValidator certificateValidator = new CertificateValidatorImpl(trusted
848849
DeviceLinkAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator)
849850

850851
```java
851-
// Set up AuthenticationResponseValidator with the CertificateValidator
852-
DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator);
852+
// Set up DeviceLinkAuthenticationResponseValidator with the CertificateValidator
853+
DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator =
854+
DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator);
853855

854856
// Create authentication request builder
855-
DeviceLinkAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...;
857+
DeviceLinkAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...;
856858
// Initialize session
857859
DeviceLinkSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession();
858860
// Get request used for starting the authentication session and use it later to validate sessions status response
@@ -863,9 +865,13 @@ SessionStatusPoller poller = smartIdClient.getSessionStatusPoller();
863865
SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID());
864866

865867
// validate sessions state is completed
866-
if("COMPLETE".equals(sessionStatus.getState())){
868+
if ("COMPLETE".equals(sessionStatus.getState())) {
869+
// For same-device flows (Web2App/App2App), get userChallengeVerifier from callback URL
870+
String userChallengeVerifier = "<userChallengeVerifier-from-callback-url>";
871+
867872
// validate the session status response with authentication session request and return authentication identity
868-
AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo");
873+
AuthenticationIdentity authenticationIdentity =
874+
deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, "smart-id-demo");
869875
}
870876
```
871877

@@ -874,11 +880,12 @@ if("COMPLETE".equals(sessionStatus.getState())){
874880
NotificationAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator)
875881

876882
```java
877-
// Set up AuthenticationResponseValidator with the CertificateValidator
878-
NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator);
883+
// Set up NotificationAuthenticationResponseValidator with the CertificateValidator
884+
NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator =
885+
NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator);
879886

880887
// Create authentication request builder
881-
NotificationAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...;
888+
NotificationAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createNotificationAuthentication()...;
882889
// Initialize session
883890
NotificationAuthenticationSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession();
884891
// Get request used for starting the authentication session and use it later to validate sessions status response
@@ -889,9 +896,10 @@ SessionStatusPoller poller = smartIdClient.getSessionStatusPoller();
889896
SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID());
890897

891898
// validate sessions state is completed
892-
if("COMPLETE".equals(sessionStatus.getState())){
899+
if ("COMPLETE".equals(sessionStatus.getState())) {
893900
// validate the session status response with authentication session request and return authentication identity
894-
AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo");
901+
AuthenticationIdentity authenticationIdentity =
902+
notificationAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo");
895903
}
896904
```
897905

src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,14 +224,14 @@ private static void validateSignatureAlgorithmParameters(SessionSignature sessio
224224
}
225225
Optional<HashAlgorithm> maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm());
226226
if (maskGenHashAlgorithm.isEmpty()) {
227-
logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm());
227+
logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm());
228228
throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value");
229229
}
230230
if (hashAlgorithm.get() != maskGenHashAlgorithm.get()) {
231-
logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}",
231+
logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}",
232232
hashAlgorithm.get().getAlgorithmName(),
233233
maskGenHashAlgorithm.get().getAlgorithmName());
234-
throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value");
234+
throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value");
235235
}
236236

237237
if (signatureAlgorithmParameters.getSaltLength() == null) {

src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* #%L
55
* Smart ID sample Java client
66
* %%
7-
* Copyright (C) 2018 - 2025 SK ID Solutions AS
7+
* Copyright (C) 2018 - 2026 SK ID Solutions AS
88
* %%
99
* Permission is hereby granted, free of charge, to any person obtaining a copy
1010
* of this software and associated documentation files (the "Software"), to deal
@@ -30,9 +30,11 @@
3030
import static org.junit.jupiter.api.Assertions.assertThrows;
3131
import static org.junit.jupiter.api.Assertions.assertTrue;
3232

33+
import ch.qos.logback.classic.Level;
3334
import org.junit.jupiter.api.BeforeEach;
3435
import org.junit.jupiter.api.Nested;
3536
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.api.extension.ExtendWith;
3638
import org.junit.jupiter.params.ParameterizedTest;
3739
import org.junit.jupiter.params.provider.ArgumentsSource;
3840
import org.junit.jupiter.params.provider.EnumSource;
@@ -49,11 +51,18 @@
4951
import ee.sk.smartid.rest.dao.SessionSignature;
5052
import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters;
5153
import ee.sk.smartid.rest.dao.SessionStatus;
54+
import ee.sk.smartid.testhelper.log.Logs;
55+
import ee.sk.smartid.testhelper.log.LogsSpy;
56+
import ee.sk.smartid.testhelper.log.LogsSpyExtension;
5257

58+
@ExtendWith(LogsSpyExtension.class)
5359
class AuthenticationResponseMapperImplTest {
5460

5561
private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt");
5662

63+
@Logs
64+
private LogsSpy logs;
65+
5766
private AuthenticationResponseMapper authenticationResponseMapper;
5867

5968
@BeforeEach
@@ -439,6 +448,9 @@ void from_hashAlgorithmIsInvalid_throwException(String invalidHashAlgorithm) {
439448
var sessionStatus = toSessionStatus(sessionResult, sessionSignature);
440449

441450
var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus));
451+
452+
logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: " + invalidHashAlgorithm);
453+
442454
assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", exception.getMessage());
443455
}
444456

@@ -487,6 +499,9 @@ void from_algorithmValueInMaskGenAlgorithmIsInvalid_throwException() {
487499
var sessionStatus = toSessionStatus(sessionResult, sessionSignature);
488500

489501
var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus));
502+
503+
logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has invalid value: " + maskGenAlgorithm.getAlgorithm());
504+
490505
assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value", exception.getMessage());
491506
}
492507

@@ -549,6 +564,9 @@ void from_hashAlgorithmInMaskGenAlgorithmParametersInvalid_throwException(String
549564
var sessionStatus = toSessionStatus(sessionResult, sessionSignature);
550565

551566
var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus));
567+
568+
logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: " + maskGenAlgorithmParameters.getHashAlgorithm());
569+
552570
assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", exception.getMessage());
553571
}
554572

@@ -570,7 +588,10 @@ void from_hashAlgorithmInMaskGenAlgorithmDoesNotMatchSignaturesHashAlgorithm_thr
570588
var sessionStatus = toSessionStatus(sessionResult, sessionSignature);
571589

572590
var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus));
573-
assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage());
591+
592+
logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: SHA3-512, actual: SHA-512");
593+
594+
assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage());
574595
}
575596

576597
@Test
@@ -608,6 +629,9 @@ void from_saltLengthDoesNotMatchHashAlgorithmOctetLength_throwException() {
608629
var sessionStatus = toSessionStatus(sessionResult, sessionSignature);
609630

610631
var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus));
632+
633+
logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected: 64, actual: 20");
634+
611635
assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", exception.getMessage());
612636
}
613637

@@ -649,6 +673,9 @@ void from_trailerFieldValueIsInvalid_throwException() {
649673
var sessionStatus = toSessionStatus(sessionResult, sessionSignature);
650674

651675
var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus));
676+
677+
logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has invalid value: invalid");
678+
652679
assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value", exception.getMessage());
653680
}
654681

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package ee.sk.smartid.testhelper.log;
2+
3+
/*-
4+
* #%L
5+
* Smart ID sample Java client
6+
* %%
7+
* Copyright (C) 2018 - 2026 SK ID Solutions AS
8+
* %%
9+
* Permission is hereby granted, free of charge, to any person obtaining a copy
10+
* of this software and associated documentation files (the "Software"), to deal
11+
* in the Software without restriction, including without limitation the rights
12+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
* copies of the Software, and to permit persons to whom the Software is
14+
* furnished to do so, subject to the following conditions:
15+
*
16+
* The above copyright notice and this permission notice shall be included in
17+
* all copies or substantial portions of the Software.
18+
*
19+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
* THE SOFTWARE.
26+
* #L%
27+
*/
28+
29+
import org.junit.jupiter.api.extension.ExtendWith;
30+
31+
import java.lang.annotation.ElementType;
32+
import java.lang.annotation.Retention;
33+
import java.lang.annotation.RetentionPolicy;
34+
import java.lang.annotation.Target;
35+
36+
@Target({ ElementType.FIELD, ElementType.PARAMETER })
37+
@Retention(RetentionPolicy.RUNTIME)
38+
@ExtendWith(LogsSpyExtension.class)
39+
public @interface Logs {}

0 commit comments

Comments
 (0)