Compare commits

..

2 Commits

Author SHA1 Message Date
Maksym Sadovnychyy
e2f930587a (feature): jwk and jws methods review 2025-11-13 19:40:04 +01:00
Maksym Sadovnychyy
8100c206bc (feature): jwk std impementation and props naming 2025-11-12 16:49:22 +01:00
13 changed files with 731 additions and 611 deletions

237
README.md
View File

@ -28,6 +28,7 @@
- [Password Hasher](#password-hasher) - [Password Hasher](#password-hasher)
- [JWT Generator](#jwt-generator) - [JWT Generator](#jwt-generator)
- [JWK Generator](#jwk-generator) - [JWK Generator](#jwk-generator)
- [JWK Thumbprint Utility](#jwk-thumbprint-utility)
- [JWS Generator](#jws-generator) - [JWS Generator](#jws-generator)
- [TOTP Generator](#totp-generator) - [TOTP Generator](#totp-generator)
- [Web API Models](#web-api-models) - [Web API Models](#web-api-models)
@ -968,85 +969,215 @@ JwtGenerator.TryGenerateToken(secret, issuer, audience, 60, "user", roles, out v
### JWK Generator ### 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 #### 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. 1. **Generate RSA Public JWK**:
- Key Serialization: Export and import JWKs for interoperability. - Extracts the RSA public exponent and modulus from an `RSA` object and encodes them as a JWK.
- Try Pattern: All methods use the Try-pattern for safe error handling.
---
#### Example Usage #### Example Usage
```csharp ```csharp
// Generate a new RSA JWK (public only) using System.Security.Cryptography;
JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var error); using MaksIT.Core.Security.JWK;
// Generate a new EC JWK (private)
JwkGenerator.TryGenerateEc(JwkCurve.P256, true, null, null, null, out var ecJwk, out var error); using var rsa = RSA.Create(2048);
// Compute a thumbprint var result = JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var errorMessage);
JwkGenerator.TryComputeThumbprint(jwk, out var thumbprint, out var error); 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 ### 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. 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
- 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).
--- ---
#### Features #### Features
1. **TOTP Generation**: 1. **JWS Creation**:
- Generate TOTPs based on shared secrets. - Sign string or object payloads using an RSA key and JWK.
- Produces a JWS message containing the protected header, payload, and signature.
2. **TOTP Validation**:
- Validate TOTPs with time tolerance.
--- ---
#### Example Usage #### Example Usage
##### Generating a TOTP
```csharp ```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<T>(
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 ## Others
### Culture ### Culture

View File

@ -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) { }
}
}

View File

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

View File

@ -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.Kty);
Assert.NotNull(jwk.N);
Assert.NotNull(jwk.E);
Assert.Null(jwk.D);
Assert.Null(jwk.P);
Assert.Null(jwk.Q);
Assert.Null(jwk.DP);
Assert.Null(jwk.DQ);
Assert.Null(jwk.QI);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid));
}
[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.Kty);
Assert.NotNull(jwk.D);
Assert.NotNull(jwk.P);
Assert.NotNull(jwk.Q);
Assert.NotNull(jwk.DP);
Assert.NotNull(jwk.DQ);
Assert.NotNull(jwk.QI);
}
[Theory]
[InlineData("P-256")]
[InlineData("P-384")]
[InlineData("P-521")]
public void GenerateEc_PublicKey_ShouldHaveRequiredFields(string curve)
{
var curveObj = JwkCurve.GetAll<JwkCurve>().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.Kty);
Assert.Equal(curve, jwk.Crv);
Assert.NotNull(jwk.X);
Assert.NotNull(jwk.Y);
Assert.Null(jwk.D_EC);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid));
}
[Theory]
[InlineData("P-256")]
[InlineData("P-384")]
[InlineData("P-521")]
public void GenerateEc_PrivateKey_ShouldHavePrivateField(string curve)
{
var curveObj = JwkCurve.GetAll<JwkCurve>().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.Kty);
Assert.Equal(curve, jwk.Crv);
Assert.NotNull(jwk.D_EC);
}
[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.Kty);
Assert.NotNull(jwk.K);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid));
}
[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.Kid, jwk2.Kid);
}
}

View File

