diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs
index b523376..bb539af 100644
--- a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs
+++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs
@@ -85,19 +85,24 @@ public class AuthTokenVersion11ValidatorTest : AbstractTestWithValidator
}
[TestCase("web-eid:1.1")]
- public void WhenFormatIsV11ThenSupportsReturnsTrue(string format) =>
+ [TestCase("web-eid:1.2")]
+ [TestCase("web-eid:1.10")]
+ [TestCase("web-eid:1.999")]
+ public void WhenFormatIsV11OrHigherMinorVersionThenSupportsReturnsTrue(string format) =>
Assert.That(v11Validator.Supports(format), Is.True);
[TestCase(null)]
[TestCase("")]
[TestCase("web-eid:1")]
[TestCase("web-eid:1.0")]
+ [TestCase("web-eid:1.")]
+ [TestCase("web-eid:1.0TEST")]
+ [TestCase("web-eid:1.00")]
[TestCase("web-eid:1.1.0")]
- [TestCase("web-eid:1.10")]
- [TestCase("web-eid:1.2")]
[TestCase("web-eid:2")]
+ [TestCase("web-eid:0.9")]
[TestCase("webauthn:1.1")]
- public void WhenFormatIsNullEmptyOrNotV11ThenSupportsReturnsFalse(string format) =>
+ public void WhenFormatIsNullEmptyMinorVersion0NonCanonicalOrMalformedThenSupportsReturnsFalse(string format) =>
Assert.That(v11Validator.Supports(format), Is.False);
[Test]
diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs
index 87e958a..1e4bdf5 100644
--- a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs
+++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs
@@ -85,6 +85,7 @@ public void SetUp()
[TestCase("web-eid:1")]
[TestCase("web-eid:1.0")]
[TestCase("web-eid:1.1")]
+ [TestCase("web-eid:1.2")]
[TestCase("web-eid:1.10")]
[TestCase("web-eid:1.999")]
public void WhenFormatIsValidMajorV1FormatThenSupportsReturnsTrue(string format) =>
@@ -95,6 +96,9 @@ public void WhenFormatIsValidMajorV1FormatThenSupportsReturnsTrue(string format)
[TestCase("web-eid")]
[TestCase("web-eid:1.")]
[TestCase("web-eid:1.0TEST")]
+ [TestCase("web-eid:1.00")]
+ [TestCase("web-eid:1.000")]
+ [TestCase("web-eid:01")]
[TestCase("web-eid:1.1.0")]
[TestCase("web-eid:0.9")]
[TestCase("web-eid:2")]
diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionTest.cs
new file mode 100644
index 0000000..8669fe9
--- /dev/null
+++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionTest.cs
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2025-2025 Estonian Information System Authority
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+namespace WebEid.Security.Tests.Validator.VersionValidators
+{
+ using NUnit.Framework;
+ using WebEid.Security.Validator.VersionValidators;
+
+ public class AuthTokenVersionTest
+ {
+ [TestCase("web-eid:1", 1, 0, true)]
+ [TestCase("web-eid:1.0", 1, 0, true)]
+ [TestCase("web-eid:1.1", 1, 0, true)]
+ [TestCase("web-eid:1.1", 1, 1, true)]
+ [TestCase("web-eid:1.999", 1, 1, true)]
+ [TestCase("web-eid:2.0", 2, 0, true)]
+ [TestCase("web-eid:2.3", 2, 1, true)]
+ [TestCase("web-eid:1.0", 1, 1, false)]
+ [TestCase("web-eid:1", 1, 1, false)]
+ [TestCase("web-eid:2", 1, 0, false)]
+ [TestCase("web-eid:1.5", 2, 0, false)]
+ [TestCase("web-eid:1.00", 1, 0, false)]
+ [TestCase("web-eid:1.000", 1, 0, false)]
+ [TestCase("web-eid:01", 1, 0, false)]
+ [TestCase("web-eid:1.", 1, 0, false)]
+ [TestCase("web-eid:1.1.0", 1, 0, false)]
+ [TestCase("web-eid:0.9", 1, 0, false)]
+ [TestCase("webauthn:1", 1, 0, false)]
+ public void WhenFormatMatchesRequiredMajorAndAtLeastRequiredMinorThenSupportsReturnsExpected(
+ string format, int requiredMajorVersion, int requiredMinorVersion, bool expected) =>
+ Assert.That(
+ AuthTokenVersion.Supports(format, requiredMajorVersion, requiredMinorVersion),
+ Is.EqualTo(expected));
+
+ [Test]
+ public void WhenFormatIsNullThenSupportsReturnsFalse() =>
+ Assert.That(AuthTokenVersion.Supports(null, 1, 0), Is.False);
+
+ [TestCase("web-eid:1", 1, 0, true)]
+ [TestCase("web-eid:1.0", 1, 0, true)]
+ [TestCase("web-eid:1.1", 1, 1, true)]
+ [TestCase("web-eid:2.0", 2, 0, true)]
+ [TestCase("web-eid:1.1", 1, 0, false)]
+ [TestCase("web-eid:1.2", 1, 1, false)]
+ [TestCase("web-eid:1.0", 1, 1, false)]
+ [TestCase("web-eid:1", 2, 0, false)]
+ [TestCase("web-eid:1.00", 1, 0, false)]
+ [TestCase("web-eid:01", 1, 0, false)]
+ [TestCase("webauthn:1", 1, 0, false)]
+ public void WhenFormatMatchesRequiredMajorAndExactMinorThenSupportsExactlyReturnsExpected(
+ string format, int requiredMajorVersion, int requiredMinorVersion, bool expected) =>
+ Assert.That(
+ AuthTokenVersion.SupportsExactly(format, requiredMajorVersion, requiredMinorVersion),
+ Is.EqualTo(expected));
+
+ [Test]
+ public void WhenFormatIsNullThenSupportsExactlyReturnsFalse() =>
+ Assert.That(AuthTokenVersion.SupportsExactly(null, 1, 0), Is.False);
+ }
+}
diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion.cs
new file mode 100644
index 0000000..8fe9019
--- /dev/null
+++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion.cs
@@ -0,0 +1,87 @@
+/*
+ * Copyright © 2025-2025 Estonian Information System Authority
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+namespace WebEid.Security.Validator.VersionValidators
+{
+ using System.Globalization;
+ using System.Text.RegularExpressions;
+
+ ///
+ /// Utility for matching Web eID authentication token format version strings of the form
+ /// web-eid:<major>[.<minor>], e.g. web-eid:1 or web-eid:1.1.
+ ///
+ internal static partial class AuthTokenVersion
+ {
+ // Matches 'web-eid:' with an optional canonical '.', where both numbers have no leading
+ // zeros ('0' or '[1-9]\d*') and are at most 9 digits so that they always fit in an int. Non-canonical
+ // spellings such as 'web-eid:1.00' or 'web-eid:01' are rejected so that ambiguous version numbers cannot
+ // bypass the more specific validators.
+ [GeneratedRegex(@"^web-eid:(0|[1-9]\d{0,8})(?:\.(0|[1-9]\d{0,8}))?$")]
+ private static partial Regex TokenFormatRegex();
+
+ ///
+ /// Returns whether the given token format has exactly the required major version and a minor version that
+ /// is greater than or equal to the required minor version. A missing minor version is treated as 0.
+ /// Backwards-compatible minor version changes are supported within the same major version, while an
+ /// incompatible major version change is not.
+ ///
+ internal static bool Supports(string format, int requiredExactMajorVersion, int requiredMinimalMinorVersion)
+ {
+ var version = Parse(format);
+ return version.HasValue &&
+ version.Value.Major == requiredExactMajorVersion &&
+ version.Value.Minor >= requiredMinimalMinorVersion;
+ }
+
+ ///
+ /// Returns whether the given token format has exactly the required major version and exactly the required
+ /// minor version. A missing minor version is treated as 0.
+ ///
+ internal static bool SupportsExactly(string format, int requiredExactMajorVersion, int requiredExactMinorVersion)
+ {
+ var version = Parse(format);
+ return version.HasValue &&
+ version.Value.Major == requiredExactMajorVersion &&
+ version.Value.Minor == requiredExactMinorVersion;
+ }
+
+ private static (int Major, int Minor)? Parse(string format)
+ {
+ if (format == null)
+ {
+ return null;
+ }
+
+ var match = TokenFormatRegex().Match(format);
+ if (!match.Success)
+ {
+ return null;
+ }
+
+ var major = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
+ var minor = match.Groups[2].Success
+ ? int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture)
+ : 0;
+
+ return (major, minor);
+ }
+ }
+}
diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs
index feec139..d540bdb 100644
--- a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs
+++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs
@@ -25,7 +25,6 @@ namespace WebEid.Security.Validator.VersionValidators
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
- using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AuthToken;
using CertValidators;
@@ -37,13 +36,12 @@ namespace WebEid.Security.Validator.VersionValidators
using Util;
///
- /// Validator for token format web-eid:1.1.
+ /// Validator for token formats with major version 1 and minor version 1 or higher, e.g. web-eid:1.1.
/// Extends V1 validator with additional checks for signing certificate + supported algorithms.
///
- public sealed partial class AuthTokenVersion11Validator : AuthTokenVersion1Validator
+ public sealed class AuthTokenVersion11Validator : AuthTokenVersion1Validator
{
- [GeneratedRegex(@"^web-eid:1\.1$", RegexOptions.IgnoreCase)]
- private static partial Regex V11SupportedTokenFormatPattern();
+ private const int SupportedMinimalMinorVersion = 1;
private static readonly HashSet SupportedSigningCryptoAlgorithms =
new(StringComparer.OrdinalIgnoreCase)
@@ -79,7 +77,8 @@ public sealed partial class AuthTokenVersion11Validator : AuthTokenVersion1Valid
private readonly ILogger logger;
///
- /// Initializes a validator for Web eID authentication tokens in format web-eid:1.1.
+ /// Initializes a validator for Web eID authentication tokens with token format major version 1
+ /// and minor version 1 or higher.
///
internal AuthTokenVersion11Validator(
SubjectCertificateValidatorBatch simpleSubjectCertificateValidators,
@@ -97,13 +96,15 @@ internal AuthTokenVersion11Validator(
this.logger = logger;
}
- ///
- protected override Regex GetSupportedFormatPattern() =>
- V11SupportedTokenFormatPattern();
+ ///
+ /// Determines whether this validator supports the specified token format.
+ ///
+ public override bool Supports(string format) =>
+ AuthTokenVersion.Supports(format, SupportedExactMajorVersion, SupportedMinimalMinorVersion);
///
- /// Validates a Web eID authentication token in format web-eid:1.1
- /// and returns the authenticated user's certificate.
+ /// Validates a Web eID authentication token with token format major version 1
+ /// and minor version 1 or higher, and returns the authenticated user's certificate.
///
public override async Task Validate(WebEidAuthToken authToken, string currentChallengeNonce)
{
@@ -154,7 +155,7 @@ private static List ValidateSigningCertificates(WebEidAuthToke
if (signingCertificates == null || signingCertificates.Count == 0)
{
throw new AuthTokenParseException(
- "'unverifiedSigningCertificates' field is missing, null or empty for format 'web-eid:1.1'");
+ $"'unverifiedSigningCertificates' field is missing, null or empty for format '{token.Format}'");
}
var result = new List();
@@ -164,7 +165,7 @@ private static List ValidateSigningCertificates(WebEidAuthToke
if (certificate == null || string.IsNullOrEmpty(certificate.Certificate))
{
throw new AuthTokenParseException(
- "'unverifiedSigningCertificates' contains a null or empty entry for format 'web-eid:1.1'");
+ $"'unverifiedSigningCertificates' contains a null or empty entry for format '{token.Format}'");
}
ValidateSupportedSignatureAlgorithms(certificate);
diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs
index 425bff1..3e9c872 100644
--- a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs
+++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs
@@ -21,9 +21,7 @@
*/
namespace WebEid.Security.Validator.VersionValidators
{
- using System;
using System.Security.Cryptography.X509Certificates;
- using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AuthToken;
using CertValidators;
@@ -33,14 +31,12 @@ namespace WebEid.Security.Validator.VersionValidators
using Util;
///
- /// Validator for token format web-eid:1.0.
+ /// Validator for token formats with major version 1, e.g. web-eid:1 and web-eid:1.0.
///
public partial class AuthTokenVersion1Validator : IAuthTokenVersionValidator
{
- private const string V1_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1";
-
- [GeneratedRegex(@"^web-eid:1(?:\.\d+)?$", RegexOptions.IgnoreCase)]
- private static partial Regex V1SupportedTokenFormatPattern();
+ internal const int SupportedExactMajorVersion = 1;
+ private const int SupportedMinimalMinorVersion = 0;
private readonly SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
private readonly AuthTokenSignatureValidator signatureValidator;
@@ -50,7 +46,7 @@ public partial class AuthTokenVersion1Validator : IAuthTokenVersionValidator
private readonly ILogger logger;
///
- /// Initializes a validator for Web eID authentication tokens in format web-eid:1.0.
+ /// Initializes a validator for Web eID authentication tokens with token format major version 1.
///
internal AuthTokenVersion1Validator(
SubjectCertificateValidatorBatch simpleSubjectCertificateValidators,
@@ -72,21 +68,15 @@ internal AuthTokenVersion1Validator(
/// Determines whether this validator supports the specified token format.
///
public virtual bool Supports(string format) =>
- format != null &&
- GetSupportedFormatPattern().IsMatch(format);
-
- ///
- /// Gets the supported token format pattern for this validator.
- ///
- protected virtual Regex GetSupportedFormatPattern() =>
- V1SupportedTokenFormatPattern();
+ AuthTokenVersion.Supports(format, SupportedExactMajorVersion, SupportedMinimalMinorVersion);
///
/// Validates a Web eID authentication token and returns the authenticated user's certificate.
///
public virtual async Task Validate(WebEidAuthToken authToken, string currentChallengeNonce)
{
- if (IsExactV10Format(authToken.Format) && authToken.UnverifiedSigningCertificates != null)
+ if (AuthTokenVersion.SupportsExactly(authToken.Format, SupportedExactMajorVersion, SupportedMinimalMinorVersion) &&
+ authToken.UnverifiedSigningCertificates != null)
{
throw new AuthTokenParseException($"'unverifiedSigningCertificates' field is not allowed for format '{authToken.Format}'");
}
@@ -123,9 +113,5 @@ public virtual async Task Validate(WebEidAuthToken authToken,
return subjectCertificate;
}
-
- private static bool IsExactV10Format(string format) =>
- V1_SUPPORTED_TOKEN_FORMAT_PREFIX.Equals(format, StringComparison.OrdinalIgnoreCase) ||
- "web-eid:1.0".Equals(format, StringComparison.OrdinalIgnoreCase);
}
}