You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 04:57:32