(feature): jwk std impementation and props naming

This commit is contained in:
Maksym Sadovnychyy 2025-11-12 13:03:28 +01:00
parent e619cf276c
commit 8100c206bc
5 changed files with 256 additions and 142 deletions

View File

@ -10,16 +10,16 @@ public class JwkGeneratorTests
var result = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var errorMessage); var result = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage); Assert.True(result, errorMessage);
Assert.NotNull(jwk); Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Rsa.Name, jwk.Kty); Assert.Equal(JwkKeyType.Rsa.Name, jwk.KeyType);
Assert.NotNull(jwk.N); Assert.NotNull(jwk.RsaModulus);
Assert.NotNull(jwk.E); Assert.NotNull(jwk.RsaExponent);
Assert.Null(jwk.D); Assert.Null(jwk.PrivateKey);
Assert.Null(jwk.P); Assert.Null(jwk.RsaFirstPrimeFactor);
Assert.Null(jwk.Q); Assert.Null(jwk.RsaSecondPrimeFactor);
Assert.Null(jwk.DP); Assert.Null(jwk.RsaFirstFactorCRTExponent);
Assert.Null(jwk.DQ); Assert.Null(jwk.RsaSecondFactorCRTExponent);
Assert.Null(jwk.QI); Assert.Null(jwk.RsaFirstCRTCoefficient);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid)); Assert.False(string.IsNullOrWhiteSpace(jwk.KeyId));
} }
[Fact] [Fact]
@ -28,13 +28,13 @@ public class JwkGeneratorTests
var result = JwkGenerator.TryGenerateRsa(2048, true, null, null, null, out var jwk, out var errorMessage); var result = JwkGenerator.TryGenerateRsa(2048, true, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage); Assert.True(result, errorMessage);
Assert.NotNull(jwk); Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Rsa.Name, jwk.Kty); Assert.Equal(JwkKeyType.Rsa.Name, jwk.KeyType);
Assert.NotNull(jwk.D); Assert.NotNull(jwk.PrivateKey);
Assert.NotNull(jwk.P); Assert.NotNull(jwk.RsaFirstPrimeFactor);
Assert.NotNull(jwk.Q); Assert.NotNull(jwk.RsaSecondPrimeFactor);
Assert.NotNull(jwk.DP); Assert.NotNull(jwk.RsaFirstFactorCRTExponent);
Assert.NotNull(jwk.DQ); Assert.NotNull(jwk.RsaSecondFactorCRTExponent);
Assert.NotNull(jwk.QI); Assert.NotNull(jwk.RsaFirstCRTCoefficient);
} }
[Theory] [Theory]
@ -47,12 +47,12 @@ public class JwkGeneratorTests
var result = JwkGenerator.TryGenerateEc(curveObj, false, null, null, null, out var jwk, out var errorMessage); var result = JwkGenerator.TryGenerateEc(curveObj, false, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage); Assert.True(result, errorMessage);
Assert.NotNull(jwk); Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Ec.Name, jwk.Kty); Assert.Equal(JwkKeyType.Ec.Name, jwk.KeyType);
Assert.Equal(curve, jwk.Crv); Assert.Equal(curve, jwk.EcCurve);
Assert.NotNull(jwk.X); Assert.NotNull(jwk.EcX);
Assert.NotNull(jwk.Y); Assert.NotNull(jwk.EcY);
Assert.Null(jwk.D_EC); Assert.Null(jwk.PrivateKey);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid)); Assert.False(string.IsNullOrWhiteSpace(jwk.KeyId));
} }
[Theory] [Theory]
@ -65,9 +65,9 @@ public class JwkGeneratorTests
var result = JwkGenerator.TryGenerateEc(curveObj, true, null, null, null, out var jwk, out var errorMessage); var result = JwkGenerator.TryGenerateEc(curveObj, true, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage); Assert.True(result, errorMessage);
Assert.NotNull(jwk); Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Ec.Name, jwk.Kty); Assert.Equal(JwkKeyType.Ec.Name, jwk.KeyType);
Assert.Equal(curve, jwk.Crv); Assert.Equal(curve, jwk.EcCurve);
Assert.NotNull(jwk.D_EC); Assert.NotNull(jwk.PrivateKey);
} }
[Fact] [Fact]
@ -76,9 +76,9 @@ public class JwkGeneratorTests
var result = JwkGenerator.TryGenerateOct(256, null, null, null, out var jwk, out var errorMessage); var result = JwkGenerator.TryGenerateOct(256, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage); Assert.True(result, errorMessage);
Assert.NotNull(jwk); Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Oct.Name, jwk.Kty); Assert.Equal(JwkKeyType.Oct.Name, jwk.KeyType);
Assert.NotNull(jwk.K); Assert.NotNull(jwk.SymmetricKey);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid)); Assert.False(string.IsNullOrWhiteSpace(jwk.KeyId));
} }
[Fact] [Fact]
@ -102,6 +102,6 @@ public class JwkGeneratorTests
Assert.True(result2, errorMessage2); Assert.True(result2, errorMessage2);
Assert.NotNull(jwk1); Assert.NotNull(jwk1);
Assert.NotNull(jwk2); Assert.NotNull(jwk2);
Assert.NotEqual(jwk1.Kid, jwk2.Kid); Assert.NotEqual(jwk1.KeyId, jwk2.KeyId);
} }
} }

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.6</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

