Skip to content

Commit 6db807d

Browse files
aarmammrts
authored andcommitted
NFC-176 Fix Web eID minor version validator routing
1 parent 711ff79 commit 6db807d

7 files changed

Lines changed: 187 additions & 35 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ It contains the following fields:
317317
318318
- `signature`: the base64-encoded signature of the token (see the description below),
319319
320-
- `format`: the type identifier and version of the token format separated by a colon character '`:`'. While minor version changes are intended to be backwards-compatible within the same major version, this validation library accepts only explicitly supported token format versions.
320+
- `format`: the type identifier and version of the token format separated by a colon character '`:`', `web-eid:1.0` as of now; the version number consists of the major and minor number separated by a dot, major version changes are incompatible with previous versions, minor version changes are backwards-compatible within the given major version,
321321
322322
- `appVersion`: the URL identifying the name and version of the application that issued the token; informative purpose, can be used to identify the affected application in case of faulty tokens.
323323
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.versionvalidators;
24+
25+
import java.util.regex.Matcher;
26+
import java.util.regex.Pattern;
27+
28+
/**
29+
* Utility for matching Web eID authentication token format version strings of the form
30+
* {@code web-eid:<major>[.<minor>]}, e.g. {@code web-eid:1} or {@code web-eid:1.1}.
31+
*/
32+
final class AuthTokenVersion {
33+
34+
// Matches 'web-eid:<major>' with an optional canonical '.<minor>', where both numbers have no leading
35+
// zeros ('0' or '[1-9]\d*') and are at most 9 digits so that they always fit in an int. Non-canonical
36+
// spellings such as 'web-eid:1.00' or 'web-eid:01' are rejected so that ambiguous version numbers cannot
37+
// bypass the more specific validators.
38+
private static final Pattern TOKEN_FORMAT_PATTERN =
39+
Pattern.compile("^web-eid:(0|[1-9]\\d{0,8})(?:\\.(0|[1-9]\\d{0,8}))?$");
40+
41+
private AuthTokenVersion() {
42+
throw new IllegalStateException("Utility class");
43+
}
44+
45+
/**
46+
* Returns whether the given token format has exactly the required major version and a minor version that
47+
* is greater than or equal to the required minor version. A missing minor version is treated as 0.
48+
* Backwards-compatible minor version changes are supported within the same major version, while an
49+
* incompatible major version change is not.
50+
*/
51+
static boolean supports(String format, int requiredExactMajorVersion, int requiredMinimalMinorVersion) {
52+
final Version version = parse(format);
53+
return version != null
54+
&& version.major() == requiredExactMajorVersion
55+
&& version.minor() >= requiredMinimalMinorVersion;
56+
}
57+
58+
/**
59+
* Returns whether the given token format has exactly the required major version and exactly the required
60+
* minor version. A missing minor version is treated as 0.
61+
*/
62+
static boolean supportsExactly(String format, int requiredExactMajorVersion, int requiredExactMinorVersion) {
63+
final Version version = parse(format);
64+
return version != null
65+
&& version.major() == requiredExactMajorVersion
66+
&& version.minor() == requiredExactMinorVersion;
67+
}
68+
69+
private static Version parse(String format) {
70+
if (format == null) {
71+
return null;
72+
}
73+
final Matcher matcher = TOKEN_FORMAT_PATTERN.matcher(format);
74+
if (!matcher.matches()) {
75+
return null;
76+
}
77+
final int major = Integer.parseInt(matcher.group(1));
78+
final int minor = matcher.group(2) == null ? 0 : Integer.parseInt(matcher.group(2));
79+
return new Version(major, minor);
80+
}
81+
82+
private record Version(int major, int minor) {
83+
}
84+
}

src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11Validator.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,12 @@
5454
import java.util.Arrays;
5555
import java.util.List;
5656
import java.util.Set;
57-
import java.util.regex.Pattern;
5857

5958
import static eu.webeid.security.util.Strings.isNullOrEmpty;
6059

