Skip to content

Commit dd0fef0

Browse files
authored
Improve authentication response validation (#138)
* SLIB-110 - refactor common logic from DeviceLinkAuthenticationResponseValidator and NotificationAuthenticationResponseValidator to AuthenticationResponseValidator * SLIB-110 - improve certificate purpose validation tests for authentication certificate * SLIB-110 - improve documentation and code style * SLIB-110 - improve code style
1 parent de455b3 commit dd0fef0

18 files changed

Lines changed: 566 additions & 285 deletions

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1313
* copies of the Software, and to permit persons to whom the Software is
1414
* furnished to do so, subject to the following conditions:
15-
*
15+
*
1616
* The above copyright notice and this permission notice shall be included in
1717
* all copies or substantial portions of the Software.
18-
*
18+
*
1919
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2020
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2121
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -28,6 +28,13 @@
2828

2929
import ee.sk.smartid.rest.dao.SessionStatus;
3030

31+
/**
32+
* Represents a mapper for converting a SessionStatus to an AuthenticationResponse.
33+
* <p>
34+
* Used to map the received session status to an authentication response object.
35+
* <p>
36+
* Implementers should ensure that all mandatory fields are present.
37+
*/
3138
public interface AuthenticationResponseMapper {
3239

3340
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ private static void validateSignature(SessionSignature sessionSignature) {
187187

188188
private static void validateSignatureAlgorithmParameters(SessionSignature sessionSignature) {
189189
var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters();
190-
if (sessionSignature.getSignatureAlgorithmParameters() == null) {
190+
if (signatureAlgorithmParameters == null) {
191191
throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters' is missing");
192192
}
193193
if (StringUtil.isEmpty(signatureAlgorithmParameters.getHashAlgorithm())) {
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package ee.sk.smartid;
2+
3+
/*-
4+
* #%L
5+
* Smart ID sample Java client
6+
* %%
7+
* Copyright (C) 2018 - 2025 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 java.nio.charset.StandardCharsets;
30+
import java.util.Base64;
31+
32+
import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator;
33+
import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory;
34+
import ee.sk.smartid.exception.permanent.SmartIdClientException;
35+
import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException;
36+
import ee.sk.smartid.rest.dao.AuthenticationSessionRequest;
37+
import ee.sk.smartid.rest.dao.SessionStatus;
38+
import ee.sk.smartid.util.StringUtil;
39+
40+
/**
41+
* Represents a template to validate authentication session status response.
42+
* <p>
43+
* Use implementations {@link DeviceLinkAuthenticationResponseValidator} or {@link NotificationAuthenticationResponseValidator}
44+
* to validate the flow specific authentication response.
45+
*
46+
* @param <T> the type of authentication session request
47+
*/
48+
abstract class AuthenticationResponseValidator<T extends AuthenticationSessionRequest> {
49+
50+
private final CertificateValidator certificateValidator;
51+
private final AuthenticationResponseMapper authenticationResponseMapper;
52+
private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory;
53+
private final SignatureValueValidator signatureValueValidator;
54+
55+
protected AuthenticationResponseValidator(CertificateValidator certificateValidator,
56+
AuthenticationResponseMapper authenticationResponseMapper,
57+
AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory,
58+
SignatureValueValidator signatureValueValidator) {
59+
this.certificateValidator = certificateValidator;
60+
this.authenticationResponseMapper = authenticationResponseMapper;
61+
this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory;
62+
this.signatureValueValidator = signatureValueValidator;
63+
}
64+
65+
/**
66+
* Validates the authentication session status and converts it to {@link AuthenticationIdentity}.
67+
*
68+
* @param sessionStatus the session status
69+
* @param authenticationSessionRequest the authentication session request
70+
* @param schemaName the schema name used in the QR-code or device link
71+
* @return the authentication identity
72+
*/
73+
public final AuthenticationIdentity validate(SessionStatus sessionStatus,
74+
T authenticationSessionRequest,
75+
String schemaName) {
76+
return validate(sessionStatus, authenticationSessionRequest, schemaName, null);
77+
}
78+
79+
/**
80+
* Validates the authentication session status and converts it to {@link AuthenticationIdentity}.
81+
*
82+
* @param sessionStatus the authentication session status to be validated
83+
* @param authenticationSessionRequest the authentication session request that was used to start the session
84+
* @param schemaName the schema name used in the QR-code or device link
85+
* @param brokeredRpName the brokered relying party name
86+
* @return authentication identity containing details about the authenticated user
87+
*/
88+
public final AuthenticationIdentity validate(SessionStatus sessionStatus,
89+
T authenticationSessionRequest,
90+
String schemaName,
91+
String brokeredRpName) {
92+
validateInputs(sessionStatus, authenticationSessionRequest, schemaName);
93+
AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus);
94+
validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest));
95+
validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName);
96+
return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate());
97+
}
98+
99+
/**
100+
* Constructs the payload used for signature validation.
101+
*
102+
* @param authenticationResponse the converted session status
103+
* @param authenticationSessionRequest the authentication session request to start the session
104+
* @param schemaName the schema name used in the QR-code or device link
105+
* @param brokeredRpName the brokered relying party name
106+
* @return the payload as a byte array
107+
*/
108+
protected abstract byte[] constructPayload(AuthenticationResponse authenticationResponse,
109+
T authenticationSessionRequest,
110+
String schemaName,
111+
String brokeredRpName);
112+
113+
/**
114+
* Gets the requested certificate level from the authentication session request.
115+
*
116+
* @param authenticationSessionRequest the request to get certificate level from
117+
* @return authentication certificate level
118+
*/
119+
protected abstract AuthenticationCertificateLevel getRequestedCertificateLevel(T authenticationSessionRequest);
120+
121+
private void validateInputs(SessionStatus sessionStatus, T authenticationSessionRequest, String schemaName) {
122+
if (sessionStatus == null) {
123+
throw new SmartIdClientException("Parameter 'sessionStatus' is not provided");
124+
}
125+
if (authenticationSessionRequest == null) {
126+
throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided");
127+
}
128+
if (StringUtil.isEmpty(schemaName)) {
129+
throw new SmartIdClientException("Parameter 'schemaName' is not provided");
130+
}
131+
}
132+
133+
private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) {
134+
validateCertificateLevel(authenticationResponse, requestedCertificateLevel);
135+
certificateValidator.validate(authenticationResponse.getCertificate());
136+
AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator =
137+
authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel());
138+
authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate());
139+
}
140+
141+
private void validateSignature(AuthenticationResponse authenticationResponse,
142+
T authenticationSessionRequest,
143+
String schemaName,
144+
String brokeredRpName) {
145+
byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName);
146+
signatureValueValidator.validate(authenticationResponse.getSignatureValue(),
147+
payload,
148+
authenticationResponse.getCertificate(),
149+
authenticationResponse.getRsaSsaPssSignatureParameters());
150+
}
151+
152+
protected static String toInteractionsBase64(String interactions) {
153+
return Base64.getEncoder().encodeToString(calculateInteractionsDigest(interactions));
154+
}
155+
156+
protected static String toBase64(String input) {
157+
return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8));
158+
}
159+
160+
private static byte[] calculateInteractionsDigest(String interactions) {
161+
return DigestCalculator.calculateDigest(interactions.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256);
162+
}
163+
164+
private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) {
165+
if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) {
166+
throw new CertificateLevelMismatchException();
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)