@ -1,7 +1,6 @@
using System;
using System.Buffers;
using System.Text; using System.Text;
namespace MaksIT.Core.Security.JWK; namespace MaksIT.Core.Security.JWK;
/// <summary> /// <summary>
@ -14,8 +13,11 @@ public static class Base64UrlUtility
/// </summary> /// </summary>
public static string Encode(byte[] data) public static string Encode(byte[] data)
{ {
if (data == null) throw new ArgumentNullException(nameof(data)); if (data == null)
throw new ArgumentNullException(nameof(data));
string base64 = Convert.ToBase64String(data); string base64 = Convert.ToBase64String(data);
return base64.TrimEnd('=') return base64.TrimEnd('=')
.Replace('+', '-') .Replace('+', '-')
.Replace('/', '_'); .Replace('/', '_');
@ -26,7 +28,9 @@ public static class Base64UrlUtility
/// </summary> /// </summary>
public static string Encode(string value) public static string Encode(string value)
{ {
if (value == null) throw new ArgumentNullException(nameof(value)); if (value == null)
throw new ArgumentNullException(nameof(value));
return Encode(Encoding.UTF8.GetBytes(value)); return Encode(Encoding.UTF8.GetBytes(value));
} }
@ -35,13 +39,17 @@ public static class Base64UrlUtility
/// </summary> /// </summary>
public static byte[] Decode(string base64Url) public static byte[] Decode(string base64Url)
{ {
if (base64Url == null) throw new ArgumentNullException(nameof(base64Url)); if (base64Url == null)
throw new ArgumentNullException(nameof(base64Url));
string padded = base64Url.Replace('-', '+').Replace('_', '/'); string padded = base64Url.Replace('-', '+').Replace('_', '/');
switch (base64Url.Length % 4) switch (base64Url.Length % 4)
{ {
case 2: padded += "=="; break; case 2: padded += "=="; break;
case 3: padded += "="; break; case 3: padded += "="; break;
} }
return Convert.FromBase64String(padded); return Convert.FromBase64String(padded);
} }

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
/// <summary>
/// "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")] [JsonPropertyName("kty")]
public string? Kty { get; set; } public string? KeyType { get; set; }
/// <summary>
/// "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")] [JsonPropertyName("kid")]
public string? Kid { get; set; } public string? KeyId { get; set; }
/// <summary>
/// "alg" (Algorithm) Parameter
/// <para>
/// The "alg" (algorithm) parameter identifies the algorithm intended for
/// use with the key.
/// </para>
/// </summary>
[JsonPropertyName("alg")] [JsonPropertyName("alg")]
public string? Alg { get; set; } public string? Algorithm { get; set; }
/// <summary>
/// "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")] [JsonPropertyName("use")]
public string? Use { get; set; } public string? KeyUse { get; set; }
/// <summary>
/// "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")] [JsonPropertyName("key_ops")]
public string[]? KeyOps { get; set; } public string[]? KeyOperations { get; set; }
#endregion
// RSA fields #region RSA fields
/// <summary>
/// The modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
/// </summary>
[JsonPropertyName("n")] [JsonPropertyName("n")]
public string? N { get; set; } // Modulus public string? RsaModulus { get; set; }
/// <summary>
/// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
/// </summary>
[JsonPropertyName("e")] [JsonPropertyName("e")]
public string? E { get; set; } // Exponent public string? RsaExponent { get; set; }
[JsonPropertyName("d")]
public string? D { get; set; } // Private exponent
/// <summary>
/// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("p")] [JsonPropertyName("p")]
public string? P { get; set; } public string? RsaFirstPrimeFactor { get; set; }
/// <summary>
/// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("q")] [JsonPropertyName("q")]
public string? Q { get; set; } public string? RsaSecondPrimeFactor { get; set; }
/// <summary>
/// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("dp")] [JsonPropertyName("dp")]
public string? DP { get; set; } public string? RsaFirstFactorCRTExponent { get; set; }
/// <summary>
/// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("dq")] [JsonPropertyName("dq")]
public string? DQ { get; set; } public string? RsaSecondFactorCRTExponent { get; set; }
/// <summary>
/// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("qi")] [JsonPropertyName("qi")]
public string? QI { get; set; } public string? RsaFirstCRTCoefficient { get; set; }
// EC fields /// <summary>
/// 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
#region EC fields
/// <summary>
/// The "crv" (Curve) parameter identifies the cryptographic curve used with the key.
/// </summary>
[JsonPropertyName("crv")] [JsonPropertyName("crv")]
public string? Crv { get; set; } public string? EcCurve { get; set; }
/// <summary>
/// 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")] [JsonPropertyName("x")]
public string? X { get; set; } public string? EcX { get; set; }
/// <summary>
/// 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")] [JsonPropertyName("y")]
public string? Y { get; set; } 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
/// <summary>
/// The symmetric (octet) key value. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary>
[JsonPropertyName("k")] [JsonPropertyName("k")]
public string? K { get; set; } public string? SymmetricKey { get; set; }
#endregion
// Backward compatibility for old code }
[JsonIgnore]
public string? Exponent { get => E; set => E = value; } /// <summary>
[JsonIgnore] /// Represents an entry in the 'oth' (Other Primes Info) parameter for multi-prime RSA keys.
public string? Modulus { get => N; set => N = value; } /// </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

@ -4,10 +4,11 @@ using System.Text.Json.Serialization;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using MaksIT.Core.Extensions; 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 TryGenerateRsa(int keySize, bool includePrivate, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) {
@ -15,7 +16,8 @@ public static class JwkGenerator {
jwk = GenerateRsa(keySize, includePrivate, alg, use, keyOps); jwk = GenerateRsa(keySize, includePrivate, alg, use, keyOps);
errorMessage = null; errorMessage = null;
return true; return true;
} catch (Exception ex) { }
catch (Exception ex) {
jwk = null; jwk = null;
errorMessage = ex.Message; errorMessage = ex.Message;
return false; return false;
@ -27,7 +29,8 @@ public static class JwkGenerator {
jwk = GenerateEc(curve, includePrivate, alg, use, keyOps); jwk = GenerateEc(curve, includePrivate, alg, use, keyOps);
errorMessage = null; errorMessage = null;
return true; return true;
} catch (Exception ex) { }
catch (Exception ex) {
jwk = null; jwk = null;
errorMessage = ex.Message; errorMessage = ex.Message;
return false; return false;
@ -39,7 +42,8 @@ public static class JwkGenerator {
jwk = GenerateOct(keySizeBits, alg, use, keyOps); jwk = GenerateOct(keySizeBits, alg, use, keyOps);
errorMessage = null; errorMessage = null;
return true; return true;
} catch (Exception ex) { }
catch (Exception ex) {
jwk = null; jwk = null;
errorMessage = ex.Message; errorMessage = ex.Message;
return false; return false;
@ -51,7 +55,8 @@ public static class JwkGenerator {
jwk = GenerateRsaFromRsa(rsa, includePrivate, alg, use, keyOps); jwk = GenerateRsaFromRsa(rsa, includePrivate, alg, use, keyOps);
errorMessage = null; errorMessage = null;
return true; return true;
} catch (Exception ex) { }
catch (Exception ex) {
jwk = null; jwk = null;
errorMessage = ex.Message; errorMessage = ex.Message;
return false; return false;
@ -69,17 +74,17 @@ public static class JwkGenerator {
errorMessage = "JWK cannot be null."; errorMessage = "JWK cannot be null.";
return false; return false;
} }
if (string.IsNullOrEmpty(jwk.E) || string.IsNullOrEmpty(jwk.N)) { if (string.IsNullOrEmpty(jwk.RsaExponent) || string.IsNullOrEmpty(jwk.RsaModulus)) {
errorMessage = "JWK must have Exponent and Modulus set."; errorMessage = "JWK must have RsaExponent and RsaModulus set.";
return false; return false;
} }
try { try {
// RFC 7638: Lexicographic order: e, kty, n // RFC7638: Lexicographic order: e, kty, n
var orderedJwk = new OrderedJwk { var orderedJwk = new OrderedJwk {
E = jwk.E!, E = jwk.RsaExponent!,
Kty = "RSA", Kty = "RSA",
N = jwk.N! N = jwk.RsaModulus!
}; };
var json = orderedJwk.ToJson(); var json = orderedJwk.ToJson();
@ -97,22 +102,22 @@ public static class JwkGenerator {
using var rsa = RSA.Create(keySize); using var rsa = RSA.Create(keySize);
var parameters = rsa.ExportParameters(includePrivate); var parameters = rsa.ExportParameters(includePrivate);
var jwk = new Jwk { var jwk = new Jwk {
Kty = JwkKeyType.Rsa.Name, KeyType = JwkKeyType.Rsa.Name,
N = Base64UrlEncode(parameters.Modulus!), RsaModulus = Base64UrlEncode(parameters.Modulus!),
E = Base64UrlEncode(parameters.Exponent!), RsaExponent = Base64UrlEncode(parameters.Exponent!),
Alg = (alg ?? (keySize >= 4096 ? JwkAlgorithm.Rs512 : JwkAlgorithm.Rs256)).Name, Algorithm = (alg ?? (keySize >= 4096 ? JwkAlgorithm.Rs512 : JwkAlgorithm.Rs256)).Name,
Use = use, KeyUse = use,
KeyOps = keyOps, KeyOperations = keyOps,
}; };
if (includePrivate) { if (includePrivate) {
jwk.D = Base64UrlEncode(parameters.D!); jwk.PrivateKey = Base64UrlEncode(parameters.D!);
jwk.P = Base64UrlEncode(parameters.P!); jwk.RsaFirstPrimeFactor = Base64UrlEncode(parameters.P!);
jwk.Q = Base64UrlEncode(parameters.Q!); jwk.RsaSecondPrimeFactor = Base64UrlEncode(parameters.Q!);
jwk.DP = Base64UrlEncode(parameters.DP!); jwk.RsaFirstFactorCRTExponent = Base64UrlEncode(parameters.DP!);
jwk.DQ = Base64UrlEncode(parameters.DQ!); jwk.RsaSecondFactorCRTExponent = Base64UrlEncode(parameters.DQ!);
jwk.QI = Base64UrlEncode(parameters.InverseQ!); jwk.RsaFirstCRTCoefficient = Base64UrlEncode(parameters.InverseQ!);
} }
jwk.Kid = ComputeKid(jwk); jwk.KeyId = ComputeKid(jwk);
return jwk; return jwk;
} }
@ -127,31 +132,31 @@ public static class JwkGenerator {
using var ec = ECDsa.Create(ecCurve); using var ec = ECDsa.Create(ecCurve);
var parameters = ec.ExportParameters(includePrivate); var parameters = ec.ExportParameters(includePrivate);
var jwk = new Jwk { var jwk = new Jwk {
Kty = JwkKeyType.Ec.Name, KeyType = JwkKeyType.Ec.Name,
Crv = curve.Name, EcCurve = curve.Name,
X = Base64UrlEncode(parameters.Q.X!), EcX = Base64UrlEncode(parameters.Q.X!),
Y = Base64UrlEncode(parameters.Q.Y!), EcY = Base64UrlEncode(parameters.Q.Y!),
Alg = (alg ?? (curve == JwkCurve.P384 ? JwkAlgorithm.Es384 : curve == JwkCurve.P521 ? JwkAlgorithm.Es512 : JwkAlgorithm.Es256)).Name, Algorithm = (alg ?? (curve == JwkCurve.P384 ? JwkAlgorithm.Es384 : curve == JwkCurve.P521 ? JwkAlgorithm.Es512 : JwkAlgorithm.Es256)).Name,
Use = use, KeyUse = use,
KeyOps = keyOps, KeyOperations = keyOps,
}; };
if (includePrivate && parameters.D != null) { if (includePrivate && parameters.D != null) {
jwk.D_EC = Base64UrlEncode(parameters.D); jwk.PrivateKey = Base64UrlEncode(parameters.D);
} }
jwk.Kid = ComputeKid(jwk); jwk.KeyId = ComputeKid(jwk);
return jwk; return jwk;
} }
private static Jwk GenerateOct(int keySizeBits = 256, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) { private static Jwk GenerateOct(int keySizeBits = 256, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) {
var key = RandomNumberGenerator.GetBytes(keySizeBits / 8); var key = RandomNumberGenerator.GetBytes(keySizeBits / 8);
var jwk = new Jwk { var jwk = new Jwk {
Kty = JwkKeyType.Oct.Name, KeyType = JwkKeyType.Oct.Name,
K = Base64UrlEncode(key), SymmetricKey = Base64UrlEncode(key),
Alg = (alg ?? (keySizeBits == 256 ? JwkAlgorithm.A256Gcm : keySizeBits == 128 ? JwkAlgorithm.A128Gcm : JwkAlgorithm.A512Gcm)).Name, Algorithm = (alg ?? (keySizeBits == 256 ? JwkAlgorithm.A256Gcm : keySizeBits == 128 ? JwkAlgorithm.A128Gcm : JwkAlgorithm.A512Gcm)).Name,
Use = use, KeyUse = use,
KeyOps = keyOps, KeyOperations = keyOps,
}; };
jwk.Kid = ComputeKid(jwk); jwk.KeyId = ComputeKid(jwk);
return jwk; return jwk;
} }
@ -159,22 +164,22 @@ public static class JwkGenerator {
if (rsa == null) throw new ArgumentNullException(nameof(rsa)); if (rsa == null) throw new ArgumentNullException(nameof(rsa));
var parameters = rsa.ExportParameters(includePrivate); var parameters = rsa.ExportParameters(includePrivate);
var jwk = new Jwk { var jwk = new Jwk {
Kty = JwkKeyType.Rsa.Name, KeyType = JwkKeyType.Rsa.Name,
N = Base64UrlUtility.Encode(parameters.Modulus!), RsaModulus = Base64UrlUtility.Encode(parameters.Modulus!),
E = Base64UrlUtility.Encode(parameters.Exponent!), RsaExponent = Base64UrlUtility.Encode(parameters.Exponent!),
Alg = (alg ?? JwkAlgorithm.Rs256).Name, Algorithm = (alg ?? JwkAlgorithm.Rs256).Name,
Use = use, KeyUse = use,
KeyOps = keyOps, KeyOperations = keyOps,
}; };
if (includePrivate) { if (includePrivate) {
jwk.D = Base64UrlUtility.Encode(parameters.D!); jwk.PrivateKey = Base64UrlUtility.Encode(parameters.D!);
jwk.P = Base64UrlUtility.Encode(parameters.P!); jwk.RsaFirstPrimeFactor = Base64UrlUtility.Encode(parameters.P!);
jwk.Q = Base64UrlUtility.Encode(parameters.Q!); jwk.RsaSecondPrimeFactor = Base64UrlUtility.Encode(parameters.Q!);
jwk.DP = Base64UrlUtility.Encode(parameters.DP!); jwk.RsaFirstFactorCRTExponent = Base64UrlUtility.Encode(parameters.DP!);
jwk.DQ = Base64UrlUtility.Encode(parameters.DQ!); jwk.RsaSecondFactorCRTExponent = Base64UrlUtility.Encode(parameters.DQ!);
jwk.QI = Base64UrlUtility.Encode(parameters.InverseQ!); jwk.RsaFirstCRTCoefficient = Base64UrlUtility.Encode(parameters.InverseQ!);
} }
jwk.Kid = ComputeKid(jwk); jwk.KeyId = ComputeKid(jwk);
return jwk; return jwk;
} }
@ -184,15 +189,15 @@ public static class JwkGenerator {
private static string ComputeKid(Jwk jwk) { private static string ComputeKid(Jwk jwk) {
// Use thumbprint as kid if possible // Use thumbprint as kid if possible
if (jwk.Kty == "RSA" && !string.IsNullOrEmpty(jwk.N) && !string.IsNullOrEmpty(jwk.E)) { if (jwk.KeyType == "RSA" && !string.IsNullOrEmpty(jwk.RsaModulus) && !string.IsNullOrEmpty(jwk.RsaExponent)) {
TryComputeThumbprint(jwk, out var thumb, out _); TryComputeThumbprint(jwk, out var thumb, out _);
return thumb ?? Guid.NewGuid().ToString("N"); return thumb ?? Guid.NewGuid().ToString("N");
} }
// For EC and oct, use a hash of the key material // For EC and oct, use a hash of the key material
using var sha = SHA256.Create(); using var sha = SHA256.Create();
string keyMaterial = jwk.Kty switch { string keyMaterial = jwk.KeyType switch {
"EC" => jwk.X + jwk.Y + jwk.Crv, "EC" => jwk.EcX + jwk.EcY + jwk.EcCurve,
"oct" => jwk.K, "oct" => jwk.SymmetricKey,
_ => null _ => null
} ?? Guid.NewGuid().ToString(); } ?? Guid.NewGuid().ToString();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(keyMaterial)); var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(keyMaterial));