6160
class AuthTokenVersion11Validator extends AuthTokenVersion1Validator implements AuthTokenVersionValidator {
6261

63-
private static final String V11_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1.1";
62+
private static final int SUPPORTED_MINIMAL_MINOR_VERSION = 1;
6463
private static final Set<String> SUPPORTED_SIGNING_CRYPTO_ALGORITHMS = Set.of("ECC", "RSA");
6564
private static final Set<String> SUPPORTED_SIGNING_PADDING_SCHEMES = Set.of("NONE", "PKCS1.5", "PSS");
6665
private static final Set<String> SUPPORTED_SIGNING_HASH_FUNCTIONS = Set.of(
@@ -96,7 +95,7 @@ public AuthTokenVersion11Validator(
9695

9796
@Override
9897
public boolean supports(String format) {
99-
return V11_SUPPORTED_TOKEN_FORMAT_PREFIX.equals(format);
98+
return AuthTokenVersion.supports(format, SUPPORTED_EXACT_MAJOR_VERSION, SUPPORTED_MINIMAL_MINOR_VERSION);
10099
}
101100

102101
@Override
@@ -140,14 +139,14 @@ private static List<X509Certificate> validateSigningCertificates(WebEidAuthToken
140139
List<UnverifiedSigningCertificate> signingCertificates = token.getUnverifiedSigningCertificates();
141140

142141
if (signingCertificates == null || signingCertificates.isEmpty()) {
143-
throw new AuthTokenParseException("'unverifiedSigningCertificates' field is missing, null or empty for format 'web-eid:1.1'");
142+
throw new AuthTokenParseException("'unverifiedSigningCertificates' field is missing, null or empty for format '" + token.getFormat() + "'");
144143
}
145144

146145
List<X509Certificate> result = new ArrayList<>();
147146

148147
for (UnverifiedSigningCertificate certificate : signingCertificates) {
149148
if (certificate == null || isNullOrEmpty(certificate.getCertificate())) {
150-
throw new AuthTokenParseException("'unverifiedSigningCertificates' contains a null or empty entry for format 'web-eid:1.1'");
149+
throw new AuthTokenParseException("'unverifiedSigningCertificates' contains a null or empty entry for format '" + token.getFormat() + "'");
151150
}
152151
validateSupportedSignatureAlgorithms(certificate);
153152
result.add(CertificateLoader.decodeCertificateFromBase64(certificate.getCertificate()));

src/main/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1Validator.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,10 @@
3636
import java.security.cert.TrustAnchor;
3737
import java.security.cert.X509Certificate;
3838
import java.util.Set;
39-
import java.util.regex.Pattern;
4039

4140
class AuthTokenVersion1Validator implements AuthTokenVersionValidator {
42-
43-
private static final String V1_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1";
41+
static final int SUPPORTED_EXACT_MAJOR_VERSION = 1;
42+
private static final int SUPPORTED_MINIMAL_MINOR_VERSION = 0;
4443
private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
4544
private final Set<TrustAnchor> trustedCACertificateAnchors;
4645
private final CertStore trustedCACertificateCertStore;
@@ -69,13 +68,12 @@ public AuthTokenVersion1Validator(
6968

7069
@Override
7170
public boolean supports(String format) {
72-
return V1_SUPPORTED_TOKEN_FORMAT_PREFIX.equals(format)
73-
|| "web-eid:1.0".equals(format);
71+
return AuthTokenVersion.supports(format, SUPPORTED_EXACT_MAJOR_VERSION, SUPPORTED_MINIMAL_MINOR_VERSION);
7472
}
7573

7674
@Override
7775
public X509Certificate validate(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException {
78-
if (isExactV10Format(token.getFormat()) && token.getUnverifiedSigningCertificates() != null) {
76+
if (AuthTokenVersion.supportsExactly(token.getFormat(), SUPPORTED_EXACT_MAJOR_VERSION, SUPPORTED_MINIMAL_MINOR_VERSION) && token.getUnverifiedSigningCertificates() != null) {
7977
throw new AuthTokenParseException(
8078
"'unverifiedSigningCertificates' field is not allowed for format '" + token.getFormat() + "'"
8179
);
@@ -108,8 +106,4 @@ public X509Certificate validate(WebEidAuthToken token, String currentChallengeNo
108106

109107
return subjectCertificate;
110108
}
111-
112-
private static boolean isExactV10Format(String format) {
113-
return V1_SUPPORTED_TOKEN_FORMAT_PREFIX.equals(format) || "web-eid:1.0".equals(format);
114-
}
115109
}

src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion11ValidatorTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,15 @@ void setUp() {
8282
}
8383

8484
@ParameterizedTest
85-
@ValueSource(strings = {"web-eid:1.1"})
86-
void whenFormatIsV11_thenSupportsReturnsTrue(String format) {
85+
@ValueSource(strings = {"web-eid:1.1", "web-eid:1.2", "web-eid:1.10", "web-eid:1.999"})
86+
void whenFormatIsV11OrHigherMinorVersion_thenSupportsReturnsTrue(String format) {
8787
assertThat(validator.supports(format)).isTrue();
8888
}
8989

9090
@ParameterizedTest
9191
@NullAndEmptySource
92-
@ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:1.1.0", "web-eid:1.10", "web-eid:1.2", "web-eid:2", "webauthn:1.1"})
93-
void whenFormatIsNullEmptyOrNotV11_thenSupportsReturnsFalse(String format) {
92+
@ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:1.", "web-eid:1.0TEST", "web-eid:1.00", "web-eid:1.1.0", "web-eid:2", "web-eid:0.9", "webauthn:1.1"})
93+
void whenFormatIsNullEmptyMinorVersion0NonCanonicalOrMalformed_thenSupportsReturnsFalse(String format) {
9494
assertThat(validator.supports(format)).isFalse();
9595
}
9696

src/test/java/eu/webeid/security/validator/versionvalidators/AuthTokenVersion1ValidatorTest.java

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,26 +61,14 @@ class AuthTokenVersion1ValidatorTest {
6161
);
6262

6363
@ParameterizedTest
64-
@ValueSource(strings = {"web-eid:1", "web-eid:1.0"})
65-
void whenFormatIsSupportedV1_thenSupportsReturnsTrue(String format) {
64+
@ValueSource(strings = {"web-eid:1", "web-eid:1.0", "web-eid:1.1", "web-eid:1.2", "web-eid:1.10", "web-eid:1.999"})
65+
void whenFormatIsValidMajorV1Format_thenSupportsReturnsTrue(String format) {
6666
assertThat(validator.supports(format)).isTrue();
6767
}
6868

6969
@ParameterizedTest
7070
@NullAndEmptySource
71-
@ValueSource(strings = {
72-
"web-eid",
73-
"web-eid:1.",
74-
"web-eid:1.0TEST",
75-
"web-eid:1.1",
76-
"web-eid:1.1.0",
77-
"web-eid:1.2",
78-
"web-eid:1.10",
79-
"web-eid:1.999",
80-
"web-eid:0.9",
81-
"web-eid:2",
82-
"webauthn:1"
83-
})
71+
@ValueSource(strings = {"web-eid", "web-eid:1.", "web-eid:1.0TEST", "web-eid:1.00", "web-eid:1.000", "web-eid:01", "web-eid:1.1.0", "web-eid:0.9", "web-eid:2", "webauthn:1"})
8472
void whenFormatIsNullEmptyOrMalformedOrNotV1_thenSupportsReturnsFalse(String format) {
8573
assertThat(validator.supports(format)).isFalse();
8674
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.versionvalidators;
24+
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.CsvSource;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
class AuthTokenVersionTest {
32+
33+
@ParameterizedTest
34+
@CsvSource({
35+
"web-eid:1, 1, 0, true",
36+
"web-eid:1.0, 1, 0, true",
37+
"web-eid:1.1, 1, 0, true",
38+
"web-eid:1.1, 1, 1, true",
39+
"web-eid:1.999, 1, 1, true",
40+
"web-eid:2.0, 2, 0, true",
41+
"web-eid:2.3, 2, 1, true",
42+
"web-eid:1.0, 1, 1, false",
43+
"web-eid:1, 1, 1, false",
44+
"web-eid:2, 1, 0, false",
45+
"web-eid:1.5, 2, 0, false",
46+
"web-eid:1.00, 1, 0, false",
47+
"web-eid:1.000, 1, 0, false",
48+
"web-eid:01, 1, 0, false",
49+
"web-eid:1., 1, 0, false",
50+
"web-eid:1.1.0, 1, 0, false",
51+
"web-eid:0.9, 1, 0, false",
52+
"webauthn:1, 1, 0, false"
53+
})
54+
void whenFormatMatchesRequiredMajorAndAtLeastRequiredMinor_thenSupportsReturnsExpected(
55+
String format, int requiredMajorVersion, int requiredMinorVersion, boolean expected) {
56+
assertThat(AuthTokenVersion.supports(format, requiredMajorVersion, requiredMinorVersion)).isEqualTo(expected);
57+
}
58+
59+
@Test
60+
void whenFormatIsNull_thenSupportsReturnsFalse() {
61+
assertThat(AuthTokenVersion.supports(null, 1, 0)).isFalse();
62+
}
63+
64+
@ParameterizedTest
65+
@CsvSource({
66+
"web-eid:1, 1, 0, true",
67+
"web-eid:1.0, 1, 0, true",
68+
"web-eid:1.1, 1, 1, true",
69+
"web-eid:2.0, 2, 0, true",
70+
"web-eid:1.1, 1, 0, false",
71+
"web-eid:1.2, 1, 1, false",
72+
"web-eid:1.0, 1, 1, false",
73+
"web-eid:1, 2, 0, false",
74+
"web-eid:1.00, 1, 0, false",
75+
"web-eid:01, 1, 0, false",
76+
"webauthn:1, 1, 0, false"
77+
})
78+
void whenFormatMatchesRequiredMajorAndExactMinor_thenSupportsExactlyReturnsExpected(
79+
String format, int requiredMajorVersion, int requiredMinorVersion, boolean expected) {
80+
assertThat(AuthTokenVersion.supportsExactly(format, requiredMajorVersion, requiredMinorVersion)).isEqualTo(expected);
81+
}
82+
83+
@Test
84+
void whenFormatIsNull_thenSupportsExactlyReturnsFalse() {
85+
assertThat(AuthTokenVersion.supportsExactly(null, 1, 0)).isFalse();
86+
}
87+
}

0 commit comments

Comments
 (0)