Search code examples
c#cryptographyx509certificateecdsa

How to verify signature with x509 certificate using EC-key in c# payconiq


I'm trying to verify a signature I get in the callback of Payconiq (payment-platform)

The signature is put together based on this logic

A JWS represents these logical values separated by dots(.):

  • JOSE Header
  • JWS Payload (Not included)
  • JWS Signature

The signature will be generated as per following instructions:

jws = base64URLEncode(JOSE Header)..base64URLEncode(alg(base64URLEncode(JOSE Header).base64URLEncode(Request Body)))

The data I have available is:

  1. Certificates -> https://ext.payconiq.com/certificates actual certificate ->

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

  1. body -> body of the callback post call
{
    "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"
    }
}
  1. signature which is a header of the callback call

eyJ0eXAiOiJKT1NFK0pTT04iLCJraWQiOiJlcy5zaWduYXR1cmUuZXh0LjIwMjIiLCJhbGciOiJFUzI1NiIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2lhdCI6IjIwMjItMDYtMjhUMDk6NTE6MTQuNzE0MjU0WiIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2p0aSI6ImU0OWIzNmNhM2EzM2I4ODIiLCJodHRwczovL3BheWNvbmlxLmNvbS9wYXRoIjoiaHR0cHM6Ly90ZXN0LnBhdGlzc2VyaWVzdGVmYW4ubmV0L2FwaS93ZWJzaG9wY29udHJvbGxlci9DYWxsYmFja1BheWNvbmlxIiwiaHR0cHM6Ly9wYXljb25pcS5jb20vaXNzIjoiUGF5Y29uaXEiLCJodHRwczovL3BheWNvbmlxLmNvbS9zdWIiOiI2MjVlN2ZmMDFlMjRiNzA0NDI5MWNkYzUiLCJjcml0IjpbImh0dHBzOi8vcGF5Y29uaXEuY29tL2lhdCIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2p0aSIsImh0dHBzOi8vcGF5Y29uaXEuY29tL3BhdGgiLCJodHRwczovL3BheWNvbmlxLmNvbS9pc3MiLCJodHRwczovL3BheWNvbmlxLmNvbS9zdWIiXX0..SIG71tYh8l0rRn7n7Bg3e1goWIloBlSwdkkXhXjIHZlelhNgKM4GJcFbimk-sIpdNl8XEOtKHVx_Tf93P3V-GA

I have tried multiple things including:

        var verified = false;
        byte[] dataToBeVerifiedByteArray = 
        Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(body));
        //this gets the string of the certificate mentioned above
        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;

Can anyone see what I'm doing wrong I can't find any solution. There is a documentation where they just referr to the IETF regarding the verification of the signature -> https://datatracker.ietf.org/doc/html/rfc7515#section-5.2

Payconiq documentation -> https://developer.payconiq.com/online-payments-dock/#the-callback-signature

EDIT: I have not found a solution but just check the headers and fetch the status fysically instead of verifying the signature