@ -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.JWK;
using MaksIT.Core.Security.JWS; using MaksIT.Core.Security.JWS;
@ -7,78 +7,91 @@ using MaksIT.Core.Security.JWS;
namespace MaksIT.Core.Tests.Security; namespace MaksIT.Core.Tests.Security;
public class JwsGeneratorTests { public class JwsGeneratorTests {
private static (RSA rsa, Jwk jwk) GenerateRsaAndJwk() { [Fact]
var rsa = RSA.Create(2048); public void TryEncode_ValidRsaAndJwk_ReturnsTrueAndMessage()
var result = JwkGenerator.TryGenerateRsaFromRsa(rsa, true, null, null, null, out var jwk, out var errorMessage); {
Assert.True(result, errorMessage); using var rsa = RSA.Create(2048);
var jwkResult = JwkGenerator.TryGenerateFromRCA(rsa, out var jwk, out var jwkError);
Assert.True(jwkResult);
Assert.NotNull(jwk); 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] [Fact]
public void Encode_WithStringPayload_ProducesValidJws() { public void TryEncode_WithPayload_ReturnsEncodedPayload()
var (rsa, jwk) = GenerateRsaAndJwk(); {
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 header = new JwsHeader();
var payload = "test-payload"; var payload = "test-payload";
var result = JwsGenerator.TryEncode(rsa, jwk!, header, payload, out var message, out var error);
var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage); Assert.True(result);
Assert.True(result, errorMessage); Assert.NotNull(message);
Assert.NotNull(jws); Assert.Null(error);
Assert.False(string.IsNullOrEmpty(jws.Protected)); Assert.False(string.IsNullOrEmpty(message!.Payload));
Assert.False(string.IsNullOrEmpty(jws.Payload)); // Decoded payload should match
Assert.False(string.IsNullOrEmpty(jws.Signature)); var decoded = Base64UrlUtility.DecodeToString(message.Payload);
Assert.Equal(payload, decoded);
} }
[Fact] [Fact]
public void Encode_WithByteArrayPayload_ProducesValidJws() { public void TryEncode_InvalidRsa_ReturnsFalseAndError()
var (rsa, jwk) = GenerateRsaAndJwk(); {
var fakeRsa = new FakeRsaThrows();
var jwk = new Jwk { KeyType = JwkKeyType.Rsa.Name };
var header = new JwsHeader(); var header = new JwsHeader();
var payload = Encoding.UTF8.GetBytes("test-bytes"); var result = JwsGenerator.TryEncode(fakeRsa, jwk, header, out var message, out var error);
Assert.False(result);
var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage); Assert.Null(message);
Assert.True(result, errorMessage); Assert.NotNull(error);
Assert.NotNull(jws);
Assert.False(string.IsNullOrEmpty(jws.Protected));
Assert.False(string.IsNullOrEmpty(jws.Payload));
Assert.False(string.IsNullOrEmpty(jws.Signature));
} }
[Fact] [Fact]
public void Encode_WithGenericPayload_ProducesValidJws() { public void TryEncode_JwkWithKeyId_SetsHeaderKid()
var (rsa, jwk) = GenerateRsaAndJwk(); {
using var rsa = RSA.Create(2048);
var jwk = new Jwk { KeyType = JwkKeyType.Rsa.Name, KeyId = "my-key-id" };
var header = new JwsHeader(); var header = new JwsHeader();
var payload = new { foo = "bar", n = 42 }; var result = JwsGenerator.TryEncode(rsa, jwk, header, out var message, out var error);
Assert.True(result);
var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage); Assert.NotNull(message);
Assert.True(result, errorMessage); Assert.Null(error);
Assert.NotNull(jws); // Decode protected header
Assert.False(string.IsNullOrEmpty(jws.Protected)); var protectedJson = Base64UrlUtility.DecodeToString(message!.Protected);
Assert.False(string.IsNullOrEmpty(jws.Payload)); Assert.Contains("my-key-id", protectedJson);
Assert.False(string.IsNullOrEmpty(jws.Signature));
} }
[Fact] [Fact]
public void Encode_PostAsGet_ProducesValidJws() { public void TryEncode_JwkWithoutKeyId_SetsHeaderJwk()
var (rsa, jwk) = GenerateRsaAndJwk(); {
using var rsa = RSA.Create(2048);
var jwk = new Jwk { KeyType = JwkKeyType.Rsa.Name };
var header = new JwsHeader(); var header = new JwsHeader();
var result = JwsGenerator.TryEncode(rsa, jwk, header, out var message, out var error);
var result = JwsGenerator.TryEncode(rsa, jwk, header, out var jws, out var errorMessage); Assert.True(result);
Assert.True(result, errorMessage); Assert.NotNull(message);
Assert.NotNull(jws); Assert.Null(error);
Assert.False(string.IsNullOrEmpty(jws.Protected)); var protectedJson = Base64UrlUtility.DecodeToString(message!.Protected);
Assert.Equal(string.Empty, jws.Payload); Assert.Contains("jwk", protectedJson);
Assert.False(string.IsNullOrEmpty(jws.Signature));
} }
[Fact] private class FakeRsaThrows : RSA
public void GetKeyAuthorization_ReturnsExpectedFormat() { {
var (rsa, jwk) = GenerateRsaAndJwk(); public override RSAParameters ExportParameters(bool includePrivateParameters)
var token = "test-token"; => throw new Exception("ExportParameters failed");
public override byte[] Decrypt(byte[] data, RSAEncryptionPadding padding) => throw new NotImplementedException();
var result = JwsGenerator.TryGetKeyAuthorization(jwk, token, out var keyAuth, out var errorMessage); public override byte[] Encrypt(byte[] data, RSAEncryptionPadding padding) => throw new NotImplementedException();
Assert.True(result, errorMessage); public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) => throw new Exception("SignData failed");
Assert.NotNull(keyAuth); public override bool VerifyHash(byte[] hash, byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) => throw new NotImplementedException();
Assert.StartsWith(token + ".", keyAuth); public override void ImportParameters(RSAParameters parameters) => throw new NotImplementedException();
Assert.True(keyAuth.Length > token.Length + 1); protected override void Dispose(bool disposing) { }
} }
} }

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata --> <!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId> <PackageId>MaksIT.Core</PackageId>
<Version>1.5.4</Version> <Version>1.5.7</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product> <Product>MaksIT.Core</Product>

