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

C#使用EC密钥X509证书验证Payconiq回调JWS签名失败问题

Payconiq回调JWS签名验证实现方案

问题描述

验证Payconiq支付平台回调接口返回的JWS规范签名时,现有C#代码始终返回false,临时方案为校验请求头后主动调用接口查询支付状态,需要正确的签名验证实现。
该JWS签名以.分隔为三部分:JOSE头、空载荷、JWS签名,签名生成规则为:
jws = base64URLEncode(JOSE Header)..base64URLEncode(alg(base64URLEncode(JOSE Header).base64URLEncode(Request Body)))

可用验证数据

  • Payconiq官方X509证书Base64内容:

MIIE1zCCBH2gAwIBAgIQHzgeQOjemgrfp6IwTS5XfzAKBggqhkjOPQQDAjCBjzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIEVDQyBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMB4XDTIxMTEyMzAwMDAwMFoXDTIyMTIyNDIzNTk1OVowKDEmMCQGA1UEAxMdZXMuc2lnbmF0dXJlLmV4dC5wYXljb25pcS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARIpLe02lsuMs6G1lQQRw3Zo4GlBwxi1h7EDD6GC9MxYRkkxOQMrJ1UKD3ni4dXcCZjHyv2GGvWhNICOaCso9Elo4IDHzCCAxswHwYDVR0jBBgwFoAU9oUKOxGG4QR9DqoLLNLuzGR7e64wHQYDVR0OBBYEFHUsvJY0jGLPbsoGZeOmkk09+ADEMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgIHMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBhAYIKwYBBQUHAQEEeDB2ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29FQ0NEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0MCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTBLBgNVHREERDBCgh1lcy5zaWduYXR1cmUuZXh0LnBheWNvbmlxLmNvbYIhd3d3LmVzLnNpZ25hdHVyZS5leHQucGF5Y29uaXEuY29tMIIBewYKKwYBBAHWeQIEAgSCAWsEggFnAWUAdQBGpVXrdfqRIDC1oolp9PN9ESxBdL79SbiFq/L8cP5tRwAAAX1MZuRtAAAEAwBGMEQCIErmMHlQjPe/aNTo08NiFGS2hlKeBU5Ubrl9OG7myLWcAiB4bWXL8HOl2oNVci3Cv0RMnNTyMHIrAm8Lw9QQq/UxTQB1AEHIyrHfIkZKEMahOglCh15OMYsbA+vrS8do8JBilgb2AAABfUxm5DUAAAQDAEYwRAIgNEbgqCHIAjLqhRGBmiHRAqNwX5qI1GSlfAbqVq4V/W0CIHRCmucjmXpbVKzPsOfJ6RBPHWSUJJSjiGLf1QTtvliDAHUAKXm+8J45OSHwVnOfY6V35b5XfZxgCvj5TV0mXCVdx4QAAAF9TGbj/QAABAMARjBEAiAlPQGU1X34G+wtrYEpGFodWifIfxfeOwKx9o3qjVr4LAIgUQenz7z8a0zIC5XATCAwEG3uXnbATrl+ss5cu6YqvPowCgYIKoZIzj0EAwIDSAAwRQIhAN5vKyEhzWAj6Wc6bhr8l9YXIGn4e4dNVSYeHcRoK0AkAiAhhXJkG+SzWyp/bFJeCfXbnWw59mww9GOOkoNizKCG6w==

  • 回调POST请求JSON体:
{
    "PaymentId": "8016ab30f89882a72c6827e6",
    "TransferAmount": 100,
    "TippingAmount": 0,
    "TotalAmount": 100,
    "Currency": "EUR",
    "Amount": 100,
    "Description": "betaling Webshop Patisserie Stefan",
    "Reference": "5902",
    "CreatedAt": "2022-06-28T09: 50: 58.298Z",
    "ExpireAt": "2022-06-28T10: 10: 58.298Z",
    "Status": "SUCCEEDED",
    "Debtor": {
        "Name": "Nathan",
        "Iban": "***51944"
    }
}
  • 回调请求头携带的JWS签名值:

eyJ0eXAiOiJKT1NFK0pTT04iLCJraWQiOiJlcy5zaWduYXR1cmUuZXh0LjIwMjIiLCJhbGciOiJFUzI1NiIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2lhdCI6IjIwMjItMDYtMjhUMDk6NTE6MTQuNzE0MjU0WiIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2p0aSI6ImU0OWIzNmNhM2EzM2I4ODIiLCJodHRwczovL3BheWNvbmlxLmNvbS9wYXRoIjoiaHR0cHM6Ly90ZXN0LnBhdGlzc2VyaWVzdGVmYW4ubmV0L2FwaS93ZWJzaG9wY29udHJvbGxlci9DYWxsYmFja1BheWNvbmlxIiwiaHR0cHM6Ly9wYXljb25pcS5jb20vaXNzIjoiUGF5Y29uaXEiLCJodHRwczovL3BheWNvbmlxLmNvbS9zdWIiOiI2MjVlN2ZmMDFlMjRiNzA0NDI5MWNkYzUiLCJjcml0IjpbImh0dHBzOi8vcGF5Y29uaXEuY29tL2lhdCIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2p0aSIsImh0dHBzOi8vcGF5Y29uaXEuY29tL3BhdGgiLCJodHRwczovL3BheWNvbmlxLmNvbS9pc3MiLCJodHRwczovL3BheWNvbmlxLmNvbS9zdWIiXX0..SIG71tYh8l0rRn7n7Bg3e1goWIloBlSwdkkXhXjIHZlelhNgKM4GJcFbimk-sIpdNl8XEOtKHVx_Tf93P3V-GA

