From e2f930587a8127d1a5dd3b935876d9ccb83b0cd6 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Thu, 13 Nov 2025 19:40:04 +0100 Subject: [PATCH] (feature): jwk and jws methods review --- README.md | 237 ++++++++++++++---- .../Security/JWK/JwkGeneratorTests.cs | 70 ++++++ .../Security/JWK/JwkThumbprintUtilityTests.cs | 63 +++++ .../Security/JwkGeneratorTests.cs | 107 -------- .../Security/JwsGeneratorTests.cs | 123 +++++---- src/MaksIT.Core/MaksIT.Core.csproj | 2 +- src/MaksIT.Core/Security/Base64UrlUtility.cs | 54 ++++ .../Security/JWK/Base64UrlUtility.cs | 63 ----- src/MaksIT.Core/Security/JWK/JwkGenerator.cs | 213 ++-------------- .../Security/JWK/JwkThumbprintUtility.cs | 56 +++++ src/MaksIT.Core/Security/JWK/OrderedJwk.cs | 18 ++ src/MaksIT.Core/Security/JWS/JwsGenerator.cs | 148 ++++------- 12 files changed, 580 insertions(+), 574 deletions(-) create mode 100644 src/MaksIT.Core.Tests/Security/JWK/JwkGeneratorTests.cs create mode 100644 src/MaksIT.Core.Tests/Security/JWK/JwkThumbprintUtilityTests.cs delete mode 100644 src/MaksIT.Core.Tests/Security/JwkGeneratorTests.cs create mode 100644 src/MaksIT.Core/Security/Base64UrlUtility.cs delete mode 100644 src/MaksIT.Core/Security/JWK/Base64UrlUtility.cs create mode 100644 src/MaksIT.Core/Security/JWK/JwkThumbprintUtility.cs create mode 100644 src/MaksIT.Core/Security/JWK/OrderedJwk.cs diff --git a/README.md b/README.md index ea39298..d4c631a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ - [Password Hasher](#password-hasher) - [JWT Generator](#jwt-generator) - [JWK Generator](#jwk-generator) + - [JWK Thumbprint Utility](#jwk-thumbprint-utility) - [JWS Generator](#jws-generator) - [TOTP Generator](#totp-generator) - [Web API Models](#web-api-models) @@ -968,85 +969,215 @@ JwtGenerator.TryGenerateToken(secret, issuer, audience, 60, "user", roles, out v ### JWK Generator -The `JwkGenerator` class provides methods for generating and managing JSON Web Keys (JWK) for cryptographic operations. It supports RSA, EC, and symmetric keys, and provides thumbprint computation and key serialization. +The `JwkGenerator` class in the `MaksIT.Core.Security.JWK` namespace provides a utility method for generating a minimal RSA public JWK (JSON Web Key) from a given `RSA` instance. + +--- #### Features -- Key Generation: Generate RSA, EC, and symmetric (octet) JWKs, with or without private key material. -- Thumbprint Computation: Compute RFC 7638-compliant thumbprints for JWKs. -- Key Serialization: Export and import JWKs for interoperability. -- Try Pattern: All methods use the Try-pattern for safe error handling. + +1. **Generate RSA Public JWK**: + - Extracts the RSA public exponent and modulus from an `RSA` object and encodes them as a JWK. + +--- #### Example Usage + ```csharp -// Generate a new RSA JWK (public only) -JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var error); -// Generate a new EC JWK (private) -JwkGenerator.TryGenerateEc(JwkCurve.P256, true, null, null, null, out var ecJwk, out var error); -// Compute a thumbprint -JwkGenerator.TryComputeThumbprint(jwk, out var thumbprint, out var error); +using System.Security.Cryptography; +using MaksIT.Core.Security.JWK; + +using var rsa = RSA.Create(2048); +var result = JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var errorMessage); +if (result) +{ + // jwk contains KeyType, RsaExponent, RsaModulus + Console.WriteLine($"Exponent: {jwk!.RsaExponent}, Modulus: {jwk.RsaModulus}"); +} +else +{ + Console.WriteLine($"Error: {errorMessage}"); +} ``` --- +#### API + +```csharp +public static bool TryGenerateFromRCA( + RSA rsa, + out Jwk? jwk, + [NotNullWhen(false)] out string? errorMessage +) +``` +- `rsa`: The RSA instance to extract public parameters from. +- `jwk`: The resulting JWK object (with `KeyType`, `RsaExponent`, and `RsaModulus`). +- `errorMessage`: Error message if generation fails. + +--- + +#### Notes +- Only supports RSA public keys. +- The generated JWK includes only the public exponent and modulus. +- Returns `false` and an error message if the RSA parameters are missing or invalid. + +--- + ### JWS Generator -The `JwsGenerator` class provides methods for creating and verifying JSON Web Signatures (JWS) using JWKs. It supports signing payloads with RSA keys and produces JWS objects with protected headers, payload, and signature. - -#### Features -- JWS Creation: Sign string, byte[], or object payloads using RSA JWKs. -- Try Pattern: All methods use the Try-pattern for safe error handling. -- Key Authorization: Generate key authorization strings for ACME/Let's Encrypt flows. - -#### Example Usage -```csharp -// Sign a payload -JwsGenerator.TryEncode(rsa, jwk, new JwsHeader(), "payload", out var jws, out var error); -// Generate key authorization -JwsGenerator.TryGetKeyAuthorization(jwk, "token", out var keyAuth, out var error); -``` - ---- - -### JWT Generator - -The `JwtGenerator` class provides methods for generating and validating JSON Web Tokens (JWTs). - -#### Features -- Token Generation: Generate JWTs with claims and metadata. -- Token Validation: Validate JWTs against a secret. - -#### Example Usage -```csharp -JwtGenerator.TryGenerateToken(secret, issuer, audience, 60, "user", roles, out var token, out var error); -``` - ---- - -### TOTP Generator - -The `TotpGenerator` class provides methods for generating and validating Time-Based One-Time Passwords (TOTP). +The `JwsGenerator` class in the `MaksIT.Core.Security.JWS` namespace provides methods for creating JSON Web Signatures (JWS) using RSA keys and JWKs. It supports signing string or object payloads and produces JWS objects with protected headers, payload, and signature. --- #### Features -1. **TOTP Generation**: - - Generate TOTPs based on shared secrets. - -2. **TOTP Validation**: - - Validate TOTPs with time tolerance. +1. **JWS Creation**: + - Sign string or object payloads using an RSA key and JWK. + - Produces a JWS message containing the protected header, payload, and signature. --- #### Example Usage -##### Generating a TOTP ```csharp -TotpGenerator.TryGenerate(secret, TotpGenerator.GetCurrentTimeStepNumber(), out var totp, out var error); +using System.Security.Cryptography; +using MaksIT.Core.Security.JWK; +using MaksIT.Core.Security.JWS; + +using var rsa = RSA.Create(2048); +JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var errorMessage); +var header = new JwsHeader(); +var payload = "my-payload"; +var result = JwsGenerator.TryEncode(rsa, jwk!, header, payload, out var jwsMessage, out var error); +if (result) +{ + Console.WriteLine($"Signature: {jwsMessage!.Signature}"); +} +else +{ + Console.WriteLine($"Error: {error}"); +} ``` --- +#### API + +```csharp +public static bool TryEncode( + RSA rsa, + Jwk jwk, + JwsHeader protectedHeader, + out JwsMessage? message, + [NotNullWhen(false)] out string? errorMessage +) +``` +- Signs an empty payload. + +```csharp +public static bool TryEncode( + RSA rsa, + Jwk jwk, + JwsHeader protectedHeader, + T? payload, + out JwsMessage? message, + [NotNullWhen(false)] out string? errorMessage +) +``` +- Signs the provided payload (string or object). + +--- + +#### Notes +- Only supports signing (no verification or key authorization). +- The protected header is automatically set to use RS256. +- The payload is base64url encoded. +- Returns `false` and an error message if signing fails. + +--- + +### JWK Thumbprint Utility + +The `JwkThumbprintUtility` class in the `MaksIT.Core.Security.JWK` namespace provides methods for computing RFC7638 JWK SHA-256 thumbprints and generating key authorization strings for ACME challenges. + +--- + +#### Features + +1. **JWK SHA-256 Thumbprint**: + - Computes the RFC7638-compliant SHA-256 thumbprint of a JWK (Base64Url encoded). +2. **ACME Key Authorization**: + - Generates the key authorization string for ACME/Let's Encrypt HTTP challenges. + +--- + +#### Example Usage + +##### Computing a JWK Thumbprint +```csharp +using System.Security.Cryptography; +using MaksIT.Core.Security.JWK; + +using var rsa = RSA.Create(2048); +JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var errorMessage); +var result = JwkThumbprintUtility.TryGetSha256Thumbprint(jwk!, out var thumbprint, out var error); +if (result) +{ + Console.WriteLine($"Thumbprint: {thumbprint}"); +} +else +{ + Console.WriteLine($"Error: {error}"); +} +``` + +##### Generating ACME Key Authorization +```csharp +var token = "acme-token"; +var result = JwkThumbprintUtility.TryGetKeyAuthorization(jwk!, token, out var keyAuth, out var error); +if (result) +{ + Console.WriteLine($"Key Authorization: {keyAuth}"); +} +else +{ + Console.WriteLine($"Error: {error}"); + } +} +``` + +--- + +#### API + +```csharp +public static bool TryGetSha256Thumbprint( + Jwk jwk, + out string? thumbprint, + [NotNullWhen(false)] out string? errorMessage +) +``` +- Computes the RFC7638 SHA-256 thumbprint of the JWK. + +```csharp +public static bool TryGetKeyAuthorization( + Jwk jwk, + string token, + out string? keyAuthorization, + [NotNullWhen(false)] out string? errorMessage +) +``` +- Generates the ACME key authorization string: `{token}.{thumbprint}`. + +--- + +#### Notes +- Only supports RSA JWKs (requires exponent and modulus). +- Returns `false` and an error message if required JWK fields are missing or invalid. +- Thumbprint is Base64Url encoded and suitable for ACME/Let's Encrypt HTTP challenges. + +--- + ## Others ### Culture diff --git a/src/MaksIT.Core.Tests/Security/JWK/JwkGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JWK/JwkGeneratorTests.cs new file mode 100644 index 0000000..4afb469 --- /dev/null +++ b/src/MaksIT.Core.Tests/Security/JWK/JwkGeneratorTests.cs @@ -0,0 +1,70 @@ +using System.Security.Cryptography; +using MaksIT.Core.Security.JWK; + + +namespace MaksIT.Core.Tests.Security.JWK; + +public class JwkGeneratorTests +{ + [Fact] + public void TryGenerateFromRCA_ValidRsa_ReturnsTrueAndJwk() + { + using var rsa = RSA.Create(2048); + var result = JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var errorMessage); + Assert.True(result); + Assert.NotNull(jwk); + Assert.Null(errorMessage); + Assert.Equal(JwkKeyType.Rsa.Name, jwk!.KeyType); + Assert.False(string.IsNullOrEmpty(jwk.RsaExponent)); + Assert.False(string.IsNullOrEmpty(jwk.RsaModulus)); + } + + [Fact] + public void TryGenerateFromRCA_MissingExponentOrModulus_ReturnsFalseAndError() + { + using var rsa = RSA.Create(); + // ExportParameters returns valid values, so we simulate missing exponent/modulus by mocking + // Instead, test with a custom RSA implementation that throws + var fakeRsa = new FakeRsaMissingParams(); + var result = JwkGenerator.TryGenerateFromRCA(fakeRsa, out var jwk, out var errorMessage); + Assert.False(result); + Assert.Null(jwk); + Assert.Contains("missing exponent or modulus", errorMessage); + } + + [Fact] + public void TryGenerateFromRCA_ExportParametersThrows_ReturnsFalseAndError() + { + var fakeRsa = new FakeRsaThrows(); + var result = JwkGenerator.TryGenerateFromRCA(fakeRsa, out var jwk, out var errorMessage); + Assert.False(result); + Assert.Null(jwk); + Assert.Contains("ExportParameters failed", errorMessage); + } + + private class FakeRsaMissingParams : RSA + { + public override RSAParameters ExportParameters(bool includePrivateParameters) + => new RSAParameters { Exponent = null, Modulus = null }; + // ...other abstract members throw NotImplementedException + public override byte[] Decrypt(byte[] data, RSAEncryptionPadding padding) => throw new NotImplementedException(); + public override byte[] Encrypt(byte[] data, RSAEncryptionPadding padding) => throw new NotImplementedException(); + public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) => throw new NotImplementedException(); + public override bool VerifyHash(byte[] hash, byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) => throw new NotImplementedException(); + public override void ImportParameters(RSAParameters parameters) => throw new NotImplementedException(); + protected override void Dispose(bool disposing) { } + } + + private class FakeRsaThrows : RSA + { + public override RSAParameters ExportParameters(bool includePrivateParameters) + => throw new Exception("ExportParameters failed"); + // ...other abstract members throw NotImplementedException + public override byte[] Decrypt(byte[] data, RSAEncryptionPadding padding) => throw new NotImplementedException(); + public override byte[] Encrypt(byte[] data, RSAEncryptionPadding padding) => throw new NotImplementedException(); + public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) => throw new NotImplementedException(); + public override bool VerifyHash(byte[] hash, byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) => throw new NotImplementedException(); + public override void ImportParameters(RSAParameters parameters) => throw new NotImplementedException(); + protected override void Dispose(bool disposing) { } + } +} diff --git a/src/MaksIT.Core.Tests/Security/JWK/JwkThumbprintUtilityTests.cs b/src/MaksIT.Core.Tests/Security/JWK/JwkThumbprintUtilityTests.cs new file mode 100644 index 0000000..eccf924 --- /dev/null +++ b/src/MaksIT.Core.Tests/Security/JWK/JwkThumbprintUtilityTests.cs @@ -0,0 +1,63 @@ +using System.Security.Cryptography; +using MaksIT.Core.Security; +using MaksIT.Core.Security.JWK; + + +namespace MaksIT.Core.Tests.Security.JWK; + +public class JwkThumbprintUtilityTests +{ + [Fact] + public void TryGetSha256Thumbprint_ValidRsaJwk_ReturnsTrueAndThumbprint() + { + using var rsa = RSA.Create(2048); + var genResult = JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var genError); + Assert.True(genResult); + Assert.NotNull(jwk); + var result = JwkThumbprintUtility.TryGetSha256Thumbprint(jwk!, out var thumbprint, out var error); + Assert.True(result); + Assert.False(string.IsNullOrEmpty(thumbprint)); + Assert.Null(error); + // Should be base64url encoded and of expected length (SHA256 =32 bytes) + var decoded = Base64UrlUtility.Decode(thumbprint!); + Assert.Equal(32, decoded.Length); + } + + [Fact] + public void TryGetSha256Thumbprint_NullExponentOrModulus_ReturnsFalseAndError() + { + var jwk = new Jwk { RsaExponent = null, RsaModulus = null }; + var result = JwkThumbprintUtility.TryGetSha256Thumbprint(jwk, out var thumbprint, out var error); + Assert.False(result); + Assert.Null(thumbprint); + Assert.Contains("exponent or modulus", error); + } + + [Fact] + public void TryGetKeyAuthorization_ValidJwk_ReturnsTrueAndKeyAuthorization() + { + using var rsa = RSA.Create(2048); + var genResult = JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var genError); + Assert.True(genResult); + Assert.NotNull(jwk); + var token = "test-token"; + var result = JwkThumbprintUtility.TryGetKeyAuthorization(jwk!, token, out var keyAuth, out var error); + Assert.True(result); + Assert.Null(error); + Assert.StartsWith(token + ".", keyAuth); + var parts = keyAuth!.Split('.'); + Assert.Equal(2, parts.Length); + Assert.False(string.IsNullOrEmpty(parts[1])); + } + + [Fact] + public void TryGetKeyAuthorization_NullExponentOrModulus_ReturnsFalseAndError() + { + var jwk = new Jwk { RsaExponent = null, RsaModulus = null }; + var token = "test-token"; + var result = JwkThumbprintUtility.TryGetKeyAuthorization(jwk, token, out var keyAuth, out var error); + Assert.False(result); + Assert.Null(keyAuth); + Assert.Contains("Failed to compute thumbprint", error); + } +} diff --git a/src/MaksIT.Core.Tests/Security/JwkGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JwkGeneratorTests.cs deleted file mode 100644 index d336ced..0000000 --- a/src/MaksIT.Core.Tests/Security/JwkGeneratorTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using MaksIT.Core.Security.JWK; - -namespace MaksIT.Core.Tests.Security; - -public class JwkGeneratorTests -{ - [Fact] - public void GenerateRsa_PublicKey_ShouldHaveRequiredFields() - { - var result = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jwk); - Assert.Equal(JwkKeyType.Rsa.Name, jwk.KeyType); - Assert.NotNull(jwk.RsaModulus); - Assert.NotNull(jwk.RsaExponent); - Assert.Null(jwk.PrivateKey); - Assert.Null(jwk.RsaFirstPrimeFactor); - Assert.Null(jwk.RsaSecondPrimeFactor); - Assert.Null(jwk.RsaFirstFactorCRTExponent); - Assert.Null(jwk.RsaSecondFactorCRTExponent); - Assert.Null(jwk.RsaFirstCRTCoefficient); - Assert.False(string.IsNullOrWhiteSpace(jwk.KeyId)); - } - - [Fact] - public void GenerateRsa_PrivateKey_ShouldHavePrivateFields() - { - var result = JwkGenerator.TryGenerateRsa(2048, true, null, null, null, out var jwk, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jwk); - Assert.Equal(JwkKeyType.Rsa.Name, jwk.KeyType); - Assert.NotNull(jwk.PrivateKey); - Assert.NotNull(jwk.RsaFirstPrimeFactor); - Assert.NotNull(jwk.RsaSecondPrimeFactor); - Assert.NotNull(jwk.RsaFirstFactorCRTExponent); - Assert.NotNull(jwk.RsaSecondFactorCRTExponent); - Assert.NotNull(jwk.RsaFirstCRTCoefficient); - } - - [Theory] - [InlineData("P-256")] - [InlineData("P-384")] - [InlineData("P-521")] - public void GenerateEc_PublicKey_ShouldHaveRequiredFields(string curve) - { - var curveObj = JwkCurve.GetAll().First(c => c.Name == curve); - var result = JwkGenerator.TryGenerateEc(curveObj, false, null, null, null, out var jwk, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jwk); - Assert.Equal(JwkKeyType.Ec.Name, jwk.KeyType); - Assert.Equal(curve, jwk.EcCurve); - Assert.NotNull(jwk.EcX); - Assert.NotNull(jwk.EcY); - Assert.Null(jwk.PrivateKey); - Assert.False(string.IsNullOrWhiteSpace(jwk.KeyId)); - } - - [Theory] - [InlineData("P-256")] - [InlineData("P-384")] - [InlineData("P-521")] - public void GenerateEc_PrivateKey_ShouldHavePrivateField(string curve) - { - var curveObj = JwkCurve.GetAll().First(c => c.Name == curve); - var result = JwkGenerator.TryGenerateEc(curveObj, true, null, null, null, out var jwk, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jwk); - Assert.Equal(JwkKeyType.Ec.Name, jwk.KeyType); - Assert.Equal(curve, jwk.EcCurve); - Assert.NotNull(jwk.PrivateKey); - } - - [Fact] - public void GenerateOct_ShouldHaveRequiredFields() - { - var result = JwkGenerator.TryGenerateOct(256, null, null, null, out var jwk, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jwk); - Assert.Equal(JwkKeyType.Oct.Name, jwk.KeyType); - Assert.NotNull(jwk.SymmetricKey); - Assert.False(string.IsNullOrWhiteSpace(jwk.KeyId)); - } - - [Fact] - public void TryComputeThumbprint_ShouldReturnValidThumbprint() - { - var result = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jwk); - var thumbResult = JwkGenerator.TryComputeThumbprint(jwk, out var thumb, out var error); - Assert.True(thumbResult, error); - Assert.False(string.IsNullOrWhiteSpace(thumb)); - Assert.Null(error); - } - - [Fact] - public void ComputeKid_ShouldBeUniqueForDifferentKeys() - { - var result1 = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk1, out var errorMessage1); - var result2 = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk2, out var errorMessage2); - Assert.True(result1, errorMessage1); - Assert.True(result2, errorMessage2); - Assert.NotNull(jwk1); - Assert.NotNull(jwk2); - Assert.NotEqual(jwk1.KeyId, jwk2.KeyId); - } -} diff --git a/src/MaksIT.Core.Tests/Security/JwsGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JwsGeneratorTests.cs index 1e7aa0f..a47783e 100644 --- a/src/MaksIT.Core.Tests/Security/JwsGeneratorTests.cs +++ b/src/MaksIT.Core.Tests/Security/JwsGeneratorTests.cs @@ -1,5 +1,5 @@ -using System.Text; -using System.Security.Cryptography; +using System.Security.Cryptography; +using MaksIT.Core.Security; using MaksIT.Core.Security.JWK; using MaksIT.Core.Security.JWS; @@ -7,78 +7,91 @@ using MaksIT.Core.Security.JWS; namespace MaksIT.Core.Tests.Security; public class JwsGeneratorTests { - private static (RSA rsa, Jwk jwk) GenerateRsaAndJwk() { - var rsa = RSA.Create(2048); - var result = JwkGenerator.TryGenerateRsaFromRsa(rsa, true, null, null, null, out var jwk, out var errorMessage); - Assert.True(result, errorMessage); + [Fact] + public void TryEncode_ValidRsaAndJwk_ReturnsTrueAndMessage() + { + using var rsa = RSA.Create(2048); + var jwkResult = JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var jwkError); + Assert.True(jwkResult); Assert.NotNull(jwk); - return (rsa, jwk); + var header = new JwsHeader(); + var result = JwsGenerator.TryEncode(rsa, jwk!, header, out var message, out var error); + Assert.True(result); + Assert.NotNull(message); + Assert.Null(error); + Assert.False(string.IsNullOrEmpty(message!.Protected)); + Assert.False(string.IsNullOrEmpty(message.Signature)); } [Fact] - public void Encode_WithStringPayload_ProducesValidJws() { - var (rsa, jwk) = GenerateRsaAndJwk(); + public void TryEncode_WithPayload_ReturnsEncodedPayload() + { + using var rsa = RSA.Create(2048); + var jwkResult = JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var jwkError); + Assert.True(jwkResult); + Assert.NotNull(jwk); var header = new JwsHeader(); var payload = "test-payload"; - - var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jws); - Assert.False(string.IsNullOrEmpty(jws.Protected)); - Assert.False(string.IsNullOrEmpty(jws.Payload)); - Assert.False(string.IsNullOrEmpty(jws.Signature)); + var result = JwsGenerator.TryEncode(rsa, jwk!, header, payload, out var message, out var error); + Assert.True(result); + Assert.NotNull(message); + Assert.Null(error); + Assert.False(string.IsNullOrEmpty(message!.Payload)); + // Decoded payload should match + var decoded = Base64UrlUtility.DecodeToString(message.Payload); + Assert.Equal(payload, decoded); } [Fact] - public void Encode_WithByteArrayPayload_ProducesValidJws() { - var (rsa, jwk) = GenerateRsaAndJwk(); + public void TryEncode_InvalidRsa_ReturnsFalseAndError() + { + var fakeRsa = new FakeRsaThrows(); + var jwk = new Jwk { KeyType = JwkKeyType.Rsa.Name }; var header = new JwsHeader(); - var payload = Encoding.UTF8.GetBytes("test-bytes"); - - var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jws); - Assert.False(string.IsNullOrEmpty(jws.Protected)); - Assert.False(string.IsNullOrEmpty(jws.Payload)); - Assert.False(string.IsNullOrEmpty(jws.Signature)); + var result = JwsGenerator.TryEncode(fakeRsa, jwk, header, out var message, out var error); + Assert.False(result); + Assert.Null(message); + Assert.NotNull(error); } [Fact] - public void Encode_WithGenericPayload_ProducesValidJws() { - var (rsa, jwk) = GenerateRsaAndJwk(); + public void TryEncode_JwkWithKeyId_SetsHeaderKid() + { + using var rsa = RSA.Create(2048); + var jwk = new Jwk { KeyType = JwkKeyType.Rsa.Name, KeyId = "my-key-id" }; var header = new JwsHeader(); - var payload = new { foo = "bar", n = 42 }; - - var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jws); - Assert.False(string.IsNullOrEmpty(jws.Protected)); - Assert.False(string.IsNullOrEmpty(jws.Payload)); - Assert.False(string.IsNullOrEmpty(jws.Signature)); + var result = JwsGenerator.TryEncode(rsa, jwk, header, out var message, out var error); + Assert.True(result); + Assert.NotNull(message); + Assert.Null(error); + // Decode protected header + var protectedJson = Base64UrlUtility.DecodeToString(message!.Protected); + Assert.Contains("my-key-id", protectedJson); } [Fact] - public void Encode_PostAsGet_ProducesValidJws() { - var (rsa, jwk) = GenerateRsaAndJwk(); + public void TryEncode_JwkWithoutKeyId_SetsHeaderJwk() + { + using var rsa = RSA.Create(2048); + var jwk = new Jwk { KeyType = JwkKeyType.Rsa.Name }; var header = new JwsHeader(); - - var result = JwsGenerator.TryEncode(rsa, jwk, header, out var jws, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(jws); - Assert.False(string.IsNullOrEmpty(jws.Protected)); - Assert.Equal(string.Empty, jws.Payload); - Assert.False(string.IsNullOrEmpty(jws.Signature)); + var result = JwsGenerator.TryEncode(rsa, jwk, header, out var message, out var error); + Assert.True(result); + Assert.NotNull(message); + Assert.Null(error); + var protectedJson = Base64UrlUtility.DecodeToString(message!.Protected); + Assert.Contains("jwk", protectedJson); } - [Fact] - public void GetKeyAuthorization_ReturnsExpectedFormat() { - var (rsa, jwk) = GenerateRsaAndJwk(); - var token = "test-token"; - - var result = JwsGenerator.TryGetKeyAuthorization(jwk, token, out var keyAuth, out var errorMessage); - Assert.True(result, errorMessage); - Assert.NotNull(keyAuth); - Assert.StartsWith(token + ".", keyAuth); - Assert.True(keyAuth.Length > token.Length + 1); + private class FakeRsaThrows : RSA + { + public override RSAParameters ExportParameters(bool includePrivateParameters) + => throw new Exception("ExportParameters failed"); + public override byte[] Decrypt(byte[] data, RSAEncryptionPadding padding) => throw new NotImplementedException(); + public override byte[] Encrypt(byte[] data, RSAEncryptionPadding padding) => throw new NotImplementedException(); + public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) => throw new Exception("SignData failed"); + public override bool VerifyHash(byte[] hash, byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) => throw new NotImplementedException(); + public override void ImportParameters(RSAParameters parameters) => throw new NotImplementedException(); + protected override void Dispose(bool disposing) { } } } diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index 416491f..4a924ee 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.5.6 + 1.5.7 Maksym Sadovnychyy MAKS-IT MaksIT.Core diff --git a/src/MaksIT.Core/Security/Base64UrlUtility.cs b/src/MaksIT.Core/Security/Base64UrlUtility.cs new file mode 100644 index 0000000..a63b0e5 --- /dev/null +++ b/src/MaksIT.Core/Security/Base64UrlUtility.cs @@ -0,0 +1,54 @@ +using System.Text; + + +namespace MaksIT.Core.Security; + +/// +/// Provides RFC 4648-compliant Base64Url encoding and decoding utilities. +/// +public static class Base64UrlUtility { + + /// + /// Encodes a UTF-8 string to a Base64Url string (RFC 4648 §5). + /// https://tools.ietf.org/html/rfc4648#section-5 + /// + public static string Encode(string value) => + Encode(Encoding.UTF8.GetBytes(value)); + + /// + /// Encodes a byte array to a Base64Url string (RFC 4648 §5). + /// + public static string Encode(byte[] data) { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + string base64 = Convert.ToBase64String(data); // Regular base64 encoder + + return base64.TrimEnd('=') // Remove any trailing '='s + .Replace('+', '-') // 62nd char of encoding + .Replace('/', '_'); // 63rd char of encoding + } + + /// + /// Decodes a Base64Url string to a UTF-8 string. + /// + public static string DecodeToString(string base64Url) => + Encoding.UTF8.GetString(Decode(base64Url)); + + /// + /// Decodes a Base64Url string to a byte array. + /// + public static byte[] Decode(string base64Url) { + if (base64Url == null) + throw new ArgumentNullException(nameof(base64Url)); + + string padded = base64Url.Replace('-', '+').Replace('_', '/'); + + switch (base64Url.Length % 4) { + case 2: padded += "=="; break; + case 3: padded += "="; break; + } + + return Convert.FromBase64String(padded); + } +} diff --git a/src/MaksIT.Core/Security/JWK/Base64UrlUtility.cs b/src/MaksIT.Core/Security/JWK/Base64UrlUtility.cs deleted file mode 100644 index 10e5551..0000000 --- a/src/MaksIT.Core/Security/JWK/Base64UrlUtility.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Text; - - -namespace MaksIT.Core.Security.JWK; - -/// -/// Provides RFC 4648-compliant Base64Url encoding and decoding utilities. -/// -public static class Base64UrlUtility -{ - /// - /// Encodes a byte array to a Base64Url string (RFC 4648 §5). - /// - public static string Encode(byte[] data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - string base64 = Convert.ToBase64String(data); - - return base64.TrimEnd('=') - .Replace('+', '-') - .Replace('/', '_'); - } - - /// - /// Encodes a UTF-8 string to a Base64Url string (RFC 4648 §5). - /// - public static string Encode(string value) - { - if (value == null) - throw new ArgumentNullException(nameof(value)); - - return Encode(Encoding.UTF8.GetBytes(value)); - } - - /// - /// Decodes a Base64Url string to a byte array. - /// - public static byte[] Decode(string base64Url) - { - if (base64Url == null) - throw new ArgumentNullException(nameof(base64Url)); - - string padded = base64Url.Replace('-', '+').Replace('_', '/'); - - switch (base64Url.Length % 4) - { - case 2: padded += "=="; break; - case 3: padded += "="; break; - } - - return Convert.FromBase64String(padded); - } - - /// - /// Decodes a Base64Url string to a UTF-8 string. - /// - public static string DecodeToString(string base64Url) - { - return Encoding.UTF8.GetString(Decode(base64Url)); - } -} diff --git a/src/MaksIT.Core/Security/JWK/JwkGenerator.cs b/src/MaksIT.Core/Security/JWK/JwkGenerator.cs index caa3119..87bb223 100644 --- a/src/MaksIT.Core/Security/JWK/JwkGenerator.cs +++ b/src/MaksIT.Core/Security/JWK/JwkGenerator.cs @@ -1,8 +1,5 @@ -using System.Text; -using System.Security.Cryptography; -using System.Text.Json.Serialization; +using System.Security.Cryptography; using System.Diagnostics.CodeAnalysis; -using MaksIT.Core.Extensions; namespace MaksIT.Core.Security.JWK; @@ -11,208 +8,32 @@ namespace MaksIT.Core.Security.JWK; /// Provides utilities for JWK (JSON Web Key) operations, including RFC7638 thumbprint computation and key generation. /// public static class JwkGenerator { - public static bool TryGenerateRsa(int keySize, bool includePrivate, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) { + public static bool TryGenerateFromRCA( + RSA rsa, + out Jwk? jwk, + [NotNullWhen(false)] out string? errorMessage +) { try { - jwk = GenerateRsa(keySize, includePrivate, alg, use, keyOps); - errorMessage = null; - return true; - } - catch (Exception ex) { - jwk = null; - errorMessage = ex.Message; - return false; - } - } + var publicParameters = rsa.ExportParameters(false); - public static bool TryGenerateEc(JwkCurve? curve, bool includePrivate, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) { - try { - jwk = GenerateEc(curve, includePrivate, alg, use, keyOps); - errorMessage = null; - return true; - } - catch (Exception ex) { - jwk = null; - errorMessage = ex.Message; - return false; - } - } + var exp = publicParameters.Exponent; + var mod = publicParameters.Modulus; - public static bool TryGenerateOct(int keySizeBits, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) { - try { - jwk = GenerateOct(keySizeBits, alg, use, keyOps); - errorMessage = null; - return true; - } - catch (Exception ex) { - jwk = null; - errorMessage = ex.Message; - return false; - } - } + if (exp == null || mod == null) + throw new ArgumentException("RSA parameters are missing exponent or modulus."); - public static bool TryGenerateRsaFromRsa(RSA rsa, bool includePrivate, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) { - try { - jwk = GenerateRsaFromRsa(rsa, includePrivate, alg, use, keyOps); - errorMessage = null; - return true; - } - catch (Exception ex) { - jwk = null; - errorMessage = ex.Message; - return false; - } - } - - public static bool TryComputeThumbprint( - Jwk jwk, - [NotNullWhen(true)] out string? thumbprint, - [NotNullWhen(false)] out string? errorMessage) { - thumbprint = null; - errorMessage = null; - - if (jwk == null) { - errorMessage = "JWK cannot be null."; - return false; - } - if (string.IsNullOrEmpty(jwk.RsaExponent) || string.IsNullOrEmpty(jwk.RsaModulus)) { - errorMessage = "JWK must have RsaExponent and RsaModulus set."; - return false; - } - - try { - // RFC7638: Lexicographic order: e, kty, n - var orderedJwk = new OrderedJwk { - E = jwk.RsaExponent!, - Kty = "RSA", - N = jwk.RsaModulus! + jwk = new Jwk { + KeyType = JwkKeyType.Rsa.Name, + RsaExponent = Base64UrlUtility.Encode(exp), + RsaModulus = Base64UrlUtility.Encode(mod), }; - - var json = orderedJwk.ToJson(); - var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); - thumbprint = Base64UrlEncode(hash); + errorMessage = null; return true; } catch (Exception ex) { + jwk = null; errorMessage = ex.Message; return false; } } - - private static Jwk GenerateRsa(int keySize = 2048, bool includePrivate = false, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) { - using var rsa = RSA.Create(keySize); - var parameters = rsa.ExportParameters(includePrivate); - var jwk = new Jwk { - KeyType = JwkKeyType.Rsa.Name, - RsaModulus = Base64UrlEncode(parameters.Modulus!), - RsaExponent = Base64UrlEncode(parameters.Exponent!), - Algorithm = (alg ?? (keySize >= 4096 ? JwkAlgorithm.Rs512 : JwkAlgorithm.Rs256)).Name, - KeyUse = use, - KeyOperations = keyOps, - }; - if (includePrivate) { - jwk.PrivateKey = Base64UrlEncode(parameters.D!); - jwk.RsaFirstPrimeFactor = Base64UrlEncode(parameters.P!); - jwk.RsaSecondPrimeFactor = Base64UrlEncode(parameters.Q!); - jwk.RsaFirstFactorCRTExponent = Base64UrlEncode(parameters.DP!); - jwk.RsaSecondFactorCRTExponent = Base64UrlEncode(parameters.DQ!); - jwk.RsaFirstCRTCoefficient = Base64UrlEncode(parameters.InverseQ!); - } - jwk.KeyId = ComputeKid(jwk); - return jwk; - } - - private static Jwk GenerateEc(JwkCurve? curve = null, bool includePrivate = false, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) { - curve ??= JwkCurve.P256; - ECCurve ecCurve = curve.Name switch { - "P-256" => ECCurve.NamedCurves.nistP256, - "P-384" => ECCurve.NamedCurves.nistP384, - "P-521" => ECCurve.NamedCurves.nistP521, - _ => throw new ArgumentException($"Unsupported curve: {curve.Name}") - }; - using var ec = ECDsa.Create(ecCurve); - var parameters = ec.ExportParameters(includePrivate); - var jwk = new Jwk { - KeyType = JwkKeyType.Ec.Name, - EcCurve = curve.Name, - EcX = Base64UrlEncode(parameters.Q.X!), - EcY = Base64UrlEncode(parameters.Q.Y!), - Algorithm = (alg ?? (curve == JwkCurve.P384 ? JwkAlgorithm.Es384 : curve == JwkCurve.P521 ? JwkAlgorithm.Es512 : JwkAlgorithm.Es256)).Name, - KeyUse = use, - KeyOperations = keyOps, - }; - if (includePrivate && parameters.D != null) { - jwk.PrivateKey = Base64UrlEncode(parameters.D); - } - jwk.KeyId = ComputeKid(jwk); - return jwk; - } - - private static Jwk GenerateOct(int keySizeBits = 256, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) { - var key = RandomNumberGenerator.GetBytes(keySizeBits / 8); - var jwk = new Jwk { - KeyType = JwkKeyType.Oct.Name, - SymmetricKey = Base64UrlEncode(key), - Algorithm = (alg ?? (keySizeBits == 256 ? JwkAlgorithm.A256Gcm : keySizeBits == 128 ? JwkAlgorithm.A128Gcm : JwkAlgorithm.A512Gcm)).Name, - KeyUse = use, - KeyOperations = keyOps, - }; - jwk.KeyId = ComputeKid(jwk); - return jwk; - } - - private static Jwk GenerateRsaFromRsa(RSA rsa, bool includePrivate = false, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) { - if (rsa == null) throw new ArgumentNullException(nameof(rsa)); - var parameters = rsa.ExportParameters(includePrivate); - var jwk = new Jwk { - KeyType = JwkKeyType.Rsa.Name, - RsaModulus = Base64UrlUtility.Encode(parameters.Modulus!), - RsaExponent = Base64UrlUtility.Encode(parameters.Exponent!), - Algorithm = (alg ?? JwkAlgorithm.Rs256).Name, - KeyUse = use, - KeyOperations = keyOps, - }; - if (includePrivate) { - jwk.PrivateKey = Base64UrlUtility.Encode(parameters.D!); - jwk.RsaFirstPrimeFactor = Base64UrlUtility.Encode(parameters.P!); - jwk.RsaSecondPrimeFactor = Base64UrlUtility.Encode(parameters.Q!); - jwk.RsaFirstFactorCRTExponent = Base64UrlUtility.Encode(parameters.DP!); - jwk.RsaSecondFactorCRTExponent = Base64UrlUtility.Encode(parameters.DQ!); - jwk.RsaFirstCRTCoefficient = Base64UrlUtility.Encode(parameters.InverseQ!); - } - jwk.KeyId = ComputeKid(jwk); - return jwk; - } - - private static string Base64UrlEncode(byte[] data) { - return Base64UrlUtility.Encode(data); - } - - private static string ComputeKid(Jwk jwk) { - // Use thumbprint as kid if possible - if (jwk.KeyType == "RSA" && !string.IsNullOrEmpty(jwk.RsaModulus) && !string.IsNullOrEmpty(jwk.RsaExponent)) { - TryComputeThumbprint(jwk, out var thumb, out _); - return thumb ?? Guid.NewGuid().ToString("N"); - } - // For EC and oct, use a hash of the key material - using var sha = SHA256.Create(); - string keyMaterial = jwk.KeyType switch { - "EC" => jwk.EcX + jwk.EcY + jwk.EcCurve, - "oct" => jwk.SymmetricKey, - _ => null - } ?? Guid.NewGuid().ToString(); - var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(keyMaterial)); - return Base64UrlEncode(hash); - } - - // Helper class for correct property order and names - private class OrderedJwk { - [JsonPropertyName("e")] - public string E { get; set; } = default!; - - [JsonPropertyName("kty")] - public string Kty { get; set; } = default!; - - [JsonPropertyName("n")] - public string N { get; set; } = default!; - } } diff --git a/src/MaksIT.Core/Security/JWK/JwkThumbprintUtility.cs b/src/MaksIT.Core/Security/JWK/JwkThumbprintUtility.cs new file mode 100644 index 0000000..3e1892b --- /dev/null +++ b/src/MaksIT.Core/Security/JWK/JwkThumbprintUtility.cs @@ -0,0 +1,56 @@ +using System.Security.Cryptography; +using System.Text; +using System.Diagnostics.CodeAnalysis; +using MaksIT.Core.Extensions; + + +namespace MaksIT.Core.Security.JWK; + +public static class JwkThumbprintUtility { + /// + /// Returns the key authorization string for ACME challenges. + /// + public static bool TryGetKeyAuthorization( + Jwk jwk, + string token, + out string? keyAuthorization, + [NotNullWhen(false)] out string? errorMessage + ) { + keyAuthorization = null; + errorMessage = null; + if (!TryGetSha256Thumbprint(jwk, out var thumbprint, out var thumbprintError)) { + errorMessage = $"Failed to compute thumbprint: {thumbprintError}"; + return false; + } + keyAuthorization = $"{token}.{thumbprint}"; + return true; + } + + /// + /// Computes the RFC7638 JWK SHA-256 thumbprint (Base64Url encoded). + /// For thumbprint calculation, always build the JSON string manually or use OrderedJwk for correct property order. + /// + public static bool TryGetSha256Thumbprint( + Jwk jwk, + out string? thumbprint, + [NotNullWhen(false)] out string? errorMessage + ) { + thumbprint = null; + errorMessage = null; + try { + if (jwk.RsaExponent == null || jwk.RsaModulus == null) + throw new ArgumentException("RSA exponent or modulus is null."); + var thumbprintObj = new OrderedJwk { + E = jwk.RsaExponent, + Kty = JwkKeyType.Rsa.Name, + N = jwk.RsaModulus + }; + var json = thumbprintObj.ToJson(); + thumbprint = Base64UrlUtility.Encode(SHA256.HashData(Encoding.UTF8.GetBytes(json))); + return true; + } catch (Exception ex) { + errorMessage = ex.Message; + return false; + } + } +} diff --git a/src/MaksIT.Core/Security/JWK/OrderedJwk.cs b/src/MaksIT.Core/Security/JWK/OrderedJwk.cs new file mode 100644 index 0000000..fee2cad --- /dev/null +++ b/src/MaksIT.Core/Security/JWK/OrderedJwk.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace MaksIT.Core.Security.JWK; +public class OrderedJwk { + [JsonPropertyName("e")] + public string E { get; set; } = default!; + + [JsonPropertyName("kty")] + public string Kty { get; set; } = default!; + + [JsonPropertyName("n")] + public string N { get; set; } = default!; +} diff --git a/src/MaksIT.Core/Security/JWS/JwsGenerator.cs b/src/MaksIT.Core/Security/JWS/JwsGenerator.cs index dbd4dc5..06818b7 100644 --- a/src/MaksIT.Core/Security/JWS/JwsGenerator.cs +++ b/src/MaksIT.Core/Security/JWS/JwsGenerator.cs @@ -8,110 +8,60 @@ using MaksIT.Core.Extensions; namespace MaksIT.Core.Security.JWS; public static class JwsGenerator { - public static bool TryEncode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, string? payload, [NotNullWhen(true)] out JwsMessage? jwsMessage, [NotNullWhen(false)] out string? errorMessage, string? keyId = null) { + public static bool TryEncode( + RSA rsa, + Jwk jwk, + JwsHeader protectedHeader, + out JwsMessage? message, + [NotNullWhen(false)] out string? errorMessage + ) => TryEncode(rsa, jwk, protectedHeader, null, out message, out errorMessage); + + + public static bool TryEncode( + RSA rsa, + Jwk jwk, + JwsHeader protectedHeader, + T? payload, + out JwsMessage? message, + [NotNullWhen(false)] out string? errorMessage + ) { try { - jwsMessage = Encode(rsa, jwk, protectedHeader, payload, keyId); + protectedHeader.Algorithm = JwkAlgorithm.Rs256.Name; + + if (jwk.KeyId != null) + protectedHeader.KeyId = jwk.KeyId; + else + protectedHeader.Key = jwk; + + var msg = new JwsMessage { + Payload = "", + Protected = Base64UrlUtility.Encode(protectedHeader.ToJson()) + }; + + if (payload != null) { + if (payload is string stringPayload) + msg.Payload = Base64UrlUtility.Encode(stringPayload); + else + msg.Payload = Base64UrlUtility.Encode(payload.ToJson()); + } + + var signature = rsa.SignData( + Encoding.ASCII.GetBytes($"{msg.Protected}.{msg.Payload}"), + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1 + ); + + msg.Signature = Base64UrlUtility.Encode(signature); + + message = msg; errorMessage = null; + return true; - } catch (Exception ex) { - jwsMessage = null; + } + catch (Exception ex) { + message = null; errorMessage = ex.Message; return false; } } - - public static bool TryEncode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, byte[] payload, [NotNullWhen(true)] out JwsMessage? jwsMessage, [NotNullWhen(false)] out string? errorMessage, string? keyId = null) { - try { - jwsMessage = Encode(rsa, jwk, protectedHeader, payload, keyId); - errorMessage = null; - return true; - } catch (Exception ex) { - jwsMessage = null; - errorMessage = ex.Message; - return false; - } - } - - public static bool TryEncode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, T payload, [NotNullWhen(true)] out JwsMessage? jwsMessage, [NotNullWhen(false)] out string? errorMessage, string? keyId = null) { - try { - jwsMessage = Encode(rsa, jwk, protectedHeader, payload, keyId); - errorMessage = null; - return true; - } catch (Exception ex) { - jwsMessage = null; - errorMessage = ex.Message; - return false; - } - } - - public static bool TryEncode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, [NotNullWhen(true)] out JwsMessage? jwsMessage, [NotNullWhen(false)] out string? errorMessage, string? keyId = null) { - try { - jwsMessage = Encode(rsa, jwk, protectedHeader, keyId); - errorMessage = null; - return true; - } catch (Exception ex) { - jwsMessage = null; - errorMessage = ex.Message; - return false; - } - } - - public static bool TryGetKeyAuthorization(Jwk jwk, string token, [NotNullWhen(true)] out string? keyAuthorization, [NotNullWhen(false)] out string? errorMessage) { - keyAuthorization = null; - errorMessage = null; - if (!JwkGenerator.TryComputeThumbprint(jwk, out var thumb, out errorMessage)) { - return false; - } - keyAuthorization = $"{token}.{thumb}"; - return true; - } - - private static JwsMessage Encode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, string? payload, string? keyId = null) { - return EncodeInternal(rsa, jwk, protectedHeader, payload, keyId); - } - - private static JwsMessage Encode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, byte[] payload, string? keyId = null) { - string encodedPayload = Base64UrlUtility.Encode(payload); - return EncodeInternal(rsa, jwk, protectedHeader, encodedPayload, keyId, isPayloadEncoded: true); - } - - private static JwsMessage Encode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, T payload, string? keyId = null) { - string encodedPayload = Base64UrlUtility.Encode(payload.ToJson()); - return EncodeInternal(rsa, jwk, protectedHeader, encodedPayload, keyId, isPayloadEncoded: true); - } - - private static JwsMessage Encode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, string? keyId = null) { - // POST-as-GET: empty payload - return EncodeInternal(rsa, jwk, protectedHeader, null, keyId); - } - - private static JwsMessage EncodeInternal( - RSA rsa, - Jwk jwk, - JwsHeader protectedHeader, - string? payload, - string? keyId, - bool isPayloadEncoded = false) { - protectedHeader.Algorithm = JwkAlgorithm.Rs256.Name; - - if (!string.IsNullOrEmpty(keyId)) - protectedHeader.KeyId = keyId; - else - protectedHeader.Key = jwk; - - var message = new JwsMessage { - Payload = string.Empty, - Protected = Base64UrlUtility.Encode(protectedHeader.ToJson()) - }; - - if (payload != null) { - message.Payload = isPayloadEncoded ? payload : Base64UrlUtility.Encode(payload); - } - - var signingInput = Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"); - message.Signature = Base64UrlUtility.Encode( - rsa.SignData(signingInput, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); - - return message; - } }