View File

@ -0,0 +1,54 @@
using System.Text;
namespace MaksIT.Core.Security;
/// <summary>
/// Provides RFC 4648-compliant Base64Url encoding and decoding utilities.
/// </summary>
public static class Base64UrlUtility {
/// <summary>
/// Encodes a UTF-8 string to a Base64Url string (RFC 4648 §5).
/// https://tools.ietf.org/html/rfc4648#section-5
/// </summary>
public static string Encode(string value) =>
Encode(Encoding.UTF8.GetBytes(value));
/// <summary>
/// Encodes a byte array to a Base64Url string (RFC 4648 §5).
/// </summary>
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
}
/// <summary>
/// Decodes a Base64Url string to a UTF-8 string.
/// </summary>
public static string DecodeToString(string base64Url) =>
Encoding.UTF8.GetString(Decode(base64Url));
/// <summary>
/// Decodes a Base64Url string to a byte array.
/// </summary>
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);
}
}

View File

@ -1,55 +0,0 @@
using System;
using System.Buffers;
using System.Text;
namespace MaksIT.Core.Security.JWK;
/// <summary>
/// Provides RFC 4648-compliant Base64Url encoding and decoding utilities.
/// </summary>
public static class Base64UrlUtility
{
/// <summary>
/// Encodes a byte array to a Base64Url string (RFC 4648 §5).
/// </summary>
public static string Encode(byte[] data)
{
if (data == null) throw new ArgumentNullException(nameof(data));
string base64 = Convert.ToBase64String(data);
return base64.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
/// <summary>
/// Encodes a UTF-8 string to a Base64Url string (RFC 4648 §5).
/// </summary>
public static string Encode(string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
return Encode(Encoding.UTF8.GetBytes(value));
}
/// <summary>
/// Decodes a Base64Url string to a byte array.
/// </summary>
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);
}
/// <summary>
/// Decodes a Base64Url string to a UTF-8 string.
/// </summary>
public static string DecodeToString(string base64Url)
{
return Encoding.UTF8.GetString(Decode(base64Url));
}
}

