如何在 C# 中验证 webhook 的 RS256 签名属性?

How do I verify the RS256 signature attribute of a webhook in C#?

过去两天我一直在努力解决这个问题,但它让我有点发疯。在那之前我没有深入研究密码学,所以我很困惑想弄清楚我打算做什么。

我一直致力于集成来自名为 Citizen 的支付提供商的 API。支付流程的一些步骤将 webhook 更新以以下格式发送到我的端点:

 "eventType":"<the event type of a webhook>",
 "paymentToken": {
      "id": "<token ID>",
      "paymentProvider": "<paymentProvide e.g. LLOYDS>",
      "paymentMethod": "<internal marker>",
      "paymentGiro": "<the payment giro used e.g. FPS/SEPA>",
       ...other data omitted for brevity
    },
    "signature": "token-signature."
}

并且他们的文档仅建议以下内容:

所有 webhook 更新都将包含令牌详细信息的签名,您可以使用我们的 public 密钥进行验证。

为了签名,我们使用 SHA256 和 RSA。您可以从“https://api.citizen.is/v1/entities/citizen-signing-public-key(或 testapi.citizen.is for test)”获取 public RSA 密钥 可以使用 webhook 更新的“签名”属性找到签名。

我已检索到 public 密钥,但我不清楚我应该验证消息的哪一部分。我发现签名的所有其他示例都在 header 中,因此所有请求 body 都经过哈希处理以验证签名。我是否应该删除签名并对消息的其余部分进行哈希处理?我试过了,但它仍然返回错误。到目前为止,我已经掌握了以下内容,我们将不胜感激!

        public async Task<IActionResult> Post()
        {
            using var reader = new StreamReader(HttpContext.Request.Body);
            // You now have the body string raw
            var body = await reader.ReadToEndAsync();
            var msg = JsonConvert.DeserializeObject<SignedWebhook>(body);
            var originalMsg = JsonConvert.DeserializeObject<OriginalWebhook>(body);
            var result = VerifyData(JsonConvert.SerializeObject(originalMsg), Convert.FromBase64String(msg.signature));

            return new OkResult();

        }

        public static bool VerifyData(string originalMessage, byte[] signature)
        {
            string stringpublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzIU140G9rFe6ouNFuhCxIj3Ps3ELUV9w4XTnDsti8kcSTXMf0z6LMNVIqXaZYFbSYXAZRmuM3XNmoSWmMZzPBMl2/C7uC0wyNdrYdPw0uzU2wfr8MQbnvW0yQgQ/cSHNDUZR+n/s2ipXTdNmbRd4z+k+qXxw00xMDmiJu5iMHyYo24x284lTZ3+4dgL4xFlrtjgcb/NGHBpVPQTCbBfEQcmylCwzbTUdBJlAo5ezpziOJ6CNf9FDS1hvRKRvNl7Hx8To6vQZJTwdCT5RWDC2JYL0oSdPV+SZmlfHQQe33p81MiRl4cjp5AwMVKyAosDihGT810WFYhK431EIB/NR/wIDAQAB";
            string pemPublicKey = $"-----BEGIN PUBLIC KEY-----\n{ stringpublicKey }\n-----END PUBLIC KEY-----";

            var pkey = ImportPublicKey(pemPublicKey);
            var signatureDeformatter = new RSAPKCS1SignatureDeformatter(pkey);

            // Set the hash algorithm to SHA256.
            signatureDeformatter.SetHashAlgorithm("SHA256");
            byte[] hash;
            using (SHA256 sha256 = SHA256.Create())
            {
                hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(originalMessage));
            }

            bool verified = signatureDeformatter.VerifySignature(hash, signature);


            return verified;
        }

        /// <summary>
        /// Import PEM public key string into MS RSACryptoServiceProvider
        /// </summary>
        /// <param name="pem"></param>
        /// <returns>RSACryptoServiceProvider</returns>
        public static RSACryptoServiceProvider ImportPublicKey(string pem)
        {
            PemReader pr = new(new StringReader(pem));
            AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();
            RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKey);

            RSACryptoServiceProvider csp = new();// cspParams);
            csp.ImportParameters(rsaParams);
            return csp;
        }

正在到达终点,我可以设置 public 键,但在那之后我不知道下一步该做什么。

听起来他们可能正在签署 paymentToken,而不是整个消息。也许尝试这样的事情:

    public async Task<IActionResult> Post()
    {
        using var reader = new StreamReader(HttpContext.Request.Body);
        // You now have the body string raw
        var body = await reader.ReadToEndAsync();
        var msg = JsonConvert.DeserializeObject<SignedWebhook>(body);
        var originalMsg = JsonConvert.DeserializeObject<OriginalWebhook>(body);
        var result = VerifyData(Convert.FromBase64String(msg.paymentToken), Convert.FromBase64String(msg.signature));

        return new OkResult();
    }

    public static bool VerifyData(byte[] paymentToken, byte[] signature)
    {
        string stringpublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzIU140G9rFe6ouNFuhCxIj3Ps3ELUV9w4XTnDsti8kcSTXMf0z6LMNVIqXaZYFbSYXAZRmuM3XNmoSWmMZzPBMl2/C7uC0wyNdrYdPw0uzU2wfr8MQbnvW0yQgQ/cSHNDUZR+n/s2ipXTdNmbRd4z+k+qXxw00xMDmiJu5iMHyYo24x284lTZ3+4dgL4xFlrtjgcb/NGHBpVPQTCbBfEQcmylCwzbTUdBJlAo5ezpziOJ6CNf9FDS1hvRKRvNl7Hx8To6vQZJTwdCT5RWDC2JYL0oSdPV+SZmlfHQQe33p81MiRl4cjp5AwMVKyAosDihGT810WFYhK431EIB/NR/wIDAQAB";

        var signingCert = new X509Certificate2(stringpublicKey);

        var keyParams = signingCert.GetRSAPublicKey().ExportParameters(false);
        var rsa = RSA.Create();
        rsa.ImportParameters(
            new RSAParameters()
            {
                Modulus = keyParams.Modulus,
                Exponent = keyParams.Exponent
            }
        );            

        bool verified = rsa.VerifyData(paymentToken, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

        return verified;
    }