Skip to content

Commit 3435762

Browse files
NFC-46 Add NFC token support for validation and cover new logic with unit tests
1 parent 937c55e commit 3435762

File tree

10 files changed

+367
-3
lines changed

10 files changed

+367
-3
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package eu.webeid.security.authtoken;
2+
3+
public class SupportedSignatureAlgorithm {
4+
private String cryptoAlgorithm;
5+
private String hashFunction;
6+
private String paddingScheme;
7+
8+
public String getCryptoAlgorithm() {
9+
return cryptoAlgorithm;
10+
}
11+
12+
public void setCryptoAlgorithm(String cryptoAlgorithm) {
13+
this.cryptoAlgorithm = cryptoAlgorithm;
14+
}
15+
16+
public String getHashFunction() {
17+
return hashFunction;
18+
}
19+
20+
public void setHashFunction(String hashFunction) {
21+
this.hashFunction = hashFunction;
22+
}
23+
24+
public String getPaddingScheme() {
25+
return paddingScheme;
26+
}
27+
28+
public void setPaddingScheme(String paddingScheme) {
29+
this.paddingScheme = paddingScheme;
30+
}
31+
}

src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2626

27+
import java.util.List;
28+
2729
@JsonIgnoreProperties(ignoreUnknown = true)
2830
public class WebEidAuthToken {
2931

@@ -32,6 +34,10 @@ public class WebEidAuthToken {
3234
private String algorithm;
3335
private String format;
3436

37+
// NFC-specific fields
38+
private String unverifiedSigningCertificate;
39+
private List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms;
40+
3541
public String getUnverifiedCertificate() {
3642
return unverifiedCertificate;
3743
}
@@ -64,4 +70,19 @@ public void setFormat(String format) {
6470
this.format = format;
6571
}
6672

73+
public String getUnverifiedSigningCertificate() {
74+
return unverifiedSigningCertificate;
75+
}
76+
77+
public void setUnverifiedSigningCertificate(String unverifiedSigningCertificate) {
78+
this.unverifiedSigningCertificate = unverifiedSigningCertificate;
79+
}
80+
81+
public List<SupportedSignatureAlgorithm> getSupportedSignatureAlgorithms() {
82+
return supportedSignatureAlgorithms;
83+
}
84+
85+
public void setSupportedSignatureAlgorithms(List<SupportedSignatureAlgorithm> supportedSignatureAlgorithms) {
86+
this.supportedSignatureAlgorithms = supportedSignatureAlgorithms;
87+
}
6788
}

src/main/java/eu/webeid/security/validator/AuthTokenValidator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
public interface AuthTokenValidator {
3636

3737
String CURRENT_TOKEN_FORMAT_VERSION = "web-eid:1";
38+
String CURRENT_NFC_TOKEN_FORMAT_VERSION = "web-eid:1.1";
3839

3940
/**
4041
* Parses the Web eID authentication token signed by the subject.

src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
6868
private OcspClient ocspClient;
6969
private OcspServiceProvider ocspServiceProvider;
7070
private final AuthTokenSignatureValidator authTokenSignatureValidator;
71+
private final NfcAuthTokenValidator nfcAuthTokenValidator;
7172

7273
/**
7374
* @param configuration configuration parameters for the token validator
@@ -86,6 +87,11 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
8687
new SubjectCertificatePolicyValidator(configuration.getDisallowedSubjectCertificatePolicies())::validateCertificatePolicies
8788
);
8889

90+
this.nfcAuthTokenValidator = new NfcAuthTokenValidator(
91+
simpleSubjectCertificateValidators,
92+
getCertTrustValidators()
93+
);
94+
8995
if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) {
9096
// The OCSP client may be provided by the API consumer.
9197
this.ocspClient = Objects.requireNonNull(ocspClient, "OCSP client must not be null when OCSP check is enabled");
@@ -155,6 +161,10 @@ private X509Certificate validateToken(WebEidAuthToken token, String currentChall
155161
}
156162
final X509Certificate subjectCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedCertificate());
157163

164+
if (token.getFormat().startsWith(CURRENT_NFC_TOKEN_FORMAT_VERSION)) {
165+
nfcAuthTokenValidator.validate(token, subjectCertificate);
166+
}
167+
158168
simpleSubjectCertificateValidators.executeFor(subjectCertificate);
159169
getCertTrustValidators().executeFor(subjectCertificate);
160170

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.security.validator;
24+
25+
import eu.webeid.security.authtoken.SupportedSignatureAlgorithm;
26+
import eu.webeid.security.authtoken.WebEidAuthToken;
27+
import eu.webeid.security.certificate.CertificateLoader;
28+
import eu.webeid.security.exceptions.AuthTokenException;
29+
import eu.webeid.security.exceptions.AuthTokenParseException;
30+
import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch;
31+
32+
import java.security.cert.X509Certificate;
33+
import java.util.List;
34+
import java.util.Set;
35+
36+
import static eu.webeid.security.util.Strings.isNullOrEmpty;
37+
38+
public class NfcAuthTokenValidator {
39+
40+
private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
41+
private final SubjectCertificateValidatorBatch certTrustValidators;
42+
43+
NfcAuthTokenValidator(
44+
SubjectCertificateValidatorBatch simpleSubjectCertificateValidators,
45+
SubjectCertificateValidatorBatch certTrustValidators
46+
) {
47+
this.simpleSubjectCertificateValidators = simpleSubjectCertificateValidators;
48+
this.certTrustValidators = certTrustValidators;
49+
}
50+
51+
void validate(WebEidAuthToken token, X509Certificate subjectCertificate) throws AuthTokenException {
52+
if (isNullOrEmpty(token.getUnverifiedSigningCertificate())) {
53+
throw new AuthTokenParseException("'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'");
54+
}
55+
56+
if (token.getSupportedSignatureAlgorithms() == null || token.getSupportedSignatureAlgorithms().isEmpty()) {
57+
throw new AuthTokenParseException("'supportedSignatureAlgorithms' field is missing");
58+
}
59+
60+
validateSupportedSignatureAlgorithms(token.getSupportedSignatureAlgorithms());
61+
62+
final X509Certificate signingCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedSigningCertificate());
63+
64+
if (!subjectCertificate.getSubjectX500Principal().equals(signingCertificate.getSubjectX500Principal())) {
65+
throw new AuthTokenParseException("Signing certificate subject does not match authentication certificate subject");
66+
}
67+
68+
simpleSubjectCertificateValidators.executeFor(signingCertificate);
69+
certTrustValidators.executeFor(signingCertificate);
70+
}
71+
72+
private static void validateSupportedSignatureAlgorithms(List<SupportedSignatureAlgorithm> algorithms) throws AuthTokenParseException {
73+
boolean hasInvalid = algorithms.stream().anyMatch(supportedSignatureAlgorithm ->
74+
!isValidCryptoAlgorithm(supportedSignatureAlgorithm.getCryptoAlgorithm())
75+
|| !isValidHashFunction(supportedSignatureAlgorithm.getHashFunction())
76+
|| !isValidPaddingScheme(supportedSignatureAlgorithm.getPaddingScheme())
77+
);
78+
79+
if (hasInvalid) {
80+
throw new AuthTokenParseException("Unsupported signature algorithm");
81+
}
82+
}
83+
84+
private static boolean isValidCryptoAlgorithm(String value) {
85+
return "RSA".equals(value) || "ECC".equals(value);
86+
}
87+
88+
private static boolean isValidHashFunction(String value) {
89+
return Set.of(
90+
"SHA-224", "SHA-256", "SHA-384", "SHA-512",
91+
"SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512"
92+
).contains(value);
93+
}
94+
95+
private static boolean isValidPaddingScheme(String value) {
96+
return "NONE".equals(value) || "PKCS1.5".equals(value) || "PSS".equals(value);
97+
}
98+
}

src/test/java/eu/webeid/security/testutil/AbstractTestWithValidator.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
package eu.webeid.security.testutil;
2424

25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
import com.fasterxml.jackson.databind.node.ObjectNode;
2527
import org.junit.jupiter.api.BeforeEach;
2628
import eu.webeid.security.authtoken.WebEidAuthToken;
2729
import eu.webeid.security.exceptions.AuthTokenException;
@@ -30,11 +32,9 @@
3032
import java.io.IOException;
3133
import java.security.cert.CertificateException;
3234

33-
import static eu.webeid.security.testutil.AuthTokenValidators.getAuthTokenValidator;
34-
3535
public abstract class AbstractTestWithValidator {
3636

37-
/*
37+
/*
3838
* notBefore Time UTCTime 2021-07-22 12:43:08 UTC
3939
* notAfter Time UTCTime 2026-07-09 21:59:59 UTC
4040
*/
@@ -43,16 +43,26 @@ public abstract class AbstractTestWithValidator {
4343
"\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.5.0+0\"," +
4444
"\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," +
4545
"\"format\":\"web-eid:1.0\"}";
46+
47+
public static final String VALID_NFC_AUTH_TOKEN = "{\"algorithm\":\"ES384\"," +
48+
"\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," +
49+
"\"unverifiedSigningCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," +
50+
"\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," +
51+
"\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," +
52+
"\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," +
53+
"\"format\":\"web-eid:1.1\"}";
4654
public static final String VALID_CHALLENGE_NONCE = "12345678123456781234567812345678912356789123";
4755

4856
protected AuthTokenValidator validator;
4957
protected WebEidAuthToken validAuthToken;
58+
protected WebEidAuthToken validNfcAuthToken;
5059

5160
@BeforeEach
5261
protected void setup() {
5362
try {
5463
validator = AuthTokenValidators.getAuthTokenValidator();
5564
validAuthToken = validator.parse(VALID_AUTH_TOKEN);
65+
validNfcAuthToken = validator.parse(VALID_NFC_AUTH_TOKEN);
5666
} catch (CertificateException | IOException | AuthTokenException e) {
5767
throw new RuntimeException(e);
5868
}
@@ -62,4 +72,11 @@ protected WebEidAuthToken replaceTokenField(String token, String field, String v
6272
final String tokenWithReplacedAlgorithm = token.replace(field, value);
6373
return validator.parse(tokenWithReplacedAlgorithm);
6474
}
75+
76+
protected WebEidAuthToken removeJsonField() throws Exception {
77+
ObjectMapper mapper = new ObjectMapper();
78+
ObjectNode node = (ObjectNode) mapper.readTree(AbstractTestWithValidator.VALID_NFC_AUTH_TOKEN);
79+
node.remove("supportedSignatureAlgorithms");
80+
return validator.parse(mapper.writeValueAsString(node));
81+
}
6582
}

src/test/java/eu/webeid/security/validator/AuthTokenAlgorithmTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,65 @@ void whenAlgorithmInvalid_thenParsingFails() throws AuthTokenException {
5959
.hasMessage("Unsupported signature algorithm");
6060
}
6161

62+
@Test
63+
void whenNfcTokenMissingSupportedAlgorithms_thenValidationFails() throws Exception {
64+
final WebEidAuthToken token = removeJsonField();
65+
66+
assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
67+
.isInstanceOf(AuthTokenParseException.class)
68+
.hasMessageContaining("'supportedSignatureAlgorithms' field is missing");
69+
}
70+
71+
@Test
72+
void whenNfcTokenHasInvalidCryptoAlgorithm_thenValidationFails() throws Exception {
73+
final WebEidAuthToken token = replaceTokenField(
74+
VALID_NFC_AUTH_TOKEN,
75+
"\"cryptoAlgorithm\":\"RSA\"",
76+
"\"cryptoAlgorithm\":\"INVALID\""
77+
);
78+
79+
assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
80+
.isInstanceOf(AuthTokenParseException.class)
81+
.hasMessage("Unsupported signature algorithm");
82+
}
83+
84+
@Test
85+
void whenNfcTokenHasInvalidHashFunction_thenValidationFails() throws Exception {
86+
final WebEidAuthToken token = replaceTokenField(
87+
VALID_NFC_AUTH_TOKEN,
88+
"\"hashFunction\":\"SHA-256\"",
89+
"\"hashFunction\":\"NOT_A_HASH\""
90+
);
91+
92+
assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
93+
.isInstanceOf(AuthTokenParseException.class)
94+
.hasMessage("Unsupported signature algorithm");
95+
}
96+
97+
@Test
98+
void whenNfcTokenHasInvalidPaddingScheme_thenValidationFails() throws Exception {
99+
final WebEidAuthToken token = replaceTokenField(
100+
VALID_NFC_AUTH_TOKEN,
101+
"\"paddingScheme\":\"PKCS1.5\"",
102+
"\"paddingScheme\":\"BAD_PADDING\""
103+
);
104+
105+
assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
106+
.isInstanceOf(AuthTokenParseException.class)
107+
.hasMessage("Unsupported signature algorithm");
108+
}
109+
110+
@Test
111+
void whenNfcTokenHasEmptySupportedAlgorithms_thenValidationFails() throws Exception {
112+
final WebEidAuthToken token = replaceTokenField(
113+
VALID_NFC_AUTH_TOKEN,
114+
"\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]",
115+
"\"supportedSignatureAlgorithms\":[]"
116+
);
117+
118+
assertThatThrownBy(() -> validator.validate(token, VALID_CHALLENGE_NONCE))
119+
.isInstanceOf(AuthTokenParseException.class)
120+
.hasMessage("'supportedSignatureAlgorithms' field is missing");
121+
}
122+
62123
}

0 commit comments

Comments
 (0)