View File

@ -3,71 +3,172 @@
namespace MaksIT.Core.Security.JWK; namespace MaksIT.Core.Security.JWK;
/// <summary>
/// Standard JWK class supporting RSA, EC, and octet keys.
/// </summary>
public class Jwk { public class Jwk {
// Common fields #region Common fields
[JsonPropertyName("kty")] /// <summary>
public string? Kty { get; set; } /// "kty" (Key Type) Parameter
/// <para>
/// The "kty" (key type) parameter identifies the cryptographic algorithm
/// family used with the key, such as "RSA" or "EC".
/// </para>
/// </summary>
[JsonPropertyName("kty")]
public string? KeyType { get; set; }
[JsonPropertyName("kid")] /// <summary>
public string? Kid { get; set; } /// "kid" (Key ID) Parameter
/// <para>
/// The "kid" (key ID) parameter is used to match a specific key. This
/// is used, for instance, to choose among a set of keys within a JWK Set
/// during key rollover. The structure of the "kid" value is
/// unspecified.
/// </para>
/// </summary>
[JsonPropertyName("kid")]
public string? KeyId { get; set; }
[JsonPropertyName("alg")] /// <summary>
public string? Alg { get; set; } /// "alg" (Algorithm) Parameter
/// <para>
/// The "alg" (algorithm) parameter identifies the algorithm intended for
/// use with the key.
/// </para>
/// </summary>
[JsonPropertyName("alg")]
public string? Algorithm { get; set; }
[JsonPropertyName("use")] /// <summary>
public string? Use { get; set; } /// "use" (Public Key Use) Parameter
/// <para>
/// The "use" (public key use) parameter identifies the intended use of
/// the public key. The "use" parameter is employed to indicate whether
/// a public key is used for encrypting data or verifying the signature
/// on data.
/// </para>
/// </summary>
[JsonPropertyName("use")]
public string? KeyUse { get; set; }
[JsonPropertyName("key_ops")] /// <summary>
public string[]? KeyOps { get; set; } /// "key_ops" (Key Operations) Parameter
/// <para>
/// The "key_ops" (key operations) parameter identifies the operation(s) for which the key is intended to be used.
/// </para>
/// </summary>
[JsonPropertyName("key_ops")]
public string[]? KeyOperations { get; set; }
#endregion
// RSA fields #region RSA fields
[JsonPropertyName("n")] /// <summary>
public string? N { get; set; } // Modulus /// The modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
/// </summary>
[JsonPropertyName("n")]
public string? RsaModulus { get; set; }
[JsonPropertyName("e")] /// <summary>
public string? E { get; set; } // Exponent /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
/// </summary>
[JsonPropertyName("e")]
public string? RsaExponent { get; set; }
[JsonPropertyName("d")] /// <summary>
public string? D { get; set; } // Private exponent /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("p")]
public string? RsaFirstPrimeFactor { get; set; }
[JsonPropertyName("p")] /// <summary>
public string? P { get; set; } /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("q")]
public string? RsaSecondPrimeFactor { get; set; }
[JsonPropertyName("q")] /// <summary>
public string? Q { get; set; } /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("dp")]
public string? RsaFirstFactorCRTExponent { get; set; }
[JsonPropertyName("dp")] /// <summary>
public string? DP { get; set; } /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("dq")]
public string? RsaSecondFactorCRTExponent { get; set; }
[JsonPropertyName("dq")] /// <summary>
public string? DQ { get; set; } /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("qi")]
public string? RsaFirstCRTCoefficient { get; set; }
[JsonPropertyName("qi")] /// <summary>
public string? QI { get; set; } /// The other primes information, should they exist, null or an empty list if not specified.
/// </summary>
[JsonPropertyName("oth")]
public List<OtherPrimeInfo>? RsaOtherPrimesInfo { get; set; }
#endregion
// EC fields #region EC fields
[JsonPropertyName("crv")] /// <summary>
public string? Crv { get; set; } /// The "crv" (Curve) parameter identifies the cryptographic curve used with the key.
/// </summary>
[JsonPropertyName("crv")]
public string? EcCurve { get; set; }
[JsonPropertyName("x")] /// <summary>
public string? X { get; set; } /// The "x" coordinate for the EC public key. It is represented as the Base64URL encoding of the coordinate's big endian representation.
/// </summary>
[JsonPropertyName("x")]
public string? EcX { get; set; }
[JsonPropertyName("y")] /// <summary>
public string? Y { get; set; } /// The "y" coordinate for the EC public key. It is represented as the Base64URL encoding of the coordinate's big endian representation.
/// </summary>
[JsonPropertyName("y")]
public string? EcY { get; set; }
#endregion
[JsonPropertyName("d_ec")] #region Private Key field
public string? D_EC { get; set; } // EC private key /// <summary>
/// The private key value ("d"). Used for RSA (private exponent) and EC (private key).
/// RFC 7518 uses "d" for both.
/// </summary>
[JsonPropertyName("d")]
public string? PrivateKey { get; set; }
#endregion
// Symmetric (octet) fields #region Symmetric (octet) fields
[JsonPropertyName("k")] /// <summary>
public string? K { get; set; } /// The symmetric (octet) key value. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
// Backward compatibility for old code [JsonPropertyName("k")]
[JsonIgnore] public string? SymmetricKey { get; set; }
public string? Exponent { get => E; set => E = value; } #endregion
[JsonIgnore] }
public string? Modulus { get => N; set => N = value; }
/// <summary>
/// Represents an entry in the 'oth' (Other Primes Info) parameter for multi-prime RSA keys.
/// </summary>
public class OtherPrimeInfo {
#region OtherPrimeInfo fields
/// <summary>
/// The value of the other prime factor.
/// </summary>
[JsonPropertyName("r")]
public string? PrimeFactor { get; set; }
/// <summary>
/// The CRT exponent of the other prime factor.
/// </summary>
[JsonPropertyName("d")]
public string? FactorCRTExponent { get; set; }
/// <summary>
/// The CRT coefficient of the other prime factor.
/// </summary>
[JsonPropertyName("t")]
public string? FactorCRTCoefficient { get; set; }
#endregion
} }

