使用node-jose与C# BouncyCastle生成JWK指纹结果不一致的原因
JWK指纹生成结果不一致的原因分析
Node.js 实现(node-jose库)
使用node-jose库生成JWK指纹的代码如下:
const jose = require('node-jose'); generateJwkThumbprint = async (jwk) => { let jwkKey = await jose.JWK.asKey(jwk, 'pem'); // 将PEM转换为JWK对象 let jwkThumbprintBuffer = await jwkKey.thumbprint('SHA-256'); // 对JWK对象计算SHA-256哈希 let jwkThumbprint = jose.util.base64url.encode(jwkThumbprintBuffer, 'utf8'); // 对哈希结果做Base64URL编码 return jwkThumbprint; }; let publicKey = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZ1fr5JeX52k397Eex4oDBwODEOwz\nBG5qGAfmBFSz4aRZkqJeMPq5pV6jhaPN/w9wvTYNc0ibT8129/GCag6Snw==\n-----END PUBLIC KEY-----\n"; (async () => { try { let thumbprint = await generateJwkThumbprint(publicKey); console.log(thumbprint); } catch (error) { console.error("Error:", error); } })();
运行后得到的指纹:
Mts1dD5R6PnYesvL0YLbc43h4YBVrd8_koC9jFsv_Sg
C# 实现(BouncyCastle库)
尝试用BouncyCastle实现相同功能的代码如下:
static void Main() { var publicKey = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZ1fr5JeX52k397Eex4oDBwODEOwz\nBG5qGAfmBFSz4aRZkqJeMPq5pV6jhaPN/w9wvTYNc0ibT8129/GCag6Snw==\n-----END PUBLIC KEY-----\n"; var thumbprint = GenerateJwkThumbprint(publicKey); Console.Write(thumbprint); } private static AsymmetricKeyParameter JwkPemToAsymmetricKey(string jwkPem) { using StringReader reader = new StringReader(jwkPem); var pemReader = new PemReader(reader); var keyObject = pemReader.ReadObject(); if (keyObject is ECPublicKeyParameters ecPublicKey) { return ecPublicKey; } throw new InvalidOperationException("Invalid JWK PEM format or unsupported key type."); } private static string GenerateJwkThumbprint(string jwkPem) { // Convert PEM to JWK var jwkKey = JwkPemToAsymmetricKey(jwkPem); // Calculate SHA-256 thumbprint var sha256Thumbprint = CalculateSha256Thumbprint(jwkKey); // Base64url encode the thumbprint var thumbprint = Base64UrlEncode(sha256Thumbprint); return thumbprint; } private static string Base64UrlEncode(byte[] bytes) { return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').Replace("=", ""); } private static byte[] CalculateSha256Thumbprint(AsymmetricKeyParameter jwkKey) { IDigest digest = new Sha256Digest(); var jwkBytes = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(jwkKey).GetDerEncoded(); digest.BlockUpdate(jwkBytes, 0, jwkBytes.Length); var thumbprint = new byte[digest.GetDigestSize()]; digest.DoFinal(thumbprint, 0); return thumbprint; }
运行后得到的指纹:
yLCIQ8WauQaPAC4A3-LQ5l5s_p9vRfOqPl10efd0hck
差异原因
两者结果不同的核心是计算指纹的数据源完全不同:
- node-jose严格遵循RFC 7638定义的JWK指纹流程:先将PEM转换为标准JWK对象,再对JWK进行标准化JSON序列化(键名按字典序排列、无额外空格),最后对序列化后的JSON字节计算SHA-256哈希,再做Base64URL编码。
- 你的C#代码错误地使用了X.509证书指纹的计算方式:直接对SubjectPublicKeyInfo的DER编码字节做哈希,这和JWK指纹的标准计算逻辑完全不匹配。
修正思路(C#)
要得到和Node.js一致的结果,需要按照RFC 7638的步骤实现:
- 将EC公钥转换为符合规范的JWK JSON结构(包含
kty、crv、x、y等字段,字段名按字典序) - 对JWK JSON进行标准化序列化(无空格、键名排序)
- 对序列化后的字节计算SHA-256哈希
- 对哈希结果做Base64URL编码
示例修正后的核心代码片段:
private static byte[] CalculateJwkThumbprint(ECPublicKeyParameters ecKey) { // 构建JWK对象(按RFC 7638要求的字段顺序) var jwk = new Dictionary<string, string> { { "kty", "EC" }, { "crv", "P-256" }, // 根据密钥实际曲线调整,此处为P-256 { "x", Base64UrlEncode(ecKey.Q.XCoord.GetEncoded()) }, { "y", Base64UrlEncode(ecKey.Q.YCoord.GetEncoded()) } }; // 标准化JSON序列化:键按字典序,无空格 var jsonSerializer = new JsonSerializer(); jsonSerializer.ContractResolver = new OrderedContractResolver(); // 自定义排序解析器 using var stringWriter = new StringWriter(); using var jsonWriter = new JsonTextWriter(stringWriter) { Formatting = Formatting.None }; jsonSerializer.Serialize(jsonWriter, jwk); var jwkJson = stringWriter.ToString(); // 计算SHA-256哈希 using var sha256 = SHA256.Create(); return sha256.ComputeHash(Encoding.UTF8.GetBytes(jwkJson)); } // 自定义JSON键排序解析器 public class OrderedContractResolver : DefaultContractResolver { protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) { return base.CreateProperties(type, memberSerialization) .OrderBy(p => p.PropertyName) .ToList(); } }
内容的提问来源于stack exchange,提问作者Max
相关产品推荐
相关产品推荐

