ECDsa SHA512签名验证问题:C# Webhook签名始终验证失败
排查Ubble Webhook签名验证C#方法失败问题
问题背景
以下是用于验证Ubble API生成的Webhook签名的C#方法,但isValidSignature始终返回false:
private bool SignatureValid(string rawBody, string signatureHeader, string pubKey) { string[] signatureParts = signatureHeader.Split(':'); if (signatureParts.Length is not 3) { logger.LogError("Webhook signature not split into 3 parts: {sig}", signatureHeader); return false; } string timestamp = signatureParts[0]; string signature = signatureParts[2]; string signedPayload = $"{timestamp}:{rawBody}"; byte[] signedPayloadBytes = Encoding.UTF8.GetBytes(signedPayload); using ECDsa ecdsa = ECDsa.Create(); ecdsa.ImportFromPem($"-----BEGIN PUBLIC KEY----- {pubKey} -----END PUBLIC KEY-----".ToCharArray()); byte[] signatureBytes = Convert.FromBase64String(signature); bool isValidSignature = ecdsa.VerifyData(signedPayloadBytes, signatureBytes, HashAlgorithmName.SHA512); if (!isValidSignature) logger.LogError("Unable to verify signature for body and sig header: {body}, {sig}", rawBody, signatureHeader); return isValidSignature; }
传入的参数值如下:
- rawBody:
{"specversion": "2.0", "type": "identity_verification_created", "subject": "idv_01j1zgha0pa5cwhxfhcw08da7p", "id": "evnt_01j1zgha43jd06kgzs1xe1hy96", "time": "2024-07-04T18:36:32Z", "datacontenttype": "application/json", "data": {"applicant_id": "aplt_01j1zgh9z21hjwk045ednvxbs9", "status": "pending", "identity_verification_id": "idv_01j1zgha0pa5cwhxfhcw08da7p", "response_codes": [], "user_journey_id": "usj_01j19zhh6bjwfad1w464vbzhg0"}}
- signatureHeader:
1720118192.350353:2530-test-v1:MIGHAkEMjhZbBcB6RxOhXURr0+VV8j66Wgml+UpITG3sE54nVju7K2oFOWSIX/4W1kp0jfyY1DmOu8rVpPCFjnikRB/jWAJCAQhStnif+xLIOYavKos3c8kF+0nAOMJdVQAI3fY/mPfjD7Kqru2JxRaFPcz0FJPUekL+6x9nhNAx3v23ByuDRPrg
- pubKey:
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBJKgbekd2Q01oTmu7FK8m8hASky+O ssU7lFAuTIVd5/tA+h8Cjh2ieK+gjv8bHLov6b+3eegWvp+aq25nge/nCHcBCrfi 7gJDMuZ5Yg5kG4+mEwADC8caLhejF3ITs1n01dwuWHgD2/jniqvhVwITNRta00+K PRCYbQjw32/kti8WV50=
问题排查与修复
经过分析,验证失败的核心问题有三个,修复后即可正常验证:
1. 哈希算法误用
Ubble API使用SHA256计算签名哈希,但原代码中使用了HashAlgorithmName.SHA512,这是导致验证失败的首要原因。
2. ECDSA签名格式不兼容
Ubble返回的签名是ASN.1/DER格式,而.NET的ECDsa.VerifyData默认期望IEEE P1363格式(直接拼接r和s的字节数组),需要手动转换格式。
3. 公钥PEM格式解析异常
原代码直接拼接公钥时,若公钥中的换行符包含\r,会导致PEM格式解析错误,需先清理多余的换行符。
修复后的完整代码
private bool SignatureValid(string rawBody, string signatureHeader, string pubKey) { string[] signatureParts = signatureHeader.Split(':'); if (signatureParts.Length != 3) { logger.LogError("Webhook signature not split into 3 parts: {sig}", signatureHeader); return false; } string timestamp = signatureParts[0]; string signature = signatureParts[2]; // 构建待验证的载荷 string signedPayload = $"{timestamp}:{rawBody}"; byte[] signedPayloadBytes = Encoding.UTF8.GetBytes(signedPayload); // 清理公钥中的多余换行符,生成标准PEM格式 string cleanedPubKey = pubKey.Replace("\r", "").Trim(); string pemPubKey = $"-----BEGIN PUBLIC KEY-----\n{cleanedPubKey}\n-----END PUBLIC KEY-----"; using ECDsa ecdsa = ECDsa.Create(); ecdsa.ImportFromPem(pemPubKey.ToCharArray()); // 将DER格式签名转换为.NET兼容的P1363格式 byte[] derSignature = Convert.FromBase64String(signature); byte[] p1363Signature = ConvertDerToP1363(derSignature, ecdsa.KeySize); // 使用正确的SHA256算法验证签名 bool isValidSignature = ecdsa.VerifyData(signedPayloadBytes, p1363Signature, HashAlgorithmName.SHA256); if (!isValidSignature) logger.LogError("Unable to verify signature for body and sig header: {body}, {sig}", rawBody, signatureHeader); return isValidSignature; } // 辅助方法:将DER格式ECDSA签名转换为P1363格式 private byte[] ConvertDerToP1363(byte[] derSignature, int keySize) { // DER格式结构:SEQUENCE -> INTEGER(r), INTEGER(s) int offset = 0; // 跳过SEQUENCE标签和长度字段 if (derSignature[offset++] != 0x30) throw new InvalidOperationException("Invalid DER signature"); int length = derSignature[offset++]; if (length == 0x81) length = derSignature[offset++]; // 读取r值 if (derSignature[offset++] != 0x02) throw new InvalidOperationException("Invalid r tag"); int rLength = derSignature[offset++]; byte[] r = new byte[rLength]; Array.Copy(derSignature, offset, r, 0, rLength); offset += rLength; // 读取s值 if (derSignature[offset++] != 0x02) throw new InvalidOperationException("Invalid s tag"); int sLength = derSignature[offset++]; byte[] s = new byte[sLength]; Array.Copy(derSignature, offset, s, 0, sLength); offset += sLength; // 将r和s填充到密钥对应的字节长度(256位密钥对应32字节) int expectedLength = keySize / 8; r = PadToLength(r, expectedLength); s = PadToLength(s, expectedLength); // 拼接r和s得到P1363格式签名 byte[] p1363Signature = new byte[expectedLength * 2]; Array.Copy(r, 0, p1363Signature, 0, expectedLength); Array.Copy(s, 0, p1363Signature, expectedLength, expectedLength); return p1363Signature; } // 辅助方法:将字节数组填充到指定长度(前面补0) private byte[] PadToLength(byte[] data, int length) { if (data.Length == length) return data; if (data.Length > length) throw new InvalidOperationException("Signature segment too long"); byte[] paddedData = new byte[length]; Array.Copy(data, 0, paddedData, length - data.Length, data.Length); return paddedData; }
内容的提问来源于stack exchange,提问作者Andrey Belenkiy
相关产品推荐
相关产品推荐

