From 8100c206bcd8270b3414e82249b7a0211fa226fc Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Wed, 12 Nov 2025 13:03:28 +0100 Subject: [PATCH] (feature): jwk std impementation and props naming --- .../Security/JwkGeneratorTests.cs | 60 +++--- src/MaksIT.Core/MaksIT.Core.csproj | 2 +- .../Security/JWK/Base64UrlUtility.cs | 18 +- src/MaksIT.Core/Security/JWK/Jwk.cs | 197 +++++++++++++----- src/MaksIT.Core/Security/JWK/JwkGenerator.cs | 121 +++++------ 5 files changed, 256 insertions(+), 142 deletions(-) diff --git a/src/MaksIT.Core.Tests/Security/JwkGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JwkGeneratorTests.cs index bf10eeb..d336ced 100644 --- a/src/MaksIT.Core.Tests/Security/JwkGeneratorTests.cs +++ b/src/MaksIT.Core.Tests/Security/JwkGeneratorTests.cs @@ -10,16 +10,16 @@ public class JwkGeneratorTests 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)); + 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] @@ -28,13 +28,13 @@ public class JwkGeneratorTests 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); + 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] @@ -47,12 +47,12 @@ public class JwkGeneratorTests 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)); + 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] @@ -65,9 +65,9 @@ public class JwkGeneratorTests 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); + Assert.Equal(JwkKeyType.Ec.Name, jwk.KeyType); + Assert.Equal(curve, jwk.EcCurve); + Assert.NotNull(jwk.PrivateKey); } [Fact] @@ -76,9 +76,9 @@ public class JwkGeneratorTests 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)); + Assert.Equal(JwkKeyType.Oct.Name, jwk.KeyType); + Assert.NotNull(jwk.SymmetricKey); + Assert.False(string.IsNullOrWhiteSpace(jwk.KeyId)); } [Fact] @@ -102,6 +102,6 @@ public class JwkGeneratorTests Assert.True(result2, errorMessage2); Assert.NotNull(jwk1); Assert.NotNull(jwk2); - Assert.NotEqual(jwk1.Kid, jwk2.Kid); + Assert.NotEqual(jwk1.KeyId, jwk2.KeyId); } } diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index ee16bd0..416491f 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.5.4 + 1.5.6 Maksym Sadovnychyy MAKS-IT MaksIT.Core diff --git a/src/MaksIT.Core/Security/JWK/Base64UrlUtility.cs b/src/MaksIT.Core/Security/JWK/Base64UrlUtility.cs index e4ce427..10e5551 100644 --- a/src/MaksIT.Core/Security/JWK/Base64UrlUtility.cs +++ b/src/MaksIT.Core/Security/JWK/Base64UrlUtility.cs @@ -1,7 +1,6 @@ -using System; -using System.Buffers; using System.Text; + namespace MaksIT.Core.Security.JWK; /// @@ -14,8 +13,11 @@ public static class Base64UrlUtility /// 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); + return base64.TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); @@ -26,7 +28,9 @@ public static class Base64UrlUtility /// 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)); } @@ -35,13 +39,17 @@ public static class Base64UrlUtility /// 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('_', '/'); + switch (base64Url.Length % 4) { case 2: padded += "=="; break; case 3: padded += "="; break; } + return Convert.FromBase64String(padded); } diff --git a/src/MaksIT.Core/Security/JWK/Jwk.cs b/src/MaksIT.Core/Security/JWK/Jwk.cs index 97bc897..b9920a9 100644 --- a/src/MaksIT.Core/Security/JWK/Jwk.cs +++ b/src/MaksIT.Core/Security/JWK/Jwk.cs @@ -3,71 +3,172 @@ namespace MaksIT.Core.Security.JWK; -/// -/// Standard JWK class supporting RSA, EC, and octet keys. -/// + public class Jwk { - // Common fields - [JsonPropertyName("kty")] - public string? Kty { get; set; } + #region Common fields + /// + /// "kty" (Key Type) Parameter + /// + /// The "kty" (key type) parameter identifies the cryptographic algorithm + /// family used with the key, such as "RSA" or "EC". + /// + /// + [JsonPropertyName("kty")] + public string? KeyType { get; set; } - [JsonPropertyName("kid")] - public string? Kid { get; set; } + /// + /// "kid" (Key ID) Parameter + /// + /// 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. + /// + /// + [JsonPropertyName("kid")] + public string? KeyId { get; set; } - [JsonPropertyName("alg")] - public string? Alg { get; set; } + /// + /// "alg" (Algorithm) Parameter + /// + /// The "alg" (algorithm) parameter identifies the algorithm intended for + /// use with the key. + /// + /// + [JsonPropertyName("alg")] + public string? Algorithm { get; set; } - [JsonPropertyName("use")] - public string? Use { get; set; } + /// + /// "use" (Public Key Use) Parameter + /// + /// 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. + /// + /// + [JsonPropertyName("use")] + public string? KeyUse { get; set; } - [JsonPropertyName("key_ops")] - public string[]? KeyOps { get; set; } + /// + /// "key_ops" (Key Operations) Parameter + /// + /// The "key_ops" (key operations) parameter identifies the operation(s) for which the key is intended to be used. + /// + /// + [JsonPropertyName("key_ops")] + public string[]? KeyOperations { get; set; } + #endregion - // RSA fields - [JsonPropertyName("n")] - public string? N { get; set; } // Modulus + #region RSA fields + /// + /// The modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonPropertyName("n")] + public string? RsaModulus { get; set; } - [JsonPropertyName("e")] - 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. + /// + [JsonPropertyName("e")] + public string? RsaExponent { get; set; } - [JsonPropertyName("d")] - 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. + /// + [JsonPropertyName("p")] + public string? RsaFirstPrimeFactor { get; set; } - [JsonPropertyName("p")] - public string? P { get; set; } + /// + /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("q")] + public string? RsaSecondPrimeFactor { get; set; } - [JsonPropertyName("q")] - 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. + /// + [JsonPropertyName("dp")] + public string? RsaFirstFactorCRTExponent { get; set; } - [JsonPropertyName("dp")] - 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. + /// + [JsonPropertyName("dq")] + public string? RsaSecondFactorCRTExponent { get; set; } - [JsonPropertyName("dq")] - 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. + /// + [JsonPropertyName("qi")] + public string? RsaFirstCRTCoefficient { get; set; } - [JsonPropertyName("qi")] - public string? QI { get; set; } + /// + /// The other primes information, should they exist, null or an empty list if not specified. + /// + [JsonPropertyName("oth")] + public List? RsaOtherPrimesInfo { get; set; } + #endregion - // EC fields - [JsonPropertyName("crv")] - public string? Crv { get; set; } + #region EC fields + /// + /// The "crv" (Curve) parameter identifies the cryptographic curve used with the key. + /// + [JsonPropertyName("crv")] + public string? EcCurve { get; set; } - [JsonPropertyName("x")] - 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. + /// + [JsonPropertyName("x")] + public string? EcX { get; set; } - [JsonPropertyName("y")] - 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. + /// + [JsonPropertyName("y")] + public string? EcY { get; set; } + #endregion - [JsonPropertyName("d_ec")] - public string? D_EC { get; set; } // EC private key + #region Private Key field + /// + /// The private key value ("d"). Used for RSA (private exponent) and EC (private key). + /// RFC 7518 uses "d" for both. + /// + [JsonPropertyName("d")] + public string? PrivateKey { get; set; } + #endregion - // Symmetric (octet) fields - [JsonPropertyName("k")] - public string? K { get; set; } + #region Symmetric (octet) fields + /// + /// The symmetric (octet) key value. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("k")] + public string? SymmetricKey { get; set; } + #endregion +} - // Backward compatibility for old code - [JsonIgnore] - public string? Exponent { get => E; set => E = value; } - [JsonIgnore] - public string? Modulus { get => N; set => N = value; } +/// +/// Represents an entry in the 'oth' (Other Primes Info) parameter for multi-prime RSA keys. +/// +public class OtherPrimeInfo { + #region OtherPrimeInfo fields + /// + /// The value of the other prime factor. + /// + [JsonPropertyName("r")] + public string? PrimeFactor { get; set; } + + /// + /// The CRT exponent of the other prime factor. + /// + [JsonPropertyName("d")] + public string? FactorCRTExponent { get; set; } + + /// + /// The CRT coefficient of the other prime factor. + /// + [JsonPropertyName("t")] + public string? FactorCRTCoefficient { get; set; } + #endregion } \ No newline at end of file diff --git a/src/MaksIT.Core/Security/JWK/JwkGenerator.cs b/src/MaksIT.Core/Security/JWK/JwkGenerator.cs index b842e05..caa3119 100644 --- a/src/MaksIT.Core/Security/JWK/JwkGenerator.cs +++ b/src/MaksIT.Core/Security/JWK/JwkGenerator.cs @@ -4,10 +4,11 @@ using System.Text.Json.Serialization; using System.Diagnostics.CodeAnalysis; using MaksIT.Core.Extensions; + namespace MaksIT.Core.Security.JWK; /// -/// 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. /// 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) { @@ -15,7 +16,8 @@ public static class JwkGenerator { jwk = GenerateRsa(keySize, includePrivate, alg, use, keyOps); errorMessage = null; return true; - } catch (Exception ex) { + } + catch (Exception ex) { jwk = null; errorMessage = ex.Message; return false; @@ -27,7 +29,8 @@ public static class JwkGenerator { jwk = GenerateEc(curve, includePrivate, alg, use, keyOps); errorMessage = null; return true; - } catch (Exception ex) { + } + catch (Exception ex) { jwk = null; errorMessage = ex.Message; return false; @@ -39,7 +42,8 @@ public static class JwkGenerator { jwk = GenerateOct(keySizeBits, alg, use, keyOps); errorMessage = null; return true; - } catch (Exception ex) { + } + catch (Exception ex) { jwk = null; errorMessage = ex.Message; return false; @@ -51,7 +55,8 @@ public static class JwkGenerator { jwk = GenerateRsaFromRsa(rsa, includePrivate, alg, use, keyOps); errorMessage = null; return true; - } catch (Exception ex) { + } + catch (Exception ex) { jwk = null; errorMessage = ex.Message; return false; @@ -59,9 +64,9 @@ public static class JwkGenerator { } public static bool TryComputeThumbprint( - Jwk jwk, - [NotNullWhen(true)] out string? thumbprint, - [NotNullWhen(false)] out string? errorMessage) { + Jwk jwk, + [NotNullWhen(true)] out string? thumbprint, + [NotNullWhen(false)] out string? errorMessage) { thumbprint = null; errorMessage = null; @@ -69,17 +74,17 @@ public static class JwkGenerator { errorMessage = "JWK cannot be null."; return false; } - if (string.IsNullOrEmpty(jwk.E) || string.IsNullOrEmpty(jwk.N)) { - errorMessage = "JWK must have Exponent and Modulus set."; + if (string.IsNullOrEmpty(jwk.RsaExponent) || string.IsNullOrEmpty(jwk.RsaModulus)) { + errorMessage = "JWK must have RsaExponent and RsaModulus set."; return false; } try { - // RFC 7638: Lexicographic order: e, kty, n + // RFC7638: Lexicographic order: e, kty, n var orderedJwk = new OrderedJwk { - E = jwk.E!, + E = jwk.RsaExponent!, Kty = "RSA", - N = jwk.N! + N = jwk.RsaModulus! }; var json = orderedJwk.ToJson(); @@ -97,22 +102,22 @@ public static class JwkGenerator { 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, + 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.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.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.Kid = ComputeKid(jwk); + jwk.KeyId = ComputeKid(jwk); return jwk; } @@ -127,31 +132,31 @@ public static class JwkGenerator { 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, + 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.D_EC = Base64UrlEncode(parameters.D); + jwk.PrivateKey = Base64UrlEncode(parameters.D); } - jwk.Kid = ComputeKid(jwk); + 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 { - Kty = JwkKeyType.Oct.Name, - K = Base64UrlEncode(key), - Alg = (alg ?? (keySizeBits == 256 ? JwkAlgorithm.A256Gcm : keySizeBits == 128 ? JwkAlgorithm.A128Gcm : JwkAlgorithm.A512Gcm)).Name, - Use = use, - KeyOps = keyOps, + 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.Kid = ComputeKid(jwk); + jwk.KeyId = ComputeKid(jwk); return jwk; } @@ -159,22 +164,22 @@ public static class JwkGenerator { 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, + 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.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.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.Kid = ComputeKid(jwk); + jwk.KeyId = ComputeKid(jwk); return jwk; } @@ -184,15 +189,15 @@ public static class JwkGenerator { private static string ComputeKid(Jwk jwk) { // 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 _); 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, + 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));