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); } }