| | | 1 | | using System.Security.Cryptography; |
| | | 2 | | using System.Security.Cryptography.X509Certificates; |
| | | 3 | | using System.Text; |
| | | 4 | | |
| | | 5 | | namespace Kestrun.Jwt; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Utilities for working with JSON Web Keys (JWK), including RFC 7638 thumbprints. |
| | | 9 | | /// </summary> |
| | | 10 | | public static class JwkUtilities |
| | | 11 | | { |
| | | 12 | | /// <summary> |
| | | 13 | | /// Computes the RFC 7638 JWK thumbprint for an RSA public key given its Base64Url-encoded parameters. |
| | | 14 | | /// </summary> |
| | | 15 | | /// <param name="nBase64Url">The Base64Url-encoded RSA modulus (n).</param> |
| | | 16 | | /// <param name="eBase64Url">The Base64Url-encoded RSA public exponent (e).</param> |
| | | 17 | | /// <returns>The Base64Url-encoded SHA-256 hash of the canonical JWK representation.</returns> |
| | | 18 | | public static string ComputeThumbprintRsa(string nBase64Url, string eBase64Url) |
| | | 19 | | { |
| | 16 | 20 | | if (string.IsNullOrWhiteSpace(nBase64Url)) |
| | | 21 | | { |
| | 3 | 22 | | throw new ArgumentException("Value cannot be null or empty.", nameof(nBase64Url)); |
| | | 23 | | } |
| | | 24 | | |
| | 13 | 25 | | if (string.IsNullOrWhiteSpace(eBase64Url)) |
| | | 26 | | { |
| | 3 | 27 | | throw new ArgumentException("Value cannot be null or empty.", nameof(eBase64Url)); |
| | | 28 | | } |
| | | 29 | | |
| | | 30 | | // Canonical JWK member order per RFC 7638 for RSA: e, kty, n |
| | 10 | 31 | | var canonicalJwk = "{\"e\":\"" + eBase64Url + "\",\"kty\":\"RSA\",\"n\":\"" + nBase64Url + "\"}"; |
| | 10 | 32 | | var bytes = Encoding.UTF8.GetBytes(canonicalJwk); |
| | 10 | 33 | | var hash = SHA256.HashData(bytes); |
| | 10 | 34 | | return Base64UrlEncode(hash); |
| | | 35 | | } |
| | | 36 | | |
| | | 37 | | /// <summary> |
| | | 38 | | /// Computes the RFC 7638 JWK thumbprint for an RSA public key extracted from a certificate. |
| | | 39 | | /// </summary> |
| | | 40 | | /// <param name="certificate">The X.509 certificate containing the RSA public key.</param> |
| | | 41 | | /// <returns>The Base64Url-encoded SHA-256 hash of the canonical JWK representation.</returns> |
| | | 42 | | public static string ComputeThumbprintFromCertificate(X509Certificate2 certificate) |
| | | 43 | | { |
| | 4 | 44 | | ArgumentNullException.ThrowIfNull(certificate); |
| | | 45 | | |
| | 3 | 46 | | using var rsa = certificate.GetRSAPublicKey() ?? throw new NotSupportedException("Certificate does not contain a |
| | 2 | 47 | | var parameters = rsa.ExportParameters(false); |
| | 2 | 48 | | var n = Base64UrlEncode(parameters.Modulus); |
| | 2 | 49 | | var e = Base64UrlEncode(parameters.Exponent); |
| | 2 | 50 | | return ComputeThumbprintRsa(n, e); |
| | 2 | 51 | | } |
| | | 52 | | |
| | | 53 | | /// <summary> |
| | | 54 | | /// Encodes data using Base64Url encoding as specified in RFC 7515. |
| | | 55 | | /// </summary> |
| | | 56 | | /// <param name="data">The data to encode.</param> |
| | | 57 | | /// <returns>The Base64Url-encoded string.</returns> |
| | | 58 | | private static string Base64UrlEncode(ReadOnlySpan<byte> data) |
| | | 59 | | { |
| | 14 | 60 | | return Convert.ToBase64String(data) |
| | 14 | 61 | | .TrimEnd('=') |
| | 14 | 62 | | .Replace('+', '-') |
| | 14 | 63 | | .Replace('/', '_'); |
| | | 64 | | } |
| | | 65 | | } |