Solution

  • Here is a working sample

    using System.IdentityModel.Tokens.Jwt;
    using System.Security.Cryptography;
    using System.Security.Cryptography.X509Certificates;
    using System.Text;
    using Microsoft.IdentityModel.Tokens;
    
    public class Program
    {
        public static void Main()
        {
            var signature =
                "eyJ0eXAiOiJKT1NFK0pTT04iLCJraWQiOiJlcy5zaWduYXR1cmUuZXh0LjIwMjMiLCJhbGciOiJFUzI1NiIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2lhdCI6IjIwMjMtMDktMjlUMTM6MDk6NTEuNDUwMTAyWiIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2p0aSI6ImVjMzliMWE3NzBlMzE0ZGMiLCJodHRwczovL3BheWNvbmlxLmNvbS9wYXRoIjoiaHR0cHM6Ly93ZWJob29rLnNpdGUvODQ4OTBlZmYtZTc4OC00ZGQyLThlNDYtNzRjOGY2NzRhODFiIiwiaHR0cHM6Ly9wYXljb25pcS5jb20vaXNzIjoiUGF5Y29uaXEiLCJodHRwczovL3BheWNvbmlxLmNvbS9zdWIiOiI2MjdhMjJhNWRmMDcyYTU2NWYyNjQxNmEiLCJjcml0IjpbImh0dHBzOi8vcGF5Y29uaXEuY29tL2lhdCIsImh0dHBzOi8vcGF5Y29uaXEuY29tL2p0aSIsImh0dHBzOi8vcGF5Y29uaXEuY29tL3BhdGgiLCJodHRwczovL3BheWNvbmlxLmNvbS9pc3MiLCJodHRwczovL3BheWNvbmlxLmNvbS9zdWIiXX0..FxLuOg-96-4jApaZEv7AfqIt5OZzIrHXvKaYg150dBcXDc871iCHtZGHQTusZNdvnXx6yD7PcAdQkCMZNAj3Bw";
            var payloadRaw =
                """{"paymentId":"493342bd1ce7fbdc42e90a4d","transferAmount":1,"tippingAmount":0,"amount":1,"totalAmount":1,"description":"Test payment 12345","reference":"12345","createdAt":"2023-09-29T13:09:36.276Z","expireAt":"2023-09-29T13:11:36.276Z","status":"PENDING_MERCHANT_ACKNOWLEDGEMENT","debtor":{"name":"Kaveya","iban":"***********49713"},"currency":"EUR"}""";
            //needs to be downloaded from payconiq. Hardcoded here for simplicity
            var x5C =
                "MIIE2jCCBIGgAwIBAgIQdSIb11T2uTW0huJ5dv1/TzAKBggqhkjOPQQDAjCBjzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIEVDQyBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMB4XDTIyMTIwMTAwMDAwMFoXDTIzMTIwMTIzNTk1OVowKDEmMCQGA1UEAxMdZXMuc2lnbmF0dXJlLmV4dC5wYXljb25pcS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARIpLe02lsuMs6G1lQQRw3Zo4GlBwxi1h7EDD6GC9MxYRkkxOQMrJ1UKD3ni4dXcCZjHyv2GGvWhNICOaCso9Elo4IDIzCCAx8wHwYDVR0jBBgwFoAU9oUKOxGG4QR9DqoLLNLuzGR7e64wHQYDVR0OBBYEFHUsvJY0jGLPbsoGZeOmkk09+ADEMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgIHMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBhAYIKwYBBQUHAQEEeDB2ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29FQ0NEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0MCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTBLBgNVHREERDBCgh1lcy5zaWduYXR1cmUuZXh0LnBheWNvbmlxLmNvbYIhd3d3LmVzLnNpZ25hdHVyZS5leHQucGF5Y29uaXEuY29tMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdgCt9776fP8QyIudPZwePhhqtGcpXc+xDCTKhYY069yCigAAAYTNAAkNAAAEAwBHMEUCIE0AnY89A1e8tFlYCvH8TXVIl/tiLlIOwMImTRmYXsscAiEAuxJKE1m3VpB6PNPtmUV9b94pnBCvbLkliK/ZekJ0sh8AdwB6MoxU2LcttiDqOOBSHumEFnAyE4VNO9IrwTpXo1LrUgAAAYTNAAjRAAAEAwBIMEYCIQCkzfgYg4tWlDnIK2MeuMwOjFprd9wF4IYfmpC245e1hwIhAJT1QFyPwzjPUKcN7GQZnEpW2me1pFi5ZV2t9vfivj5mAHYA6D7Q2j71BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4AAAGEzQAIpAAABAMARzBFAiEAmxBqRdzZXC1FjrZldKSKJlEijGJNypWsBGnU7ZC6lzECIDdZeg6Xv1JPzZuke5O4Ml50Z96XIP6CqGCnkRcdcEFlMAoGCCqGSM49BAMCA0cAMEQCIEW8t/Vi5bs+vnSiluCHqofw0k34q4XE6NnfNAI4mf5xAiAEZ+FNTw+lr2kujzG3VB4HrpiMrH1TcJtNa3pfZImsRg==";
    
            var result = Verify(signature, payloadRaw, x5C);
            Console.WriteLine(result);
        }
    
        private static bool Verify(string jws, string payload, string x5C)
        {
            string[] parts = jws.Split("..");
            var encodedJoseHeader = parts[0];
            JwtHeader jwtHeader = JwtHeader.Base64UrlDeserialize(encodedJoseHeader);
            //var kid = jwtHeader.Kid; Use it to get x5c
            var signature = Base64UrlEncoder.DecodeBytes(parts[1]);
    
            using (var cert = new X509Certificate2(Convert.FromBase64String(x5C)))
            {
                var publicKey = new ECDsaSecurityKey(cert.GetECDsaPublicKey());
                var data = Encoding.UTF8.GetBytes($"{encodedJoseHeader}.{Base64UrlEncoder.Encode(payload)}");
                var isValid = publicKey.ECDsa.VerifyData(data, signature, HashAlgorithmName.SHA256);
                return isValid;
            }
        }
    }
    

    JOSE Header here is

    {
       "typ":"JOSE+JSON",
       "kid":"es.signature.ext.2023",
       "alg":"ES256",
       "https://payconiq.com/iat":"2023-09-29T13:09:51.450102Z",
       "https://payconiq.com/jti":"ec39b1a770e314dc",
       "https://payconiq.com/path":"https://webhook.site/84890eff-e788-4dd2-8e46-74c8f674a81b",
       "https://payconiq.com/iss":"Payconiq",
       "https://payconiq.com/sub":"627a22a5df072a565f26416a",
       "crit":[
          "https://payconiq.com/iat",
          "https://payconiq.com/jti",
          "https://payconiq.com/path",
          "https://payconiq.com/iss",
          "https://payconiq.com/sub"
       ]
    }