现有问题代码

var verified = false;
byte[] dataToBeVerifiedByteArray = 
Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(body));
// 读取上述证书内容
byte[] cerBytes = Convert.FromBase64String(jwk.X5cValues.First().Waarde);
X509Certificate2 cer = new(cerBytes);
ECDsa ECDKey = cer.GetECDsaPublicKey();
ECParameters ECDsaPublicParam = ECDKey.ExportParameters(false);
using (var ecdsa = ECDsa.Create())
{
    ecdsa.ImportParameters(ECDsaPublicParam);
    verified = ecdsa.VerifyData(dataToBeVerifiedByteArray, 
    Encoding.UTF8.GetBytes(signature), HashAlgorithmName.SHA256);
};
return verified;

错误原因

现有代码存在4个核心问题,直接导致验签失败:

  1. 待验签原文构造错误:Detached模式JWS的待签名内容不是单独的请求体,是Base64URL编码后的JOSE头 + "." + Base64URL编码后的原始请求体字节拼接后的字符串,现有代码只取了序列化后的请求体,完全不符合签名规则。
  2. 请求体处理错误:代码把请求体反序列化后再重新序列化,会因为JSON字段顺序、空格、换行、特殊字符转义规则差异(比如时间字段里冒号后的多余空格会被序列化逻辑清除),导致参与计算的原文和Payconiq服务端计算时的原文不一致。必须直接读取HTTP请求的原始请求体字节,不能做任何序列化/反序列化转换。
  3. 签名解码错误:JWS第三部分的签名是Base64URL编码的,不能直接转UTF8字节,需要先做Base64URL解码得到原始二进制签名值,还要处理Base64URL和标准Base64的字符差异、补全填充符。
  4. 签名格式不匹配:ES256算法的JWS签名采用IEEE P1363格式(32字节R值 + 32字节S值固定拼接),而C#的ECDsa.VerifyData方法默认要求ANS.1 DER编码格式的签名,格式不匹配必然验签失败。

修正后可运行代码

/// <summary>
/// 验证Payconiq回调JWS签名
/// </summary>
/// <param name="rawRequestBody">HTTP请求读取的原始请求体字符串,不要做任何JSON转换</param>
/// <param name="jwsHeaderValue">请求头里拿到的完整JWS字符串</param>
/// <param name="certBase64">Payconiq官方X509证书Base64内容</param>
/// <returns>验签是否通过</returns>
public bool VerifyPayconiqSignature(string rawRequestBody, string jwsHeaderValue, string certBase64)
{
    // 拆分JWS的三部分
    var jwsSegments = jwsHeaderValue.Split('.');
    if (jwsSegments.Length != 3)
        throw new ArgumentException("JWS格式不合法,必须包含三个点分隔段");
    
    string encodedHeader = jwsSegments[0];
    string encodedSignature = jwsSegments[2];

    // 1. 构造符合规则的待验签内容
    byte[] requestBodyRawBytes = Encoding.UTF8.GetBytes(rawRequestBody);
    string encodedPayload = Base64UrlEncode(requestBodyRawBytes);
    string contentToSign = $"{encodedHeader}.{encodedPayload}";
    byte[] contentToSignBytes = Encoding.UTF8.GetBytes(contentToSign);

    // 2. 解码Base64URL格式的签名
    byte[] signatureRawBytes = Base64UrlDecode(encodedSignature);

    // 3. 加载证书中的ECDSA公钥
    byte[] certBytes = Convert.FromBase64String(certBase64);
    using var certificate = new X509Certificate2(certBytes);
    using var ecdsaPublicKey = certificate.GetECDsaPublicKey();
    if (ecdsaPublicKey == null)
        throw new InvalidOperationException("当前证书不包含ECDSA公钥");

    // 4. 验签,明确指定签名格式为JWS使用的IEEE P1363固定R/S拼接格式,哈希算法为SHA256
    return ecdsaPublicKey.VerifyData(
        contentToSignBytes, 
        signatureRawBytes, 
        HashAlgorithmName.SHA256, 
        DSASignatureFormat.IeeeP1363FixedFieldConcatenation
    );
}

// Base64URL编码辅助方法
private string Base64UrlEncode(byte[] input)
{
    return Convert.ToBase64String(input)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
}

// Base64URL解码辅助方法
private byte[] Base64UrlDecode(string input)
{
    string standardBase64 = input.Replace('-', '+').Replace('_', '/');
    // 补全填充符
    int paddingLength = (4 - standardBase64.Length % 4) % 4;
    standardBase64 = standardBase64.PadRight(standardBase64.Length + paddingLength, '=');
    return Convert.FromBase64String(standardBase64);
}

注意事项

  • 必须在HTTP请求处理管道中开启请求体缓冲,直接读取原始请求体流的内容,不要使用模型绑定后的对象做序列化
  • 注意匹配证书和JWS头中kid字段对应的环境(测试/生产环境证书不通用)
  • 不要对原始请求体做任何修改,包括去除空格、调整编码、转义字符等操作

内容的提问来源于stack exchange,提问作者Nathan T.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 13:48:14