View File

@ -1,213 +1,39 @@
using System.Text; using System.Security.Cryptography;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using MaksIT.Core.Extensions;
namespace MaksIT.Core.Security.JWK; namespace MaksIT.Core.Security.JWK;
/// <summary> /// <summary>
/// Provides utilities for JWK (JSON Web Key) operations, including RFC 7638 thumbprint computation and key generation. /// Provides utilities for JWK (JSON Web Key) operations, including RFC7638 thumbprint computation and key generation.
/// </summary> /// </summary>
public static class JwkGenerator { 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 { try {
jwk = GenerateRsa(keySize, includePrivate, alg, use, keyOps); var publicParameters = rsa.ExportParameters(false);
errorMessage = null;
return true;
} catch (Exception ex) {
jwk = null;
errorMessage = ex.Message;
return 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) { var exp = publicParameters.Exponent;
try { var mod = publicParameters.Modulus;
jwk = GenerateEc(curve, includePrivate, alg, use, keyOps);
errorMessage = null;
return true;
} catch (Exception ex) {
jwk = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryGenerateOct(int keySizeBits, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) { if (exp == null || mod == null)
try { throw new ArgumentException("RSA parameters are missing exponent or modulus.");
jwk = GenerateOct(keySizeBits, alg, use, keyOps);
errorMessage = null;
return true;
} catch (Exception ex) {
jwk = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryGenerateRsaFromRsa(RSA rsa, bool includePrivate, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) { jwk = new Jwk {
try { KeyType = JwkKeyType.Rsa.Name,
jwk = GenerateRsaFromRsa(rsa, includePrivate, alg, use, keyOps); RsaExponent = Base64UrlUtility.Encode(exp),
errorMessage = null; RsaModulus = Base64UrlUtility.Encode(mod),
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.E) || string.IsNullOrEmpty(jwk.N)) {
errorMessage = "JWK must have Exponent and Modulus set.";
return false;
}
try {
// RFC 7638: Lexicographic order: e, kty, n
var orderedJwk = new OrderedJwk {
E = jwk.E!,
Kty = "RSA",
N = jwk.N!
}; };
errorMessage = null;
var json = orderedJwk.ToJson();
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
thumbprint = Base64UrlEncode(hash);
return true; return true;
} }
catch (Exception ex) { catch (Exception ex) {
jwk = null;
errorMessage = ex.Message; errorMessage = ex.Message;
return false; 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 {
Kty = JwkKeyType.Rsa.Name,
N = Base64UrlEncode(parameters.Modulus!),
E = Base64UrlEncode(parameters.Exponent!),
Alg = (alg ?? (keySize >= 4096 ? JwkAlgorithm.Rs512 : JwkAlgorithm.Rs256)).Name,
Use = use,
KeyOps = keyOps,
};
if (includePrivate) {
jwk.D = Base64UrlEncode(parameters.D!);
jwk.P = Base64UrlEncode(parameters.P!);
jwk.Q = Base64UrlEncode(parameters.Q!);
jwk.DP = Base64UrlEncode(parameters.DP!);
jwk.DQ = Base64UrlEncode(parameters.DQ!);
jwk.QI = Base64UrlEncode(parameters.InverseQ!);
}
jwk.Kid = 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 {
Kty = JwkKeyType.Ec.Name,
Crv = curve.Name,
X = Base64UrlEncode(parameters.Q.X!),
Y = Base64UrlEncode(parameters.Q.Y!),
Alg = (alg ?? (curve == JwkCurve.P384 ? JwkAlgorithm.Es384 : curve == JwkCurve.P521 ? JwkAlgorithm.Es512 : JwkAlgorithm.Es256)).Name,
Use = use,
KeyOps = keyOps,
};
if (includePrivate && parameters.D != null) {
jwk.D_EC = Base64UrlEncode(parameters.D);
}
jwk.Kid = 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 {
Kty = JwkKeyType.Oct.Name,
K = Base64UrlEncode(key),
Alg = (alg ?? (keySizeBits == 256 ? JwkAlgorithm.A256Gcm : keySizeBits == 128 ? JwkAlgorithm.A128Gcm : JwkAlgorithm.A512Gcm)).Name,
Use = use,
KeyOps = keyOps,
};
jwk.Kid = 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 {
Kty = JwkKeyType.Rsa.Name,
N = Base64UrlUtility.Encode(parameters.Modulus!),
E = Base64UrlUtility.Encode(parameters.Exponent!),
Alg = (alg ?? JwkAlgorithm.Rs256).Name,
Use = use,
KeyOps = keyOps,
};
if (includePrivate) {
jwk.D = Base64UrlUtility.Encode(parameters.D!);
jwk.P = Base64UrlUtility.Encode(parameters.P!);
jwk.Q = Base64UrlUtility.Encode(parameters.Q!);
jwk.DP = Base64UrlUtility.Encode(parameters.DP!);
jwk.DQ = Base64UrlUtility.Encode(parameters.DQ!);
jwk.QI = Base64UrlUtility.Encode(parameters.InverseQ!);
}
jwk.Kid = 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.Kty == "RSA" && !string.IsNullOrEmpty(jwk.N) && !string.IsNullOrEmpty(jwk.E)) {
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.Kty switch {
"EC" => jwk.X + jwk.Y + jwk.Crv,
"oct" => jwk.K,
_ => 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!;
}
} }

View File

@ -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 {
/// <summary>
/// Returns the key authorization string for ACME challenges.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
}
}

View File

@ -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!;
}

View File

@ -8,110 +8,60 @@ using MaksIT.Core.Extensions;
namespace MaksIT.Core.Security.JWS; namespace MaksIT.Core.Security.JWS;
public static class JwsGenerator { 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<string>(rsa, jwk, protectedHeader, null, out message, out errorMessage);
public static bool TryEncode<T>(
RSA rsa,
Jwk jwk,
JwsHeader protectedHeader,
T? payload,
out JwsMessage? message,
[NotNullWhen(false)] out string? errorMessage
) {
try { 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; errorMessage = null;
return true; return true;
} catch (Exception ex) { }
jwsMessage = null; catch (Exception ex) {
message = null;
errorMessage = ex.Message; errorMessage = ex.Message;
return false; 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<T>(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<T>(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;
}
} }