From 8cb29cd54908bfa94e156c7e65a34f81ca7338ea Mon Sep 17 00:00:00 2001 From: "Sander.Kondratjev" Date: Fri, 21 Nov 2025 16:08:09 +0200 Subject: [PATCH 1/4] NFC-99 Add web-eid-1.1 token support Signed-off-by: Sander Kondratjev --- README.md | 110 ++++++++ example/README.md | 1 + .../TestUtils/AbstractTestWithValidator.cs | 26 +- .../Validator/AuthTokenAlgorithmTest.cs | 60 ++++- .../AuthTokenSignatureValidatorTests.cs | 95 ++++++- .../Validator/AuthTokenStructureTest.cs | 2 +- .../AuthTokenV11CertificateTest.cs | 139 +++++++++++ .../AuthTokenVersion11ValidatorTest.cs | 133 ++++++++++ .../AuthTokenVersion1ValidatorTest.cs | 117 +++++++++ .../AuthTokenVersionValidatorFactoryTest.cs | 108 ++++++++ .../AuthToken/SupportedSignatureAlgorithm.cs | 46 ++++ .../AuthToken/WebEidAuthToken.cs | 10 + .../Util/X509Certificate2Extensions.cs | 41 +++ .../Validator/AuthTokenValidator.cs | 118 ++------- .../ISubjectCertificateValidator.cs | 2 +- .../SubjectCertificateNotRevokedValidator.cs | 2 +- .../SubjectCertificatePolicyValidator.cs | 2 +- .../SubjectCertificateTrustedValidator.cs | 2 +- .../SubjectCertificateValidatorBatch.cs | 2 +- .../Validator/IAuthTokenValidator.cs | 7 +- .../AuthTokenVersion11Validator.cs | 236 ++++++++++++++++++ .../AuthTokenVersion1Validator.cs | 127 ++++++++++ .../AuthTokenVersionValidatorFactory.cs | 137 ++++++++++ .../IAuthTokenVersionValidator.cs | 50 ++++ 24 files changed, 1438 insertions(+), 135 deletions(-) create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs create mode 100644 src/WebEid.Security/AuthToken/SupportedSignatureAlgorithm.cs create mode 100644 src/WebEid.Security/Util/X509Certificate2Extensions.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs diff --git a/README.md b/README.md index 513a5c4..03d9474 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,116 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e } ``` +- Similarly, the `MobileAuthInitController` generates a challenge nonce and returns the mobile deep-link for starting the Web eID Mobile authentication flow, and the `MobileAuthLoginController` handles the mobile login request by validating the returned authentication token and creating the authentication cookie. + ```cs + using System; + using System.Text; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Options; + using System.Text.Json; + using System.Text.Json.Serialization; + using Options; + using Security.Challenge; + + [ApiController] + [Route("auth/mobile")] + public class MobileAuthInitController( + IChallengeNonceGenerator nonceGenerator, + IOptions mobileOptions + ) : ControllerBase + { + private const string WebEidMobileAuthPath = "auth"; + private const string MobileLoginPath = "/auth/mobile/login"; + + [HttpPost("init")] + public IActionResult Init() + { + var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5)); + var challengeBase64 = challenge.Base64EncodedNonce; + + var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}"; + + var payload = new AuthPayload + { + Challenge = challengeBase64, + LoginUri = loginUri, + GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null + }; + + var json = JsonSerializer.Serialize(payload); + var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + + var authUri = BuildAuthUri(encodedPayload); + + return Ok(new AuthUri + { + AuthUriValue = authUri + }); + } + ``` + + ```cs + using Microsoft.AspNetCore.Mvc; + using System.Text.Json; + using Dto; + using Security.Challenge; + using Security.Validator; + using System.Security.Claims; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Authentication; + using Microsoft.AspNetCore.Authentication.Cookies; + using Security.Util; + + [ApiController] + [Route("auth/mobile")] + public class MobileAuthLoginController( + IAuthTokenValidator authTokenValidator, + IChallengeNonceStore challengeNonceStore + ) : ControllerBase + { + [HttpPost("login")] + public async Task MobileLogin([FromBody] AuthenticateRequestDto dto) + { + if (dto?.AuthToken == null) + { + return BadRequest(new { error = "Missing auth_token" }); + } + + var parsedToken = dto.AuthToken; + var certificate = await authTokenValidator.Validate( + parsedToken, + challengeNonceStore.GetAndRemove().Base64EncodedNonce); + + var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); + + identity.AddClaim(new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName())); + identity.AddClaim(new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname())); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode())); + identity.AddClaim(new Claim(ClaimTypes.Name, certificate.GetSubjectCn())); + + if (!string.IsNullOrEmpty(parsedToken.UnverifiedSigningCertificate)) + { + identity.AddClaim(new Claim("signingCertificate", parsedToken.UnverifiedSigningCertificate)); + } + + if (parsedToken.SupportedSignatureAlgorithms != null) + { + identity.AddClaim(new Claim( + "supportedSignatureAlgorithms", + JsonSerializer.Serialize(parsedToken.SupportedSignatureAlgorithms))); + } + + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(identity), + new AuthenticationProperties { IsPersistent = false }); + + return Ok(new { redirect = "/welcome" }); + } + } + ``` + + # Table of contents * [Introduction](#introduction) diff --git a/example/README.md b/example/README.md index 24cf3fe..83d046d 100644 --- a/example/README.md +++ b/example/README.md @@ -130,6 +130,7 @@ The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application s - `DigiDoc`: contains the C# binding files of the `libdigidocpp` library; these files must be copied from the `libdigidocpp` installation directory `\include\digidocpp_csharp`, - `Pages`: Razor pages, - `Services`: Web eID signing service implementation that uses `libdigidocpp`. +- `Options`: strongly-typed configuration classes for mobile Web eID settings such as `BaseRequestUri` and `RequestSigningCert` (when set to false, initiates a separate signing-certificate flow to demo requesting the certificate without prior authentication, as the signing certificate normally comes from the authentication flow). ## More information diff --git a/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs b/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs index 38d8c22..3421d13 100644 --- a/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs +++ b/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs @@ -34,6 +34,15 @@ public abstract class AbstractTestWithValidator "\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," + "\"signature\":\"tbMTrZD4CKUj6atjNCHZruIeyPFAEJk2htziQ1t08BSTyA5wKKqmNmzsJ7562hWQ6+tJd6nlidHGE5jVVJRKmPtNv3f9gbT2b7RXcD4t5Pjn8eUCBCA4IX99Af32Z5ln\"," + "\"format\":\"web-eid:1\"}"; + + protected const string ValidV11AuthTokenStr = "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," + + "\"unverifiedSigningCertificate\":\"MIID6zCCA02gAwIBAgIQT7j6zk6pmVRcyspLo5SqejAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUzMVoXDTI5MDUwMjEwNDUzMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASkwENR8GmCpEs6OshDWDfIiKvGuyNMOD2rjIQW321AnZD3oIsqD0svBMNEJJj9Dlvq/47TYDObIa12KAU5IuOBfJs2lrFdSXZjaM+a5TWT3O2JTM36YDH2GcMe/eisepejggGrMIIBpzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBIBgNVHSAEQTA/MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBTVX3s48Spy/Es2TcXgkRvwUn2YcjCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwEwYGBACORgEGMAkGBwQAjkYBBgEwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgGBr+Jbo1GeqgWdIwgMo7SA29AP38JxNm2HWq2Qb+kIHpusAK574Co1K5D4+Mk7/ITTuXQaET5WphHoN7tdAciTaQJBAn0zBigYyVPYSTO68HM6hmlwTwi/KlJDdXW/2NsMjSqofFFJXpGvpxk2CTqSRCjcavxLPnkasTbNROYSJcmM8Xc=\"," + + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," + + "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," + + "\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," + + "\"format\":\"web-eid:1.1\"}"; + public const string ValidChallengeNonce = "12345678123456781234567812345678912356789123"; private DateTimeProvider dateTimeProvider; @@ -44,15 +53,22 @@ public abstract class AbstractTestWithValidator [SetUp] protected void SetUp() { - this.Validator = AuthTokenValidators.GetAuthTokenValidator(); - this.ValidAuthToken = this.Validator.Parse(ValidAuthTokenStr); - this.dateTimeProvider = DateTimeProvider.OverrideUtcNow(new DateTime(2021, 3, 1)); + Validator = AuthTokenValidators.GetAuthTokenValidator(); + ValidAuthToken = Validator.Parse(ValidAuthTokenStr); + dateTimeProvider = DateTimeProvider.OverrideUtcNow(new DateTime(2021, 8, 1)); } [TearDown] - public void TearDown() => this.dateTimeProvider?.Dispose(); + public void TearDown() => dateTimeProvider?.Dispose(); protected WebEidAuthToken ReplaceTokenField(string token, string field, string value) => - this.Validator.Parse(token.Replace(field, value)); + Validator.Parse(token.Replace(field, value)); + + protected string RemoveJsonField(string json, string fieldName) + { + var node = Newtonsoft.Json.Linq.JObject.Parse(json); + node.Remove(fieldName); + return node.ToString(Newtonsoft.Json.Formatting.None); + } } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs index caaa10c..ecc03d9 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs @@ -22,33 +22,77 @@ namespace WebEid.Security.Tests.Validator { using NUnit.Framework; - using WebEid.Security.Exceptions; - using WebEid.Security.Tests.TestUtils; + using Exceptions; + using TestUtils; public class AuthTokenAlgorithmTest : AbstractTestWithValidator { [Test] public void WhenAlgorithmNoneThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", "NONE"); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", "NONE"); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("Unsupported signature algorithm"); } [Test] public void WhenAlgorithmEmptyThenParsingFailsAsync() { - var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", ""); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", ""); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("'algorithm' is null or empty"); } [Test] public void WhenAlgorithmInvalidThenParsingFailsAsync() { - var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", "\u0000\t\ninvalid"); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", "\u0000\t\ninvalid"); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("Unsupported signature algorithm"); } + + [Test] + public void WhenV11TokenMissingSupportedAlgorithmsThenValidationFailsAsync() + { + var tokenJson = RemoveJsonField(ValidV11AuthTokenStr, "supportedSignatureAlgorithms"); + var token = Validator.Parse(tokenJson); + + var ex = Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + + Assert.That(ex.Message, Does.Contain("'supportedSignatureAlgorithms' field is missing")); + } + + [Test] + public void WhenV11TokenHasInvalidCryptoAlgorithmThenValidationFailsAsync() + { + var token = ReplaceTokenField(ValidV11AuthTokenStr, "\"cryptoAlgorithm\":\"RSA\"", "\"cryptoAlgorithm\":\"INVALID\""); + Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) + .WithMessage("Unsupported signature algorithm"); + } + + [Test] + public void WhenV11TokenHasInvalidHashFunctionThenValidationFailsAsync() + { + var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"hashFunction\":\"SHA-256\"", "\"hashFunction\":\"NOT_A_HASH\""); + Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) + .WithMessage("Unsupported signature algorithm"); + } + + [Test] + public void WhenV11TokenHasInvalidPaddingSchemeThenValidationFailsAsync() + { + var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"paddingScheme\":\"PKCS1.5\"", "\"paddingScheme\":\"BAD_PADDING\""); + Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) + .WithMessage("Unsupported signature algorithm"); + } + + [Test] + public void WhenV11TokenHasEmptySupportedAlgorithmsThenValidationFailsAsync() + { + var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]", "\"supportedSignatureAlgorithms\":[]"); + Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) + .WithMessage("'supportedSignatureAlgorithms' field is missing"); + } } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs b/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs index 6862067..0a8e13f 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs @@ -36,11 +36,25 @@ public class AuthTokenSignatureValidatorTests : AbstractTestWithValidator "\"issuerApp\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," + "\"signature\":\"xsjXsQvVYXWcdV0YPhxLthJxtf0//R8p9WFFlYJGRARrl1ruyoAUwl0xeHgeZOKeJtwiCYCNWJzCG3VM3ydgt92bKhhk1u0JXIPVqvOkmDY72OCN4q73Y8iGSPVTgjk93TgquHlodf7YcqZNhutwNNf3oldHEWJD5zmkdwdpBFXgeOwTAdFwGljDQZbHr3h1Dr+apUDuloS0WuIzUuu8YXN2b8lh8FCTlF0G0DEjhHd/MGx8dbe3UTLHmD7K9DXv4zLJs6EF9i2v/C10SIBQDkPBSVPqMxCDPECjbEPi2+ds94eU7ThOhOQlFFtJ4KjQNTUa2crSixH7cYZF2rNNmA==\"," + "\"format\":\"web-eid:1.0\"}"; + private const string ValidV11Rs256AuthToken = "{\"algorithm\":\"RS256\"," + + "\"unverifiedCertificate\":\"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\"," + + "\"unverifiedSigningCertificate\":\"X5C\"," + + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," + + "\"issuerApp\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," + + "\"signature\":\"xsjXsQvVYXWcdV0YPhxLthJxtf0//R8p9WFFlYJGRARrl1ruyoAUwl0xeHgeZOKeJtwiCYCNWJzCG3VM3ydgt92bKhhk1u0JXIPVqvOkmDY72OCN4q73Y8iGSPVTgjk93TgquHlodf7YcqZNhutwNNf3oldHEWJD5zmkdwdpBFXgeOwTAdFwGljDQZbHr3h1Dr+apUDuloS0WuIzUuu8YXN2b8lh8FCTlF0G0DEjhHd/MGx8dbe3UTLHmD7K9DXv4zLJs6EF9i2v/C10SIBQDkPBSVPqMxCDPECjbEPi2+ds94eU7ThOhOQlFFtJ4KjQNTUa2crSixH7cYZF2rNNmA==\"," + + "\"format\":\"web-eid:1.1\"}"; private const string AuthTokenWithWrongCert = "{\"algorithm\":\"ES384\"," + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQH9NeN14jo0ReaircrN2YvDAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIwMDMxMjEyMjgxMloXDTI1MDMxMjIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARVeP+9l3b1mm3fMHPeCFLbD7esXI8lDc+soWCBoMnZGo3d2Rg/mzKCIWJtw+JhcN7RwFFH9cwZ8Gni4C3QFYBIIJ2GdjX2KQfEkDvRsnKw6ZZmJQ+HC4ZFew3r8gauhfejggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFOfk7lPOq6rb9IbFZF1q97kJ4s2iMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgEQRbzFOSHIcmIEKczhN8xuteYgN2zEXZSJdP0q1iH1RR2AzZ8Ddz6SKRn/bZSzjcd4b7h3AyOEQr2hcidYkxT7sAJCAMPtOUryqp2WbTEUoOpbWrKqp8GjaAiVpBGDn/Xdu5M2Z6dvwZHnFGgRrZXtyUbcAgRW7MQJ0s/9GCVro3iqUzNN\"," + "\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," + "\"signature\":\"arx164xRiwhIQDINe0J+ZxJWZFOQTx0PBtOaWaxAe7gofEIHRIbV1w0sOCYBJnvmvMem9hU4nc2+iJx2x8poYck4Z6eI3GwtiksIec3XQ9ZIk1n/XchXnmPn3GYV+HzJ\"," + "\"format\":\"web-eid:1.0\"}"; + private const string V11AuthTokenWithWrongCert = "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEDDCCA26gAwIBAgIQM8UTDe8zVKtcysotoMgBlzAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUwMVoXDTI5MDUwMjEwNDUwMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARh/M6SBatkyMHjTmRgIF1MTqZpVIfqHZD6MrQUHdlykVSLNBmloFjoXbQbSe0l+sgKUPSZWb48IGPC7Mrudt5vLvnKy31qZ5a+2Ceg87NrVzdNCWF2oQrwXw63HieIBMmjggHMMIIByDAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwKAYDVR0RBCEwH4EdamFhay1rcmlzdGphbi5qb2VvcmdAZWVzdGkuZWUwHQYDVR0OBBYEFOSW4XJH0oDJAh2nEqFGhrlF9zXQMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQQjG3AnPzJdtmoaNI59T8vcjsNjVB5XLfUXiBguizma9I6dFqhHiTtfqo2aWpd+dcL8iz/3Dn03C0ruPLnJVt24lAkIB8M6KO+RcVJqXz8KXMUGstjK+1iIE0hd+2JtNmIJcqgNT7sj8f4NZfsix5JuUpY1j4msWG3k0h79U2bWcR8NQZdU=\"," + + "\"unverifiedSigningCertificate\":\"MIID6zCCA02gAwIBAgIQT7j6zk6pmVRcyspLo5SqejAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUzMVoXDTI5MDUwMjEwNDUzMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASkwENR8GmCpEs6OshDWDfIiKvGuyNMOD2rjIQW321AnZD3oIsqD0svBMNEJJj9Dlvq/47TYDObIa12KAU5IuOBfJs2lrFdSXZjaM+a5TWT3O2JTM36YDH2GcMe/eisepejggGrMIIBpzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBIBgNVHSAEQTA/MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBTVX3s48Spy/Es2TcXgkRvwUn2YcjCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwEwYGBACORgEGMAkGBwQAjkYBBgEwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgGBr+Jbo1GeqgWdIwgMo7SA29AP38JxNm2HWq2Qb+kIHpusAK574Co1K5D4+Mk7/ITTuXQaET5WphHoN7tdAciTaQJBAn0zBigYyVPYSTO68HM6hmlwTwi/KlJDdXW/2NsMjSqofFFJXpGvpxk2CTqSRCjcavxLPnkasTbNROYSJcmM8Xc=\"," + + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," + + "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," + + "\"signature\":\"arx164xRiwhIQDINe0J+ZxJWZFOQTx0PBtOaWaxAe7gofEIHRIbV1w0sOCYBJnvmvMem9hU4nc2+iJx2x8poYck4Z6eI3GwtiksIec3XQ9ZIk1n/XchXnmPn3GYV+HzJ\"," + + "\"format\":\"web-eid:1.1\"}"; private static readonly TextInfo Ti = new CultureInfo("et-EE", false).TextInfo; @@ -48,7 +62,7 @@ public class AuthTokenSignatureValidatorTests : AbstractTestWithValidator public void WhenValidES384SignatureThenValidationSucceedsAsync() => Assert.DoesNotThrowAsync(async () => { - var certificate = await this.Validator.Validate(this.ValidAuthToken, ValidChallengeNonce); + var certificate = await Validator.Validate(ValidAuthToken, ValidChallengeNonce); Assert.That(certificate, Is.Not.Null); Assert.That(certificate.GetSubjectCn(), Is.EqualTo("JÕEORG,JAAK-KRISTJAN,38001085718")); @@ -62,7 +76,7 @@ public void WhenValidES384SignatureThenValidationSucceedsAsync() => public void WhenValidRS256SignatureThenValidationSucceeds() => Assert.DoesNotThrow(() => { - var authToken = this.Validator.Parse(ValidRs256AuthToken); + var authToken = Validator.Parse(ValidRs256AuthToken); var publicKey = X509CertificateExtensions.ParseCertificate(authToken.UnverifiedCertificate, "unverifiedCertificate") .GetAsymmetricPublicKey() @@ -74,24 +88,93 @@ public void WhenValidRS256SignatureThenValidationSucceeds() => [Test] public void WhenValidTokenAndWrongChallengeNonceThenValidationFailsAsync() => Assert.ThrowsAsync(() => - this.Validator.Validate(this.ValidAuthToken, "12345678123456781234567812345678912356789124")); + Validator.Validate(ValidAuthToken, "12345678123456781234567812345678912356789124")); [Test] public void WhenValidTokenAndWrongOriginThenValidationFailsAsync() { var authTokenValidator = AuthTokenValidators.GetAuthTokenValidator("https://invalid.org"); Assert.ThrowsAsync(() => - authTokenValidator.Validate(this.ValidAuthToken, ValidChallengeNonce)); + authTokenValidator.Validate(ValidAuthToken, ValidChallengeNonce)); } [Test] public void WhenTokenWithWrongCertThenValidationFailsAsync() { - var authTokenWithWrongCert = this.Validator.Parse(AuthTokenWithWrongCert); + var authTokenWithWrongCert = Validator.Parse(AuthTokenWithWrongCert); Assert.ThrowsAsync(() => - this.Validator.Validate(authTokenWithWrongCert, ValidChallengeNonce)); + Validator.Validate(authTokenWithWrongCert, ValidChallengeNonce)); + } + + [Test] + public void WhenValidV11TokenAndNonceThenValidationSucceedsAsync() => + Assert.DoesNotThrowAsync(async () => + { + var token = Validator.Parse(ValidV11AuthTokenStr); + + var certificate = await Validator.Validate(token, ValidChallengeNonce); + + Assert.That(certificate, Is.Not.Null); + Assert.That(certificate.GetSubjectCn(), Is.EqualTo("JÕEORG,JAAK-KRISTJAN,38001085718")); + Assert.That(ToTitleCase(certificate.GetSubjectGivenName()), Is.EqualTo("Jaak-Kristjan")); + Assert.That(ToTitleCase(certificate.GetSubjectSurname()), Is.EqualTo("Jõeorg")); + Assert.That(certificate.GetSubjectIdCode(), Is.EqualTo("PNOEE-38001085718")); + Assert.That(certificate.GetSubjectCountryCode(), Is.EqualTo("EE")); + }); + + [Test] + public void WhenV11TokenWithWrongChallengeNonceThenValidationFailsAsync() + { + var token = Validator.Parse(ValidV11AuthTokenStr); + + var ex = Assert.ThrowsAsync(() => + Validator.Validate(token, "12345678123456781234567812345678912356789124"))!; + + Assert.That(ex.Message, Does.Contain("signature")); } + [Test] + public void WhenV11TokenWithWrongOriginThenValidationFailsAsync() + { + var validatorWithWrongOrigin = + AuthTokenValidators.GetAuthTokenValidator("https://wrong-origin.com"); + + var token = validatorWithWrongOrigin.Parse(ValidV11AuthTokenStr); + + var ex = Assert.ThrowsAsync(() => + validatorWithWrongOrigin.Validate(token, ValidChallengeNonce))!; + + Assert.That(ex.Message, Does.Contain("Token signature validation has failed")); + } + + [Test] + public void WhenV11TokenWithWrongCertThenValidationFailsAsync() + { + var token = Validator.Parse(V11AuthTokenWithWrongCert); + + var ex = Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce))!; + + Assert.That(ex.Message, Does.Contain("signature")); + } + + [Test] + public void WhenValidRS256V11SignatureThenValidationSucceeds() => + Assert.DoesNotThrow(() => + { + var authToken = Validator.Parse(ValidV11Rs256AuthToken); + var publicKey = + X509CertificateExtensions + .ParseCertificate(authToken.UnverifiedCertificate, "unverifiedCertificate") + .GetAsymmetricPublicKey() + .CreateSecurityKeyWithoutCachingSignatureProviders(); + + var signatureValidator = new AuthTokenSignatureValidator(new Uri("https://ria.ee")); + + signatureValidator.Validate( "RS256", publicKey, authToken.Signature, ValidChallengeNonce); + }); + + private static string ToTitleCase(string s) => Ti.ToTitleCase(Ti.ToLower(s)); } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs index 9887dfb..f262fdf 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs @@ -52,7 +52,7 @@ public void WhenUnknownTokenVersionThenParsingFailsAsync() { var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "web-eid:1", "invalid"); Assert.ThrowsAsync(() => this.Validator.Validate(authToken, "")) - .WithMessage("Only token format version 'web-eid:1' is currently supported"); + .WithMessage("Token format version 'invalid' is currently not supported"); } } } diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs new file mode 100644 index 0000000..b9480f4 --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs @@ -0,0 +1,139 @@ +/* + * Copyright © 2020-2024 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 System; + using System.Collections.Generic; + using NUnit.Framework; + using Exceptions; + using WebEid.Security.Validator; + using System.Text.Json; + using AuthToken; + using TestUtils; + + public class AuthTokenV11CertificateTest : AbstractTestWithValidator + { + private const string V11AuthTokenStr = "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," + + "\"unverifiedSigningCertificate\":\"X5C\"," + + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," + + "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," + + "\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," + + "\"format\":\"web-eid:1.1\"}"; + + private const string DifferentCertBase64 = "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"; + private const string MissingKeyUsageCert = "MIICxjCCAa6gAwIBAgIJANTbd26vS6fmMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNVBAMTCndlYi1laWQuZXUwHhcNMjAwOTI0MTIyNDMzWhcNMzAwOTIyMTIyNDMzWjAVMRMwEQYDVQQDEwp3ZWItZWlkLmV1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAza5qBFu5fvs47rx3o9yzBVfIxHjMotID8ppkwWVen/uFxlqsRVi+XnWkggW+K8X45inAnBAVi1rIw7GQNdacSHglyvQfwM64AallmD0+K+QgbqxcO9fvRvlAeISENBc2bGgqTIytPEON5ZmazzbOZjqY3M1QcPlPZOeUm6M9ZcZFhsxpiB4gwZUic9tnCz9eujd6k6DzNVfSRaJcpGA5hJ9aKH4vXS3x7anewna+USEXkRb4Il5zSlZR0i1yrVA1YNOxCG/+GgWvXfvXwdQ0z9BpGwNEyc0mRDNx+umaTukz9t+7/qTcB2JLTuiwM9Gqg5sDDnzPlcZSa7GnIU0MLQIDAQABoxkwFzAVBgNVHREEDjAMggp3ZWItZWlkLmV1MA0GCSqGSIb3DQEBBQUAA4IBAQAYGkBhTlet47uw3JYunYo6dj4nGWSGV4x6LYjCp5QlAmGd28HpC1RFB3ba+inwW8SP69kEOcB0sJQAZ/tV90oCATNsy/Whg/TtiHISL2pr1dyBoKDRWbgTp8jjzcp2Bj9nL14aqpj1t4K1lcoYETX41yVmyyJu6VFs80M5T3yikm2giAhszjChnjyoT2kaEKoua9EUK9SS27pVltgbbvtmeTp3ZPHtBfiDOATL6E03RZ5WfMLRefI796a+RcznnudzQHhMSwcjLpMDgIWpUU4OU7RiwrU+S3MrvgzCjkWh2MGu/OGLB+d3JZoW+eCvigoshmAsbJCMLbh4N78BCPqk"; + + private AuthTokenValidationConfiguration config; + + [SetUp] + public void Init() + { + config = new AuthTokenValidationConfiguration + { + SiteOrigin = new Uri("https://example.com"), + IsUserCertificateRevocationCheckWithOcspEnabled = false + }; + config.TrustedCaCertificates.Clear(); + } + + [Test] + public void WhenValidV11TokenThenValidationSucceeds() + { + var authTokenValidator = new AuthTokenValidator(config); + var token = authTokenValidator.Parse(ValidV11AuthTokenStr); + Assert.DoesNotThrowAsync(() => Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateMissingThenValidationFails() + { + var tokenFields = JsonSerializer.Deserialize>(V11AuthTokenStr); + tokenFields.Remove("unverifiedSigningCertificate"); + var modifiedTokenJson = JsonSerializer.Serialize(tokenFields); + var token = JsonSerializer.Deserialize(modifiedTokenJson); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateIsNotBase64ThenValidationFails() + { + var tokenFields = JsonSerializer.Deserialize>(V11AuthTokenStr); + tokenFields["unverifiedSigningCertificate"] = "This is not a certificate"; + var modifiedTokenJson = JsonSerializer.Serialize(tokenFields); + var token = JsonSerializer.Deserialize(modifiedTokenJson); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateIsNotACertificateThenValidationFails() + { + var tokenFields = JsonSerializer.Deserialize>(V11AuthTokenStr); + tokenFields["unverifiedSigningCertificate"] = "VGhpcyBpcyBub3QgYSBjZXJ0aWZpY2F0ZQ"; + var modifiedTokenJson = JsonSerializer.Serialize(tokenFields); + var token = JsonSerializer.Deserialize(modifiedTokenJson); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateSubjectDoesNotMatchThenValidationFails() + { + var tokenFields = JsonSerializer.Deserialize>(V11AuthTokenStr); + tokenFields["unverifiedSigningCertificate"] = DifferentCertBase64; + var modifiedTokenJson = JsonSerializer.Serialize(tokenFields); + var token = JsonSerializer.Deserialize(modifiedTokenJson); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateNotIssuedBySameAuthorityThenValidationFails() + { + var tokenFields = JsonSerializer.Deserialize>(V11AuthTokenStr); + tokenFields["unverifiedSigningCertificate"] = ValidChallengeNonce; + var modifiedTokenJson = JsonSerializer.Serialize(tokenFields); + var token = JsonSerializer.Deserialize(modifiedTokenJson); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateKeyUsageInvalidThenValidationFails() + { + var tokenFields = JsonSerializer.Deserialize>(V11AuthTokenStr); + tokenFields["unverifiedSigningCertificate"] = MissingKeyUsageCert; + var modifiedTokenJson = JsonSerializer.Serialize(tokenFields); + var token = JsonSerializer.Deserialize(modifiedTokenJson); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + } +} diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs new file mode 100644 index 0000000..83d47f1 --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs @@ -0,0 +1,133 @@ +/* + * Copyright © 2020-2024 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 System; + using System.Collections.Generic; + using System.Security.Cryptography.X509Certificates; + using AuthToken; + using NUnit.Framework; + using Exceptions; + using WebEid.Security.Validator; + using WebEid.Security.Validator.VersionValidators; + using WebEid.Security.Validator.Ocsp; + using Microsoft.Extensions.Logging; + using Moq; + using Security.Validator.CertValidators; + using Security.Validator.Ocsp.Service; + + public class AuthTokenVersion11ValidatorTest + { + private SubjectCertificateValidatorBatch subjectValidators; + private AuthTokenSignatureValidator signatureValidator; + private AuthTokenValidationConfiguration configuration; + private IOcspClient ocspClient; + private OcspServiceProvider ocspServiceProvider; + private AuthTokenVersion11Validator validator; + + [SetUp] + public void SetUp() + { + subjectValidators = SubjectCertificateValidatorBatch.CreateFrom(); + + configuration = new AuthTokenValidationConfiguration + { + SiteOrigin = new Uri("https://example.com") + }; + configuration.TrustedCaCertificates.Add(new X509Certificate2()); + + signatureValidator = new Mock( + new Uri("https://ria.ee") + ).Object; + + ocspClient = new Mock().Object; + + var aiaConfig = new AiaOcspServiceConfiguration( + new List(), + new List() + ); + + ocspServiceProvider = new OcspServiceProvider( + designatedOcspServiceConfiguration: null, + aiaOcspServiceConfiguration: aiaConfig + ); + + validator = new AuthTokenVersion11Validator( + subjectValidators, + signatureValidator, + configuration, + ocspClient, + ocspServiceProvider, + Mock.Of() + ); + } + + [TestCase("web-eid:1.1")] + [TestCase("web-eid:1.1.0")] + [TestCase("web-eid:1.10")] + public void WhenFormatIsV11OrPrefixedVariantThenSupportsReturnsTrue(string format) + { + Assert.That(validator.Supports(format), Is.True); + } + + [TestCase(null)] + [TestCase("")] + [TestCase("web-eid:1")] + [TestCase("web-eid:1.0")] + [TestCase("web-eid:2")] + [TestCase("webauthn:1.1")] + public void WhenFormatIsNullEmptyOrNotV11ThenSupportsReturnsFalse(string format) + { + Assert.That(validator.Supports(format), Is.False); + } + + [Test] + public void WhenUnverifiedSigningCertificateMissingThenValidationFails() + { + var token = new WebEidAuthToken + { + Format = "web-eid:1.1", + UnverifiedSigningCertificate = null, + SupportedSignatureAlgorithms = new List() + }; + + Assert.ThrowsAsync(() => + validator.Validate(token, "nonce")); + } + + [Test] + public void WhenSupportedSignatureAlgorithmsMissingThenValidationFails() + { + var token = new WebEidAuthToken + { + Format = "web-eid:1.1", + UnverifiedSigningCertificate = "ABC123", + SupportedSignatureAlgorithms = null + }; + + Assert.ThrowsAsync(() => + validator.Validate(token, "nonce")); + } + } +} + + diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs new file mode 100644 index 0000000..d05d970 --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs @@ -0,0 +1,117 @@ +/* + * Copyright © 2020-2024 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 System; + using System.Collections.Generic; + using System.Security.Cryptography.X509Certificates; + using AuthToken; + using NUnit.Framework; + using Exceptions; + using WebEid.Security.Validator; + using WebEid.Security.Validator.VersionValidators; + using WebEid.Security.Validator.CertValidators; + using WebEid.Security.Validator.Ocsp; + using WebEid.Security.Validator.Ocsp.Service; + using Microsoft.Extensions.Logging; + using Moq; + + public class AuthTokenVersion1ValidatorTest + { + private SubjectCertificateValidatorBatch subjectValidators; + private AuthTokenSignatureValidator signatureValidator; + private AuthTokenValidationConfiguration configuration; + private IOcspClient ocspClient; + private OcspServiceProvider ocspServiceProvider; + private AuthTokenVersion1Validator validator; + + [SetUp] + public void SetUp() + { + subjectValidators = SubjectCertificateValidatorBatch.CreateFrom(); + + configuration = new AuthTokenValidationConfiguration + { + SiteOrigin = new Uri("https://example.com") + }; + configuration.TrustedCaCertificates.Add(new X509Certificate2()); + + signatureValidator = new Mock( + new Uri("https://ria.ee") + ).Object; + + ocspClient = new Mock().Object; + + var aiaConfig = new AiaOcspServiceConfiguration( + new List(), + new List() + ); + + ocspServiceProvider = new OcspServiceProvider( + designatedOcspServiceConfiguration: null, + aiaOcspServiceConfiguration: aiaConfig + ); + + validator = new AuthTokenVersion1Validator( + subjectValidators, + signatureValidator, + configuration, + ocspClient, + ocspServiceProvider, + Mock.Of() + ); + } + + [TestCase("web-eid:1")] + [TestCase("web-eid:1.0")] + [TestCase("web-eid:1.1")] + [TestCase("web-eid:1.10")] + public void WhenFormatIsAnyMajorV1VariantThenSupportsReturnsTrue(string format) + { + Assert.That(validator.Supports(format), Is.True); + } + + [TestCase(null)] + [TestCase("")] + [TestCase("web-eid")] + [TestCase("web-eid:0.9")] + [TestCase("web-eid:2")] + [TestCase("webauthn:1")] + public void WhenFormatIsNullEmptyOrNotV1ThenSupportsReturnsFalse(string format) + { + Assert.That(validator.Supports(format), Is.False); + } + + [Test] + public void WhenUnverifiedCertificateMissingThenValidationFails() + { + var token = new WebEidAuthToken + { + Format = "web-eid:1", + UnverifiedCertificate = null + }; + + Assert.ThrowsAsync(() => + validator.Validate(token, "nonce")); + } + } +} diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs new file mode 100644 index 0000000..6130767 --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs @@ -0,0 +1,108 @@ +/* + * Copyright © 2020-2024 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 System.Collections.Generic; + using NUnit.Framework; + using Moq; + using Exceptions; + using WebEid.Security.Validator.VersionValidators; + + public class AuthTokenVersionValidatorFactoryTest + { + [Test] + public void WhenValidatorSupportsFormatThenSupportsReturnsTrue() + { + var v11 = new Mock(); + v11.Setup(v => v.Supports("web-eid:1.1")).Returns(true); + + var factory = new AuthTokenVersionValidatorFactory( + new List { v11.Object } + ); + + Assert.That(factory.Supports("web-eid:1.1"), Is.True); + } + + [Test] + public void WhenValidatorDoesNotSupportFormatThenSupportsReturnsFalse() + { + var v11 = new Mock(); + v11.Setup(v => v.Supports("web-eid:1.1")).Returns(false); + + var factory = new AuthTokenVersionValidatorFactory( + new List { v11.Object } + ); + + Assert.That(factory.Supports("web-eid:2"), Is.False); + } + + [TestCase("web-eid:0.9")] + [TestCase("web-eid:2")] + [TestCase("foo")] + [TestCase("1")] + [TestCase("web-eid")] + public void WhenUnsupportedFormatThenGetValidatorForThrows(string format) + { + var factory = new AuthTokenVersionValidatorFactory(new List()); + + Assert.Throws(() => + factory.GetValidatorFor(format) + ); + } + + [Test] + public void WhenMultipleValidatorsAndFirstIsV11ThenGetValidatorForReturnsV11() + { + var v11 = new Mock(); + v11.Setup(v => v.Supports("web-eid:1.1")).Returns(true); + + var v1 = new Mock(); + v1.Setup(v => v.Supports("web-eid:1")).Returns(true); + + var factory = new AuthTokenVersionValidatorFactory( + new List { v11.Object, v1.Object } + ); + + var chosen = factory.GetValidatorFor("web-eid:1.1"); + + Assert.That(chosen, Is.SameAs(v11.Object)); + } + + [Test] + public void WhenFormatIsBaseV1ThenGetValidatorForReturnsV1() + { + var v11 = new Mock(); + v11.Setup(v => v.Supports("web-eid:1.1")).Returns(true); + + var v1 = new Mock(); + v1.Setup(v => v.Supports("web-eid:1")).Returns(true); + + var factory = new AuthTokenVersionValidatorFactory( + new List { v11.Object, v1.Object } + ); + + var chosen = factory.GetValidatorFor("web-eid:1"); + + Assert.That(chosen, Is.SameAs(v1.Object)); + } + } +} diff --git a/src/WebEid.Security/AuthToken/SupportedSignatureAlgorithm.cs b/src/WebEid.Security/AuthToken/SupportedSignatureAlgorithm.cs new file mode 100644 index 0000000..e799bcf --- /dev/null +++ b/src/WebEid.Security/AuthToken/SupportedSignatureAlgorithm.cs @@ -0,0 +1,46 @@ +/* + * Copyright © 2020-2024 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.AuthToken +{ + using Newtonsoft.Json; + + /// + /// Represents a signature algorithm supported by the authentication token, + /// including its cryptographic algorithm, hash function, and padding scheme. + /// + [JsonObject] + public class SupportedSignatureAlgorithm + { + /// + /// The cryptographic algorithm, e.g. "RSA" or "ECDSA". + /// + public string CryptoAlgorithm { get; set; } + /// + /// The hash function, e.g. "SHA-256". + /// + public string HashFunction { get; set; } + /// + /// The padding scheme, e.g. "PKCS1" or "PSS". + /// + public string PaddingScheme { get; set; } + } +} diff --git a/src/WebEid.Security/AuthToken/WebEidAuthToken.cs b/src/WebEid.Security/AuthToken/WebEidAuthToken.cs index ecae20b..9424dff 100644 --- a/src/WebEid.Security/AuthToken/WebEidAuthToken.cs +++ b/src/WebEid.Security/AuthToken/WebEidAuthToken.cs @@ -21,6 +21,8 @@ */ namespace WebEid.Security.AuthToken { + using System.Collections.Generic; + /// /// The Web eID authentication token /// @@ -44,5 +46,13 @@ public class WebEidAuthToken /// The base64-encoded DER-encoded authentication certificate of the eID user. /// public string UnverifiedCertificate { get; set; } + /// + /// The base64-encoded signing certificate (DER). + /// + public string UnverifiedSigningCertificate { get; set; } + /// + /// List of supported signature algorithms from the card. + /// + public List SupportedSignatureAlgorithms { get; set; } } } diff --git a/src/WebEid.Security/Util/X509Certificate2Extensions.cs b/src/WebEid.Security/Util/X509Certificate2Extensions.cs new file mode 100644 index 0000000..27142a8 --- /dev/null +++ b/src/WebEid.Security/Util/X509Certificate2Extensions.cs @@ -0,0 +1,41 @@ +/* + * Copyright © 2020-2024 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.Util +{ + using System.Security.Cryptography.X509Certificates; + + /// + /// Provides extension methods for working with instances. + /// + public static class X509Certificate2Extensions + { + /// + /// Converts a into an equivalent + /// BouncyCastle instance. + /// + public static Org.BouncyCastle.X509.X509Certificate ToBouncyCastle(this X509Certificate2 certificate) + { + var parser = new Org.BouncyCastle.X509.X509CertificateParser(); + return parser.ReadCertificate(certificate.RawData); + } + } +} diff --git a/src/WebEid.Security/Validator/AuthTokenValidator.cs b/src/WebEid.Security/Validator/AuthTokenValidator.cs index ca7aa80..40b4b5d 100644 --- a/src/WebEid.Security/Validator/AuthTokenValidator.cs +++ b/src/WebEid.Security/Validator/AuthTokenValidator.cs @@ -22,19 +22,13 @@ namespace WebEid.Security.Validator { using System; - using System.Linq; - using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Exceptions; using Microsoft.Extensions.Logging; - using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; - using Ocsp; - using Ocsp.Service; using AuthToken; - using Util; - using WebEid.Security.Validator.CertValidators; + using VersionValidators; /// /// Represents the configuration for validating authentication tokens (JWTs) in the Web eID system. @@ -42,12 +36,9 @@ namespace WebEid.Security.Validator public sealed class AuthTokenValidator : IAuthTokenValidator { private readonly ILogger logger; - private readonly AuthTokenValidationConfiguration configuration; - private readonly AuthTokenSignatureValidator authTokenSignatureValidator; - private readonly SubjectCertificateValidatorBatch simpleSubjectCertificateValidators; - private readonly OcspClient ocspClient; - private readonly OcspServiceProvider ocspServiceProvider; + private readonly AuthTokenVersionValidatorFactory versionValidatorFactory; + // Use human-readable meaningful names for token length limits. private const int TokenMinLength = 100; private const int TokenMaxLength = 10000; @@ -59,27 +50,10 @@ public sealed class AuthTokenValidator : IAuthTokenValidator public AuthTokenValidator(AuthTokenValidationConfiguration configuration, ILogger logger = null) { this.logger = logger; - this.configuration = configuration.Copy(); - this.authTokenSignatureValidator = new AuthTokenSignatureValidator(configuration.SiteOrigin); + var validationConfig = configuration.Copy(); - this.simpleSubjectCertificateValidators = SubjectCertificateValidatorBatch.CreateFrom( - new SubjectCertificatePurposeValidator(this.logger), - new SubjectCertificatePolicyValidator(configuration.DisallowedSubjectCertificatePolicies - .Select(policy => new Oid(policy)) - .ToArray(), this.logger) - ); - - if (configuration.IsUserCertificateRevocationCheckWithOcspEnabled) - { - this.ocspClient = new OcspClient(TimeSpan.FromSeconds(5), this.logger); - this.ocspServiceProvider = - new OcspServiceProvider(configuration.DesignatedOcspServiceConfiguration, - new AiaOcspServiceConfiguration(configuration.NonceDisabledOcspUrls, - configuration.TrustedCaCertificates)); - } - - // Turn off caching of signature providers - CryptoProviderFactory.DefaultCacheSignatureProviders = false; + versionValidatorFactory = + AuthTokenVersionValidatorFactory.Create(validationConfig, null, this.logger); } /// @@ -89,7 +63,7 @@ public AuthTokenValidator(AuthTokenValidationConfiguration configuration, ILogge /// A parsed instance. public WebEidAuthToken Parse(string authToken) { - this.logger?.LogInformation("Starting token parsing"); + logger?.LogInformation("Starting token parsing"); try { @@ -99,7 +73,7 @@ public WebEidAuthToken Parse(string authToken) catch (Exception ex) { // Generally "log and rethrow" is an anti-pattern, but it fits with the surrounding logging style. - this.logger?.LogWarning("Token parsing was interrupted:", ex); + logger?.LogWarning(ex, "Token parsing was interrupted"); throw; } } @@ -112,16 +86,16 @@ public WebEidAuthToken Parse(string authToken) /// A task representing the validation result with the user certificate. public Task Validate(WebEidAuthToken authToken, string currentChallengeNonce) { - this.logger?.LogInformation("Starting token validation"); + logger?.LogInformation("Starting token validation"); try { - return this.ValidateToken(authToken, currentChallengeNonce); + var validator = versionValidatorFactory.GetValidatorFor(authToken.Format); + return validator.Validate(authToken, currentChallengeNonce); } catch (Exception ex) { - // Generally "log and rethrow" is an anti-pattern, but it fits with the surrounding logging style. - this.logger?.LogWarning("Token validation was interrupted:", ex); + logger?.LogWarning(ex, "Token validation was interrupted"); throw; } } @@ -142,77 +116,13 @@ private static WebEidAuthToken ParseToken(string authToken) { try { - var token = JsonConvert.DeserializeObject(authToken); - if (token == null) - { - throw new AuthTokenParseException("Web eID authentication token deserialization failed"); - } - return token; + return JsonConvert.DeserializeObject(authToken) + ?? throw new AuthTokenParseException("Web eID authentication token is null"); } catch (JsonException ex) { throw new AuthTokenParseException("Error parsing Web eID authentication token", ex); } } - - private async Task ValidateToken(WebEidAuthToken token, string currentChallengeNonce) - { - if (token.Format == null || !token.Format.StartsWith(IAuthTokenValidator.CURRENT_TOKEN_FORMAT_VERSION)) - { - throw new AuthTokenParseException($"Only token format version '{IAuthTokenValidator.CURRENT_TOKEN_FORMAT_VERSION}' is currently supported"); - } - if (string.IsNullOrEmpty(token.UnverifiedCertificate)) - { - throw new AuthTokenParseException("'unverifiedCertificate' field is missing, null or empty"); - } - - var subjectCertificate = X509CertificateExtensions.ParseCertificate(token.UnverifiedCertificate, "unverifiedCertificate"); - - await this.simpleSubjectCertificateValidators.ExecuteFor(subjectCertificate); - await this.GetCertTrustValidators().ExecuteFor(subjectCertificate); - - try - { - var publicKey = subjectCertificate - .GetAsymmetricPublicKey() - .CreateSecurityKeyWithoutCachingSignatureProviders(); - - // It is guaranteed that if the signature verification succeeds, then the origin and challenge - // have been implicitly and correctly verified without the need to implement any additional checks. - this.authTokenSignatureValidator.Validate(token.Algorithm, - publicKey, - token.Signature, - currentChallengeNonce); - - return subjectCertificate; - - } - catch (Exception ex) when (!(ex is AuthTokenException)) - { - throw new AuthTokenSignatureValidationException(ex); - } - } - - /// - /// Creates the certificate trust validator batch. - /// As SubjectCertificateTrustedValidator has mutable state that SubjectCertificateNotRevokedValidator depends on, - /// they cannot be reused/cached in an instance variable in a multi-threaded environment. Hence they are - /// re-created for each validation run for thread safety. - /// - /// certificate trust validator batch - private SubjectCertificateValidatorBatch GetCertTrustValidators() - { - var certTrustedValidator = - new SubjectCertificateTrustedValidator(this.configuration.TrustedCaCertificates, this.logger); - - return SubjectCertificateValidatorBatch.CreateFrom(certTrustedValidator) - .AddOptional(this.configuration.IsUserCertificateRevocationCheckWithOcspEnabled, - new SubjectCertificateNotRevokedValidator(certTrustedValidator, - this.ocspClient, - this.ocspServiceProvider, - this.configuration.AllowedOcspResponseTimeSkew, - this.configuration.MaxOcspResponseThisUpdateAge, - this.logger)); - } } } diff --git a/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs b/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs index cc955bb..92917b9 100644 --- a/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs @@ -28,7 +28,7 @@ namespace WebEid.Security.Validator.CertValidators /// Validators perform the actual user certificate validation actions. /// They are used by AuthTokenValidator internally and are not part of the public API. /// - internal interface ISubjectCertificateValidator + public interface ISubjectCertificateValidator { Task Validate(X509Certificate2 subjectCertificate); } diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs index 334f4e8..0a4a940 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs @@ -35,7 +35,7 @@ namespace WebEid.Security.Validator.CertValidators using Org.BouncyCastle.Security; using WebEid.Security.Util; - internal sealed class SubjectCertificateNotRevokedValidator : ISubjectCertificateValidator + public sealed class SubjectCertificateNotRevokedValidator : ISubjectCertificateValidator { private readonly SubjectCertificateTrustedValidator trustValidator; private readonly IOcspClient ocspClient; diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs index 04ad923..2643c77 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs @@ -32,7 +32,7 @@ namespace WebEid.Security.Validator.CertValidators using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Security; - internal sealed class SubjectCertificatePolicyValidator : ISubjectCertificateValidator + public sealed class SubjectCertificatePolicyValidator : ISubjectCertificateValidator { private readonly ICollection disallowedSubjectCertificatePolicies; private readonly ILogger logger; diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs index 90e20ff..a109a66 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs @@ -28,7 +28,7 @@ namespace WebEid.Security.Validator.CertValidators using Util; using WebEid.Security.Exceptions; - internal sealed class SubjectCertificateTrustedValidator : ISubjectCertificateValidator + public sealed class SubjectCertificateTrustedValidator : ISubjectCertificateValidator { private readonly ICollection trustedCaCertificates; private readonly ILogger logger; diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs index 0859a8e..77b2839 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs @@ -25,7 +25,7 @@ namespace WebEid.Security.Validator.CertValidators using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; - internal sealed class SubjectCertificateValidatorBatch + public sealed class SubjectCertificateValidatorBatch { private readonly List validatorList; diff --git a/src/WebEid.Security/Validator/IAuthTokenValidator.cs b/src/WebEid.Security/Validator/IAuthTokenValidator.cs index 29e1009..2530b49 100644 --- a/src/WebEid.Security/Validator/IAuthTokenValidator.cs +++ b/src/WebEid.Security/Validator/IAuthTokenValidator.cs @@ -25,18 +25,13 @@ namespace WebEid.Security.Validator using System.Threading.Tasks; using Exceptions; using Util; - using WebEid.Security.AuthToken; + using AuthToken; /// /// Interface for validating Web eID authentication tokens. /// public interface IAuthTokenValidator { - /// - /// The current token format version - /// - const string CURRENT_TOKEN_FORMAT_VERSION = "web-eid:1"; - /// /// Parses the Web eID authentication token signed by the subject. /// diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs new file mode 100644 index 0000000..e9501e3 --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs @@ -0,0 +1,236 @@ +/* + * Copyright © 2020-2024 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; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography.X509Certificates; + using System.Threading.Tasks; + using CertValidators; + using Microsoft.Extensions.Logging; + using Ocsp; + using AuthToken; + using Exceptions; + using Org.BouncyCastle.Asn1; + using Org.BouncyCastle.Asn1.X509; + using Util; + + /// + /// Validator for token format web-eid:1.1. + /// Extends V1 validator with additional checks for signing certificate + supported algorithms. + /// + public sealed class AuthTokenVersion11Validator : AuthTokenVersion1Validator + { + private const string FormatPrefix = "web-eid:1.1"; + + private static readonly HashSet SupportedCryptoAlgorithms = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "ECC", + "RSA" + }; + + private static readonly HashSet SupportedPaddingSchemes = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "NONE", + "PKCS1.5", + "PSS" + }; + + private static readonly HashSet SupportedHashFunctions = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "SHA-224", + "SHA-256", + "SHA-384", + "SHA-512", + "SHA3-224", + "SHA3-256", + "SHA3-384", + "SHA3-512" + }; + + /// + /// Initializes a validator for Web eID authentication tokens in format web-eid:1.1. + /// + public AuthTokenVersion11Validator( + SubjectCertificateValidatorBatch simpleSubjectCertificateValidators, + AuthTokenSignatureValidator signatureValidator, + AuthTokenValidationConfiguration configuration, + IOcspClient ocspClient, + OcspServiceProvider ocspServiceProvider, + ILogger logger = null) + : base(simpleSubjectCertificateValidators, signatureValidator, configuration, + ocspClient, ocspServiceProvider, logger) + { + } + + /// + /// Determines whether this validator supports the specified token format. + /// + public override bool Supports(string format) => + format != null && + format.StartsWith(FormatPrefix, StringComparison.OrdinalIgnoreCase); + + /// + /// Validates a Web eID authentication token in format web-eid:1.1 + /// and returns the authenticated user's certificate. + /// + public override async Task Validate(WebEidAuthToken authToken, string currentChallengeNonce) + { + var subjectCertificate = await base.Validate(authToken, currentChallengeNonce); + var signingCertificate = ValidateSigningCertificateExists(authToken); + ValidateSupportedSignatureAlgorithms(authToken.SupportedSignatureAlgorithms); + ValidateSameSubject(subjectCertificate, signingCertificate); + ValidateSameIssuer(subjectCertificate, signingCertificate); + ValidateSigningCertificateValidity(signingCertificate); + ValidateSigningCertificateKeyUsage(signingCertificate); + + return subjectCertificate; + } + + private static X509Certificate2 ValidateSigningCertificateExists(WebEidAuthToken token) + { + if (string.IsNullOrEmpty(token.UnverifiedSigningCertificate)) + { + throw new AuthTokenParseException( + "'unverifiedSigningCertificate' field is missing, null or empty for format 'web-eid:1.1'"); + } + + try + { + return X509CertificateExtensions.ParseCertificate( + token.UnverifiedSigningCertificate, + "unverifiedSigningCertificate"); + } + catch (Exception ex) + { + throw new AuthTokenParseException("Failed to decode signing certificate", ex); + } + } + + private static void ValidateSupportedSignatureAlgorithms( + IList algorithms) + { + if (algorithms == null || algorithms.Count == 0) + { + throw new AuthTokenParseException("'supportedSignatureAlgorithms' field is missing"); + } + + bool hasInvalid = + algorithms.Any(a => + !SupportedCryptoAlgorithms.Contains(a.CryptoAlgorithm) || + !SupportedHashFunctions.Contains(a.HashFunction) || + !SupportedPaddingSchemes.Contains(a.PaddingScheme)); + + if (hasInvalid) + { + throw new AuthTokenParseException("Unsupported signature algorithm"); + } + } + + private static void ValidateSameSubject(X509Certificate2 subjectCert, X509Certificate2 signingCert) + { + if (!SubjectsMatch(subjectCert, signingCert)) + { + throw new AuthTokenParseException( + "Signing certificate subject does not match authentication certificate subject"); + } + } + + private static void ValidateSameIssuer(X509Certificate2 subjectCert, X509Certificate2 signingCert) + { + var subjectAki = GetAuthorityKeyIdentifier(subjectCert); + var signingAki = GetAuthorityKeyIdentifier(signingCert); + + if (subjectAki.Length == 0 || + signingAki.Length == 0 || + !subjectAki.SequenceEqual(signingAki)) + { + throw new AuthTokenParseException( + "Signing certificate is not issued by the same issuing authority as the authentication certificate"); + } + } + + private static void ValidateSigningCertificateValidity(X509Certificate2 cert) + { + try + { + var bcCert = cert.ToBouncyCastle(); + bcCert.ValidateCertificateExpiry(DateTime.UtcNow, "signing_certificate"); + } + catch (Exception ex) + { + throw new AuthTokenParseException( + "Signing certificate is not valid: " + ex.Message, ex); + } + } + + private static void ValidateSigningCertificateKeyUsage(X509Certificate2 cert) + { + var keyUsage = cert.Extensions.OfType().FirstOrDefault(); + if (keyUsage == null || + !keyUsage.KeyUsages.HasFlag(X509KeyUsageFlags.NonRepudiation)) + { + throw new AuthTokenParseException( + "Signing certificate key usage extension missing or does not contain non-repudiation bit required for digital signatures"); + } + } + + private static bool SubjectsMatch(X509Certificate2 subjectCert, X509Certificate2 signingCert) + { + byte[] authRaw = subjectCert.SubjectName.RawData; + byte[] signRaw = signingCert.SubjectName.RawData; + + var authAsn1 = Asn1Object.FromByteArray(authRaw); + var signAsn1 = Asn1Object.FromByteArray(signRaw); + + var authName = X509Name.GetInstance(authAsn1); + var signName = X509Name.GetInstance(signAsn1); + + return authName.Equivalent(signName); + } + + private static byte[] GetAuthorityKeyIdentifier(X509Certificate2 cert) + { + try + { + var akiExt = cert.Extensions["2.5.29.35"]; + if (akiExt == null) + { + return Array.Empty(); + } + + var akiObj = Asn1Object.FromByteArray(akiExt.RawData); + var aki = AuthorityKeyIdentifier.GetInstance(akiObj); + + return aki.GetKeyIdentifier() ?? Array.Empty(); + } + catch (Exception ex) + { + throw new AuthTokenParseException("Failed to parse Authority Key Identifier", ex); + } + } + } +} diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs new file mode 100644 index 0000000..275a706 --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs @@ -0,0 +1,127 @@ +/* + * Copyright © 2020-2024 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; + using System.Security.Cryptography.X509Certificates; + using System.Threading.Tasks; + using CertValidators; + using AuthToken; + using Exceptions; + using Util; + using Ocsp; + using Microsoft.Extensions.Logging; + + /// + /// Validator for token format web-eid:1.0. + /// + public class AuthTokenVersion1Validator : IAuthTokenVersionValidator + { + private const string FormatPrefix = "web-eid:1"; + + private readonly SubjectCertificateValidatorBatch simpleSubjectCertificateValidators; + private readonly AuthTokenSignatureValidator signatureValidator; + private readonly AuthTokenValidationConfiguration configuration; + private readonly IOcspClient ocspClient; + private readonly OcspServiceProvider ocspServiceProvider; + private readonly ILogger logger; + + /// + /// Initializes a validator for Web eID authentication tokens in format web-eid:1.0. + /// + public AuthTokenVersion1Validator( + SubjectCertificateValidatorBatch simpleSubjectCertificateValidators, + AuthTokenSignatureValidator signatureValidator, + AuthTokenValidationConfiguration configuration, + IOcspClient ocspClient, + OcspServiceProvider ocspServiceProvider, + ILogger logger = null) + { + this.simpleSubjectCertificateValidators = simpleSubjectCertificateValidators; + this.signatureValidator = signatureValidator; + this.configuration = configuration; + this.ocspClient = ocspClient; + this.ocspServiceProvider = ocspServiceProvider; + this.logger = logger; + } + + /// + /// Determines whether this validator supports the specified token format. + /// + public virtual bool Supports(string format) => + format != null && + format.StartsWith(FormatPrefix, StringComparison.OrdinalIgnoreCase); + + /// + /// Validates a Web eID authentication token and returns the authenticated user's certificate. + /// + public virtual async Task Validate(WebEidAuthToken authToken, string currentChallengeNonce) + { + if (string.IsNullOrEmpty(authToken.UnverifiedCertificate)) + { + throw new AuthTokenParseException("'unverifiedCertificate' field is missing, null or empty"); + } + + var subjectCertificate = X509CertificateExtensions.ParseCertificate( + authToken.UnverifiedCertificate, + "unverifiedCertificate" + ); + + await simpleSubjectCertificateValidators.ExecuteFor(subjectCertificate); + + var trustValidators = + SubjectCertificateValidatorBatch.CreateFrom( + new SubjectCertificateTrustedValidator(configuration.TrustedCaCertificates, logger) + ).AddOptional( + configuration.IsUserCertificateRevocationCheckWithOcspEnabled && + ocspClient != null && + ocspServiceProvider != null, + new SubjectCertificateNotRevokedValidator( + new SubjectCertificateTrustedValidator(configuration.TrustedCaCertificates, logger), + ocspClient, + ocspServiceProvider, + configuration.AllowedOcspResponseTimeSkew, + configuration.MaxOcspResponseThisUpdateAge, + logger + ) + ); + + await trustValidators.ExecuteFor(subjectCertificate); + + try + { + signatureValidator.Validate( + authToken.Algorithm, + subjectCertificate.GetAsymmetricPublicKey().CreateSecurityKeyWithoutCachingSignatureProviders(), + authToken.Signature, + currentChallengeNonce + ); + } + catch (Exception ex) when (!(ex is AuthTokenException)) + { + throw new AuthTokenSignatureValidationException(ex); + } + + return subjectCertificate; + } + } +} diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs new file mode 100644 index 0000000..97dc6b7 --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs @@ -0,0 +1,137 @@ +/* + * Copyright © 2020-2024 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; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using CertValidators; + using Exceptions; + using Microsoft.Extensions.Logging; + using Ocsp; + using Ocsp.Service; + + /// + /// Provides a factory for selecting the correct authentication token validator + /// based on the token format version. + /// + public sealed class AuthTokenVersionValidatorFactory + { + private readonly IReadOnlyList validators; + + /// + /// Creates a new instance of the class. + /// + public AuthTokenVersionValidatorFactory(IList validators) => + this.validators = validators?.ToList() + ?? throw new ArgumentNullException(nameof(validators)); + + /// + /// Determines whether any registered validator supports the specified token format. + /// + public bool Supports(string format) => + validators.Any(v => v.Supports(format)); + + /// + /// Returns the validator that supports the given token format. + /// + public IAuthTokenVersionValidator GetValidatorFor(string format) + { + var validator = validators.FirstOrDefault(v => v.Supports(format)); + + if (validator == null) + { + throw new AuthTokenParseException( + $"Token format version '{format}' is currently not supported"); + } + + return validator; + } + + /// + /// Creates an instance configured + /// with version-specific Web eID authentication token validators. + /// + public static AuthTokenVersionValidatorFactory Create( + AuthTokenValidationConfiguration configuration, + IOcspClient ocspClient, + ILogger logger = null) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + var validationConfig = configuration.Copy(); + var trustedCaCertificates = validationConfig.TrustedCaCertificates; + var trustedCACertificate = new X509Certificate2Collection(); + trustedCACertificate.AddRange(trustedCaCertificates.ToArray()); + + var simpleSubjectValidators = SubjectCertificateValidatorBatch.CreateFrom( + new SubjectCertificatePurposeValidator(logger), + new SubjectCertificatePolicyValidator( + validationConfig.DisallowedSubjectCertificatePolicies.Select(x => new Oid(x)).ToArray(), + logger) + ); + + OcspServiceProvider ocspServiceProvider = null; + + if (validationConfig.IsUserCertificateRevocationCheckWithOcspEnabled && ocspClient != null) + { + ocspServiceProvider = new OcspServiceProvider( + validationConfig.DesignatedOcspServiceConfiguration, + new AiaOcspServiceConfiguration( + validationConfig.NonceDisabledOcspUrls, + trustedCaCertificates + ) + ); + } + + var signatureValidator = new AuthTokenSignatureValidator(validationConfig.SiteOrigin); + var validatorV10 = new AuthTokenVersion1Validator( + simpleSubjectValidators, + signatureValidator, + validationConfig, + ocspClient, + ocspServiceProvider, + logger + ); + + var validatorV11 = new AuthTokenVersion11Validator( + simpleSubjectValidators, + signatureValidator, + validationConfig, + ocspClient, + ocspServiceProvider, + logger + ); + + return new AuthTokenVersionValidatorFactory(new List + { + validatorV11, + validatorV10 + }); + } + } +} diff --git a/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs b/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs new file mode 100644 index 0000000..25921b2 --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs @@ -0,0 +1,50 @@ +/* + * Copyright © 2020-2024 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.Security.Cryptography.X509Certificates; + using System.Threading.Tasks; + using AuthToken; + using Exceptions; + + /// + /// Version-specific Web eID authentication token validator. + /// + public interface IAuthTokenVersionValidator + { + /// + /// Whether this validator supports the specified token format, + /// e.g. "web-eid:1.0" or "web-eid:1.1". + /// + bool Supports(string format); + + /// + /// Validates the Web eID authentication token according to the + /// version-specific rules and returns the authenticated user's certificate. + /// + /// Parsed Web eID authentication token. + /// Server-issued challenge nonce. + /// Validated authentication certificate. + /// If validation fails. + Task Validate(WebEidAuthToken authToken, string currentChallengeNonce); + } +} From b011dcb601cd3dcfd4f9f326ec653b57607c68a0 Mon Sep 17 00:00:00 2001 From: "Sander.Kondratjev" Date: Mon, 24 Nov 2025 20:37:16 +0200 Subject: [PATCH 2/4] NFC-99 Web eID for Mobile authentication support for web-eid example Signed-off-by: Sander Kondratjev --- .../Api/MobileAuthInitController.cs | 98 +++ .../Api/MobileAuthLoginController.cs | 80 +++ .../Dto/AuthenticateRequestDto.cs | 10 +- .../Options/WebEidMobileOptions.cs | 32 + .../Pages/Index.cshtml | 594 +++++++++++++++--- .../Pages/WebEidLogin.cshtml | 72 +++ .../Pages/WebEidLogin.cshtml.cs | 31 + .../src/WebEid.AspNetCore.Example/Startup.cs | 14 +- .../appsettings.Development.json | 6 +- .../appsettings.json | 6 +- .../wwwroot/css/bootstrap.min.css | 13 +- .../wwwroot/css/main.css | 71 +++ .../wwwroot/favicon.ico | Bin 5430 -> 0 bytes .../wwwroot/img/android-chrome-192x192.png | Bin 0 -> 11514 bytes .../wwwroot/img/android-chrome-512x512.png | Bin 0 -> 46458 bytes .../wwwroot/img/apple-touch-icon.png | Bin 0 -> 10297 bytes .../wwwroot/img/favicon-16x16.png | Bin 0 -> 651 bytes .../wwwroot/img/favicon-32x32.png | Bin 0 -> 1289 bytes .../wwwroot/img/favicon.ico | Bin 0 -> 15406 bytes .../wwwroot/js/bootstrap.bundle.min.js | 7 + 20 files changed, 925 insertions(+), 109 deletions(-) create mode 100644 example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthLoginController.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml create mode 100644 example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs delete mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/favicon.ico create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/android-chrome-192x192.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/android-chrome-512x512.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/apple-touch-icon.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon-16x16.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon-32x32.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon.ico create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/js/bootstrap.bundle.min.js diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs new file mode 100644 index 0000000..fa5a756 --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs @@ -0,0 +1,98 @@ +// Copyright (c) 2021-2024 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.AspNetCore.Example.Controllers.Api +{ + using System; + using System.Text; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Options; + using System.Text.Json; + using System.Text.Json.Serialization; + using Options; + using Security.Challenge; + + [ApiController] + [Route("auth/mobile")] + public class MobileAuthInitController( + IChallengeNonceGenerator nonceGenerator, + IOptions mobileOptions + ) : ControllerBase + { + private const string WebEidMobileAuthPath = "auth"; + private const string MobileLoginPath = "/auth/mobile/login"; + + [HttpPost("init")] + public IActionResult Init() + { + var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5)); + var challengeBase64 = challenge.Base64EncodedNonce; + + var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}"; + + var payload = new AuthPayload + { + Challenge = challengeBase64, + LoginUri = loginUri, + GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null + }; + + var json = JsonSerializer.Serialize(payload); + var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + + var authUri = BuildAuthUri(encodedPayload); + + return Ok(new AuthUri + { + AuthUriValue = authUri + }); + } + + private string BuildAuthUri(string encodedPayload) + { + var baseUri = mobileOptions.Value.BaseRequestUri; + + return baseUri.StartsWith("http", StringComparison.OrdinalIgnoreCase) + ? $"{baseUri.TrimEnd('/')}/{WebEidMobileAuthPath}#{encodedPayload}" + : $"{baseUri}{WebEidMobileAuthPath}#{encodedPayload}"; + } + + private sealed record AuthPayload + { + [JsonInclude] + [JsonPropertyName("challenge")] + public required string Challenge { get; init; } + + [JsonInclude] + [JsonPropertyName("login_uri")] + public required string LoginUri { get; init; } + + [JsonInclude] + [JsonPropertyName("get_signing_certificate")] + public bool? GetSigningCertificate { get; init; } + } + + private sealed record AuthUri + { + [JsonInclude] + [JsonPropertyName("auth_uri")] + public required string AuthUriValue { get; init; } + } + } +} diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthLoginController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthLoginController.cs new file mode 100644 index 0000000..e8f9609 --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthLoginController.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2021-2024 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.AspNetCore.Example.Controllers.Api +{ + using Microsoft.AspNetCore.Mvc; + using System.Text.Json; + using Dto; + using Security.Challenge; + using Security.Validator; + using System.Security.Claims; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Authentication; + using Microsoft.AspNetCore.Authentication.Cookies; + using Security.Util; + + [ApiController] + [Route("auth/mobile")] + public class MobileAuthLoginController( + IAuthTokenValidator authTokenValidator, + IChallengeNonceStore challengeNonceStore + ) : ControllerBase + { + [HttpPost("login")] + public async Task MobileLogin([FromBody] AuthenticateRequestDto dto) + { + if (dto?.AuthToken == null) + { + return BadRequest(new { error = "Missing auth_token" }); + } + + var parsedToken = dto.AuthToken; + var certificate = await authTokenValidator.Validate( + parsedToken, + challengeNonceStore.GetAndRemove().Base64EncodedNonce); + + var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); + + identity.AddClaim(new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName())); + identity.AddClaim(new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname())); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode())); + identity.AddClaim(new Claim(ClaimTypes.Name, certificate.GetSubjectCn())); + + if (!string.IsNullOrEmpty(parsedToken.UnverifiedSigningCertificate)) + { + identity.AddClaim(new Claim("signingCertificate", parsedToken.UnverifiedSigningCertificate)); + } + + if (parsedToken.SupportedSignatureAlgorithms != null) + { + identity.AddClaim(new Claim( + "supportedSignatureAlgorithms", + JsonSerializer.Serialize(parsedToken.SupportedSignatureAlgorithms))); + } + + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(identity), + new AuthenticationProperties { IsPersistent = false }); + + return Ok(new { redirect = "/welcome" }); + } + } +} \ No newline at end of file diff --git a/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs b/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs index c36726f..599f49e 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs @@ -24,7 +24,13 @@ public class AuthenticateRequestDto { - [JsonPropertyName("auth-token")] - public WebEidAuthToken AuthToken { get; set; } + // Mobile version uses "auth_token" + [JsonPropertyName("auth_token")] public WebEidAuthToken AuthTokenUnderscore { get; set; } + + // Desktop version uses "auth-token" + [JsonPropertyName("auth-token")] public WebEidAuthToken AuthTokenDash { get; set; } + + // Unified property for backend logic + [JsonIgnore] public WebEidAuthToken AuthToken => AuthTokenDash ?? AuthTokenUnderscore; } } diff --git a/example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs b/example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs new file mode 100644 index 0000000..a49ea1a --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2021-2024 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.AspNetCore.Example.Options +{ + using System.ComponentModel.DataAnnotations; + + public class WebEidMobileOptions + { + [Required] + [RegularExpression("^.*(?:[^/]|://)$", ErrorMessage = "Base URI must not have a trailing slash")] + public string BaseRequestUri { get; set; } = null!; + + public bool RequestSigningCert { get; set; } + } +} \ No newline at end of file diff --git a/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml b/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml index d25e828..a11aa76 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml +++ b/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml @@ -3,115 +3,517 @@ - - + + + + @{ + var tokens = Xsrf.GetAndStoreTokens(HttpContext); + } + + Web eID: electronic ID smart cards on the Web - - + + + + + + + + -
-
-
-

Web eID: electronic ID smart cards on the Web

-

- The Web eID project enables usage of European Union electronic identity (eID) smart cards for - secure authentication and digital signing of documents on the web using public-key cryptography. -

-

- Estonian, Finnish, Latvian, Lithuanian and Croatian eID cards are supported in the first phase, but only - Estonian eID card support is currently enabled in the test application below. -

-

- Please get in touch by email at help@ria.ee in case you need support with adding Web eID to your project - or want to add support for a new eID card to Web eID. -

- -
- -

- More information about the Web eID project, including installation and usage instructions - is available on the project [website](https://web-eid.eu/). -

-

Click Authenticate below to test authentication and digital signing.

- -