I'm working on an integration with Tillo, using signed webhooks as described here.
Tillo provide example code for signature validation in Python and PHP. However, I need an implementation in C# and so I've been working to produce that. I've managed to get as far as the code below but I can't get the signature verification to work, it returns isValid==false regardless of my attempts to solve it.
I have little experience of this kind of work and so need to reach out for some guidance - I've got to the point where I'm not sure what I need to do to get this working now. Can anyone provide me with some help with this? Thanks.
My (not working) code, which uses the BouncyCastle.Cryptography and Microsoft.Bcl.Cryptography libraries in .NET 9.0.1.
private const string TILLO_CA_CERT_URL = "https://ca.tillo.io/";
void Main()
{
var headers = new Dictionary<string, string>() {
{ "webhook-id", "6b8aa749707b77c4be" },
{ "webhook-signature", "v1a,fiU3Rd66lIFFqUP4F0/knu33LmhlJrlr+8LHkcVo5wKiLZ23c60Dodi+kAugvObr4sRX+rqOmYJ7+uENl/irhqb+OCBGnvC0ydP/Bo7wu7wFHF8JZFI1diW2fN9IC1jybKAvN+BNQoMMlL/hUceMV3HHWjLMVL742NiEXWHG9eAV7GHfdGFP0US8rJLfPbafwbqDtO+jnIAH/W0ve6jz821Ky2hFHPt1+W+qjYlBN8UC54cYTLpKTxF3ZkaC4WwWWpSXHClgjyxkXSXJ8NJzOx02fO/0RiJZYWSbp8yrURvBvrdTumSl9bQ/a11O6WBaqGASkZBmsyncng9+oa79/Kj2tEsQ/b2SeZEXEdfT28Vhnvlk4uWyWswC965jMfyCp66VJak8KakBBNtZ1gSiHPzUakQagz3bK0/5WBVdUukwx9I/cUzK3kFbTrqpvwW+PvB5MdYb1ove7FG47W3ZEQ2lvF+62NhiNPVyYzyw7y4DyRtHm6KBlZwpE2M6FE/cVmQNilCOYD2ttd99WrXskMYTH8wV3HEu9Xz34TmThQjCtHsRcMX2AmXMUJGEGCbYe//eU7xVvfr8WwUM7gIBnD92t5s8bVkN/y/uT4WIl/OsBvcipBzVFxZxxAb7oiGjPl6SaQ/f0aaa9j7+FIbh8/PIK/VNL7qUxQiM+gfdE80="},
{ "webhook-timestamp", "1740137197"}
};
var payload = "{\"type\":\"brands.status.updated\",\"timestamp\":\"2025-02-21T11:26:34Z\",\"certificate\":\"-----BEGIN CERTIFICATE-----\\r\\nMIIGBzCCA++gAwIBAgIUCMkS5Ti+joHJ4U2y2x4IzqC+0nMwDQYJKoZIhvcNAQEL\\r\\nBQAwgZ0xFDASBgNVBAMMC1RpbGxvQ0FSb290MQswCQYDVQQGEwJHQjEPMA0GA1UE\\r\\nCAwGU3Vzc2V4MRgwFgYDVQQHDA9CcmlnaHRvbiAmIEhvdmUxDjAMBgNVBAoMBVRp\\r\\nbGxvMRYwFAYDVQQLDA1QbGF0Zm9ybSBUZWFtMSUwIwYJKoZIhvcNAQkBFhZwbGF0\\r\\nZm9ybS50ZWFtQHRpbGxvLmlvMB4XDTI0MTIxMDE1MDk0NloXDTI5MTIwOTE1MDk0\\r\\nNlowZzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBlN1c3NleDEOMAwGA1UECgwFVGls\\r\\nbG8xFjAUBgNVBAsMDVBsYXRmb3JtIFRlYW0xHzAdBgNVBAMMFnRpbGxvLXdlYmhv\\r\\nb2tzLXN0YWdpbmcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJf16N\\r\\nTnvyIgEg88tl7B+2ZoOsDnYTMTBf12zSRdNre5KhDYWgXQ5Bgd3dx412VSU1eoWN\\r\\nHV7494I\\/Vm1HQBNBj63uCAcivykOZttYayj4tPIv5qL4LzO2rZVzeRkJMe1bK6E9\\r\\nwoABGhLQDO9QUnxzQykPIBTjt0lE0PY1uGiJxzrn99jcDMKgM1p9AfQrJQIV3BzQ\\r\\nz27dS4vQzcIvVgqbVRt1ZPU8jlbo6HuEexOM47NVToll83T8hLJG1FzDkbJ4HyCm\\r\\n3UFEEp20fMnxMtRgipp5OttjOjfVKUHaUQEawf9B8LZM9WRxIIcQHV1LvQ0BYIRO\\r\\n5PoWd6dV9khakAPMlynHu68FnJPF5xbMaURW9mcp++s8r4OvVBP2d07jh9pkze45\\r\\nHo0TTza3F1vu2eMq72rETQhEwjeR0gAhT\\/87dJDTKgiI+jPfsh6NxH4PVsvqXl8h\\r\\nKa5cRpOOvFn0oI4OKMovuQ1FA5Q+Q9Ttj1QAnfUqj2mYm2PmIcH3ljUExjtsgfXG\\r\\nxf67neF\\/lum5ZYD0njeWqynkbEBr61VKXnCdNGfSfsgytbTeGPbrlAXlJ8Fa4JSp\\r\\nNGQOi99DDnyR1QRiVjESoqME+ps1GkZB7Za5f28fbcOb3clL9YHEo8PDmUwfsm5J\\r\\nt1CfrpUeQrI6xmX6HWkprp5yBXY63ZIEQcIUtQIDAQABo3QwcjAwBgNVHR8EKTAn\\r\\nMCWgI6Ahhh9odHRwczovL2NhLnRpbGxvLmlvL2NybC9jcmwucGVtMB0GA1UdDgQW\\r\\nBBQyNSvOXcnhPIdncEPJSdXJtZdcsjAfBgNVHSMEGDAWgBSkN2+VN1dBCA8ay0zz\\r\\nxK1dw8K+RjANBgkqhkiG9w0BAQsFAAOCAgEAEmn8EknSOTrH90b3ec1yKJ5EeHKY\\r\\nqY5OeoFS4NHWpVcJ4wpo80r+zzxFJjD4kdoUDi4fcsOh4XC8OnFk6QW0fVcJbrp3\\r\\nuSyInD19aom7FNz+qWpPdcIg2ZNsCJ64TR6NR8EjZtqjLMR4+J\\/H7Aqb+VL2mFTC\\r\\nvQlMQsmmTa6fC4IYAd8woyfPF+Y56z81hZPNNaWQhvby52bzO27kLKCJmN2eZN\\/Q\\r\\nmmNIwQZnEb8UdCoEuzS8iElPGGniB4x\\/uXwhkE9kIJhmjHhIFVm1hWTLWYO3hcmP\\r\\nWBnVjplhx3Un9i1gTXeCrmpetCEGtRoHuIFQ8BruS+XFja3eReqHuz4Dhwtd3L6y\\r\\nm+Ni2PkiVe4wTGKCD37h9FTTbKiNt1ImR07+Zpy5edG2i\\/LD3LzH00rs\\/e41IfcB\\r\\nlmxd5QuI7Cjp7kdHzGB6oMtYBawMOFIRYgfGeCNE\\/7EgSFsW6wExfEZz1hNO7Cm1\\r\\nvQe8pe9s\\/b3tGovvLZQf0F6CRBd6VtpyxztmWQRIsUNgXl3LMCnawP0JmnR6wQDi\\r\\nq9H0SIzeK52YoEFxQNnItqvNLLhjOFI1TEd16AaILPyvQaPOKOj0U4KqnBd+BR2s\\r\\njdabaA9YPN09PiTIFbONBkwq1ZyGcF9gHCiRkW6jISuc17tf5w49sdeERzDttmwi\\r\\n5qiGBh41yjM59Ks=\\r\\n-----END CERTIFICATE-----\",\"version\":1,\"data\":[{\"name\":\"Costa\",\"slug\":\"costa\",\"status\":{\"code\":\"DISABLED\",\"reason\":\"TEST\"}},{\"name\":\"Farmfoods\",\"slug\":\"farmfoods\",\"status\":{\"code\":\"DISABLED\",\"reason\":\"TEST\"}},{\"name\":\"HelloFresh\",\"slug\":\"hello-fresh\",\"status\":{\"code\":\"DISABLED\",\"reason\":\"TEST\"}},{\"name\":\"Nike\",\"slug\":\"nike-usa\",\"status\":{\"code\":\"DISABLED\",\"reason\":\"TEST\"}}]}";
var jo = JObject.Parse(payload);
var certificate = jo["certificate"].ToString();
var signingCert = new Org.BouncyCastle.X509.X509CertificateParser().ReadCertificate(Encoding.UTF8.GetBytes(certificate));
using (var httpClient = new HttpClient())
{
var caCertPem = httpClient.GetStringAsync(TILLO_CA_CERT_URL).Result;
var caCert = new Org.BouncyCastle.X509.X509CertificateParser().ReadCertificate(Encoding.UTF8.GetBytes(caCertPem));
signingCert.Verify(caCert.GetPublicKey());
signingCert.CheckValidity();
AsymmetricKeyParameter keyParams = signingCert.GetPublicKey();
var rsaKeyParams = (Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters)keyParams;
var rsa=DotNetUtilities.ToRSA(rsaKeyParams);
string concatenatedData = $"{headers["webhook-id"]}.{headers["webhook-timestamp"]}.{payload}";
byte[] dataBytes = Encoding.UTF8.GetBytes(concatenatedData);
byte[] signature = Convert.FromBase64String(headers["webhook-signature"].Replace("v1a,",""));
bool isValid;
using (var rsaCng = new RSACng())
{
var rsaParameters = rsa.ExportParameters(false);
rsaCng.ImportParameters(rsaParameters);
isValid = rsaCng.VerifyData(dataBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
}
if (!isValid) throw new Exception("isValid should be true");
}
}
Following various comments, here's some received payload bytes (not the same payload as above), base64 encoded:
eyJ0eXBlIjoiYnJhbmRzLmRlbm9taW5hdGlvbnMudXBkYXRlZCIsInRpbWVzdGFtcCI6IjIwMjUtMDItMjdUMjM6Mjg6MzBaIiwiY2VydGlmaWNhdGUiOiItLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS1cclxuTUlJR0J6Q0NBKytnQXdJQkFnSVVDTWtTNVRpK2pvSEo0VTJ5Mng0SXpxQyswbk13RFFZSktvWklodmNOQVFFTFxyXG5CUUF3Z1oweEZEQVNCZ05WQkFNTUMxUnBiR3h2UTBGU2IyOTBNUXN3Q1FZRFZRUUdFd0pIUWpFUE1BMEdBMVVFXHJcbkNBd0dVM1Z6YzJWNE1SZ3dGZ1lEVlFRSERBOUNjbWxuYUhSdmJpQW1JRWh2ZG1VeERqQU1CZ05WQkFvTUJWUnBcclxuYkd4dk1SWXdGQVlEVlFRTERBMVFiR0YwWm05eWJTQlVaV0Z0TVNVd0l3WUpLb1pJaHZjTkFRa0JGaFp3YkdGMFxyXG5abTl5YlM1MFpXRnRRSFJwYkd4dkxtbHZNQjRYRFRJME1USXhNREUxTURrME5sb1hEVEk1TVRJd09URTFNRGswXHJcbk5sb3daekVMTUFrR0ExVUVCaE1DUjBJeER6QU5CZ05WQkFnTUJsTjFjM05sZURFT01Bd0dBMVVFQ2d3RlZHbHNcclxuYkc4eEZqQVVCZ05WQkFzTURWQnNZWFJtYjNKdElGUmxZVzB4SHpBZEJnTlZCQU1NRm5ScGJHeHZMWGRsWW1odlxyXG5iMnR6TFhOMFlXZHBibWN3Z2dJaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQ0R3QXdnZ0lLQW9JQ0FRREpmMTZOXHJcblRudnlJZ0VnODh0bDdCKzJab09zRG5ZVE1UQmYxMnpTUmROcmU1S2hEWVdnWFE1QmdkM2R4NDEyVlNVMWVvV05cclxuSFY3NDk0SVwvVm0xSFFCTkJqNjN1Q0FjaXZ5a09adHRZYXlqNHRQSXY1cUw0THpPMnJaVnplUmtKTWUxYks2RTlcclxud29BQkdoTFFETzlRVW54elF5a1BJQlRqdDBsRTBQWTF1R2lKeHpybjk5amNETUtnTTFwOUFmUXJKUUlWM0J6UVxyXG56MjdkUzR2UXpjSXZWZ3FiVlJ0MVpQVThqbGJvNkh1RWV4T000N05WVG9sbDgzVDhoTEpHMUZ6RGtiSjRIeUNtXHJcbjNVRkVFcDIwZk1ueE10UmdpcHA1T3R0ak9qZlZLVUhhVVFFYXdmOUI4TFpNOVdSeElJY1FIVjFMdlEwQllJUk9cclxuNVBvV2Q2ZFY5a2hha0FQTWx5bkh1NjhGbkpQRjV4Yk1hVVJXOW1jcCsrczhyNE92VkJQMmQwN2poOXBremU0NVxyXG5IbzBUVHphM0YxdnUyZU1xNzJyRVRRaEV3amVSMGdBaFRcLzg3ZEpEVEtnaUkralBmc2g2TnhINFBWc3ZxWGw4aFxyXG5LYTVjUnBPT3ZGbjBvSTRPS01vdnVRMUZBNVErUTlUdGoxUUFuZlVxajJtWW0yUG1JY0gzbGpVRXhqdHNnZlhHXHJcbnhmNjduZUZcL2x1bTVaWUQwbmplV3F5bmtiRUJyNjFWS1huQ2ROR2ZTZnNneXRiVGVHUGJybEFYbEo4RmE0SlNwXHJcbk5HUU9pOTlERG55UjFRUmlWakVTb3FNRStwczFHa1pCN1phNWYyOGZiY09iM2NsTDlZSEVvOFBEbVV3ZnNtNUpcclxudDFDZnJwVWVRckk2eG1YNkhXa3BycDV5QlhZNjNaSUVRY0lVdFFJREFRQUJvM1F3Y2pBd0JnTlZIUjhFS1RBblxyXG5NQ1dnSTZBaGhoOW9kSFJ3Y3pvdkwyTmhMblJwYkd4dkxtbHZMMk55YkM5amNtd3VjR1Z0TUIwR0ExVWREZ1FXXHJcbkJCUXlOU3ZPWGNuaFBJZG5jRVBKU2RYSnRaZGNzakFmQmdOVkhTTUVHREFXZ0JTa04yK1ZOMWRCQ0E4YXkwenpcclxueEsxZHc4SytSakFOQmdrcWhraUc5dzBCQVFzRkFBT0NBZ0VBRW1uOEVrblNPVHJIOTBiM2VjMXlLSjVFZUhLWVxyXG5xWTVPZW9GUzROSFdwVmNKNHdwbzgwcit6enhGSmpENGtkb1VEaTRmY3NPaDRYQzhPbkZrNlFXMGZWY0picnAzXHJcbnVTeUluRDE5YW9tN0ZOeitxV3BQZGNJZzJaTnNDSjY0VFI2TlI4RWpadHFqTE1SNCtKXC9IN0FxYitWTDJtRlRDXHJcbnZRbE1Rc21tVGE2ZkM0SVlBZDh3b3lmUEYrWTU2ejgxaFpQTk5hV1FodmJ5NTJiek8yN2tMS0NKbU4yZVpOXC9RXHJcbm1tTkl3UVpuRWI4VWRDb0V1elM4aUVsUEdHbmlCNHhcL3VYd2hrRTlrSUpobWpIaElGVm0xaFdUTFdZTzNoY21QXHJcbldCblZqcGxoeDNVbjlpMWdUWGVDcm1wZXRDRUd0Um9IdUlGUThCcnVTK1hGamEzZVJlcUh1ejREaHd0ZDNMNnlcclxubStOaTJQa2lWZTR3VEdLQ0QzN2g5RlRUYktpTnQxSW1SMDcrWnB5NWVkRzJpXC9MRDNMekgwMHJzXC9lNDFJZmNCXHJcbmxteGQ1UXVJN0NqcDdrZEh6R0I2b010WUJhd01PRklSWWdmR2VDTkVcLzdFZ1NGc1c2d0V4ZkVaejFoTk83Q20xXHJcbnZRZThwZTlzXC9iM3RHb3Z2TFpRZjBGNkNSQmQ2VnRweXh6dG1XUVJJc1VOZ1hsM0xNQ25hd1AwSm1uUjZ3UURpXHJcbnE5SDBTSXplSzUyWW9FRnhRTm5JdHF2TkxMaGpPRkkxVEVkMTZBYUlMUHl2UWFQT0tPajBVNEtxbkJkK0JSMnNcclxuamRhYmFBOVlQTjA5UGlUSUZiT05Ca3dxMVp5R2NGOWdIQ2lSa1c2aklTdWMxN3RmNXc0OXNkZUVSekR0dG13aVxyXG41cWlHQmg0MXlqTTU5S3M9XHJcbi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0iLCJ2ZXJzaW9uIjoxLCJkYXRhIjp7Im5hbWUiOiJIZWxsb0ZyZXNoIiwic2x1ZyI6ImhlbGxvLWZyZXNoIiwiZGVub21pbmF0aW9ucyI6W119fQ==
The headers related to that byte payload follow:
"webhook-id" : "test_b6b0481330c34693ab139213b3a84c14"
"webhook-signature" : "v1a,QxwQDrh5Y9pEKN8DzdWj7/BzJ7wsbAXzeYHWSrNC6Kk/v3nAqfQpvrK9wF0gW9c9kY2i/8Ya6BBrGhk+w9yheUWaCKAFgMkF2yYuX7fKVRtHdGlK8kQ78XCx+LVWXQWC06ddvbp+Tqj41Qy1VI+OVaAJ9VW09KU61FXBa9oA9B4WtM7K0VIE2wgYDHLC1e7j9ZkLHL6QHbHto/eUMygE44Zrcs5yCvg06ww4Sn4pgQczIXNT90sSwJZnPyPMindPVHW5iUFciUXzzABYlkkM+t1JUjMzk6jnuU0YfnvPOH3i9N42pqiV50yYkRzM8VE3Fqn29IceUrSoVMuXJP4dqWPs+J0HnBpFtanil4FhOJbMCwao0v33DKBitmPe0/Oc+FX1elF8fwsFGCD6Ez47Y7kg0eXpHxaX+1ZRdFoSx3+qDhhmru700yxn1IHKNISwhx//87WZZWc0HGogXQ8EWhLtpGiVMNH7a/7aYV0wXV7XIfmoy8Y23Yl5UiXdOl4IHJ/nGEckA4QfH7OysevCiPo5yWLeCG8LJ2HgQBDu38plNAcYw6nN0LWnXi7K1UjnjP0ok4MobjP1FngVz6GVUrGoIztG8uA2WdWUq8eMuQ5zDIa5NHrLex3jkTyE9kpn7pdonC6Q3SRHjBsKnxhWqL8nXmXNPZbHyNrB/kmF62s="
"webhook-timestamp" : "1740698913"
Here's my version to verify the data using only Bouncy Castle API.
using System.Text;
using System.Text.Json.Nodes;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
internal class Program
{
private const string TILLO_CA_CERT_URL = "https://ca.tillo.io/";
private static HttpClient _httpClient = new();
private static async Task<bool> Verify(Dictionary<string, string> headers, byte[] responseBody)
{
// get certificate from webhook response
var jo = JsonNode.Parse(Encoding.UTF8.GetString(responseBody));
var signingCertStr = jo["certificate"].ToString();
// download CA cert
var caCertBytes = await _httpClient.GetByteArrayAsync(TILLO_CA_CERT_URL);
// parse certificates
var certParser = new X509CertificateParser();
var caCert = certParser.ReadCertificate(caCertBytes);
var signingCert = certParser.ReadCertificate(Encoding.UTF8.GetBytes(signingCertStr));
// verify certificate is the correct chain
signingCert.CheckValidity();
signingCert.Verify(caCert.GetPublicKey());
// create payload
var payloadData = Encoding.UTF8.GetString(responseBody);
var dataConcat = $"{headers["webhook-id"]}.{headers["webhook-timestamp"]}.{payloadData}";
// get as bytes
var dataBytes = Encoding.UTF8.GetBytes(dataConcat);
var signatureBytes = Convert.FromBase64String(headers["webhook-signature"].Replace("v1a,", ""));
// verify signature - SHA-256withRSAandMGF1
// https://github.com/bcgit/bc-csharp/blob/master/crypto/src/security/SignerUtilities.cs
var signer = SignerUtilities.GetSigner("SHA-256withRSAandMGF1");
signer.Init(false, signingCert.GetPublicKey());
signer.BlockUpdate(dataBytes, 0, dataBytes.Length);
return signer.VerifySignature(signatureBytes);
}
private static async Task Main(string[] args)
{
// webhook data
var webhookHeaders = new Dictionary<string, string>() {
{ "webhook-id", "6b8aa749707b77c4be" },
{ "webhook-signature", "v1a,fiU3Rd66lIFFqUP4F0/knu33LmhlJrlr+8LHkcVo5wKiLZ23c60Dodi+kAugvObr4sRX+rqOmYJ7+uENl/irhqb+OCBGnvC0ydP/Bo7wu7wFHF8JZFI1diW2fN9IC1jybKAvN+BNQoMMlL/hUceMV3HHWjLMVL742NiEXWHG9eAV7GHfdGFP0US8rJLfPbafwbqDtO+jnIAH/W0ve6jz821Ky2hFHPt1+W+qjYlBN8UC54cYTLpKTxF3ZkaC4WwWWpSXHClgjyxkXSXJ8NJzOx02fO/0RiJZYWSbp8yrURvBvrdTumSl9bQ/a11O6WBaqGASkZBmsyncng9+oa79/Kj2tEsQ/b2SeZEXEdfT28Vhnvlk4uWyWswC965jMfyCp66VJak8KakBBNtZ1gSiHPzUakQagz3bK0/5WBVdUukwx9I/cUzK3kFbTrqpvwW+PvB5MdYb1ove7FG47W3ZEQ2lvF+62NhiNPVyYzyw7y4DyRtHm6KBlZwpE2M6FE/cVmQNilCOYD2ttd99WrXskMYTH8wV3HEu9Xz34TmThQjCtHsRcMX2AmXMUJGEGCbYe//eU7xVvfr8WwUM7gIBnD92t5s8bVkN/y/uT4WIl/OsBvcipBzVFxZxxAb7oiGjPl6SaQ/f0aaa9j7+FIbh8/PIK/VNL7qUxQiM+gfdE80="},
{ "webhook-timestamp", "1740137197"}
};
var webhookBody = @"{""type"":""brands.status.updated"",""timestamp"":""2025-02-21T11:26:34Z"",""certificate"":""-----BEGIN CERTIFICATE-----\r\nMIIGBzCCA++gAwIBAgIUCMkS5Ti+joHJ4U2y2x4IzqC+0nMwDQYJKoZIhvcNAQEL\r\nBQAwgZ0xFDASBgNVBAMMC1RpbGxvQ0FSb290MQswCQYDVQQGEwJHQjEPMA0GA1UE\r\nCAwGU3Vzc2V4MRgwFgYDVQQHDA9CcmlnaHRvbiAmIEhvdmUxDjAMBgNVBAoMBVRp\r\nbGxvMRYwFAYDVQQLDA1QbGF0Zm9ybSBUZWFtMSUwIwYJKoZIhvcNAQkBFhZwbGF0\r\nZm9ybS50ZWFtQHRpbGxvLmlvMB4XDTI0MTIxMDE1MDk0NloXDTI5MTIwOTE1MDk0\r\nNlowZzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBlN1c3NleDEOMAwGA1UECgwFVGls\r\nbG8xFjAUBgNVBAsMDVBsYXRmb3JtIFRlYW0xHzAdBgNVBAMMFnRpbGxvLXdlYmhv\r\nb2tzLXN0YWdpbmcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJf16N\r\nTnvyIgEg88tl7B+2ZoOsDnYTMTBf12zSRdNre5KhDYWgXQ5Bgd3dx412VSU1eoWN\r\nHV7494I\/Vm1HQBNBj63uCAcivykOZttYayj4tPIv5qL4LzO2rZVzeRkJMe1bK6E9\r\nwoABGhLQDO9QUnxzQykPIBTjt0lE0PY1uGiJxzrn99jcDMKgM1p9AfQrJQIV3BzQ\r\nz27dS4vQzcIvVgqbVRt1ZPU8jlbo6HuEexOM47NVToll83T8hLJG1FzDkbJ4HyCm\r\n3UFEEp20fMnxMtRgipp5OttjOjfVKUHaUQEawf9B8LZM9WRxIIcQHV1LvQ0BYIRO\r\n5PoWd6dV9khakAPMlynHu68FnJPF5xbMaURW9mcp++s8r4OvVBP2d07jh9pkze45\r\nHo0TTza3F1vu2eMq72rETQhEwjeR0gAhT\/87dJDTKgiI+jPfsh6NxH4PVsvqXl8h\r\nKa5cRpOOvFn0oI4OKMovuQ1FA5Q+Q9Ttj1QAnfUqj2mYm2PmIcH3ljUExjtsgfXG\r\nxf67neF\/lum5ZYD0njeWqynkbEBr61VKXnCdNGfSfsgytbTeGPbrlAXlJ8Fa4JSp\r\nNGQOi99DDnyR1QRiVjESoqME+ps1GkZB7Za5f28fbcOb3clL9YHEo8PDmUwfsm5J\r\nt1CfrpUeQrI6xmX6HWkprp5yBXY63ZIEQcIUtQIDAQABo3QwcjAwBgNVHR8EKTAn\r\nMCWgI6Ahhh9odHRwczovL2NhLnRpbGxvLmlvL2NybC9jcmwucGVtMB0GA1UdDgQW\r\nBBQyNSvOXcnhPIdncEPJSdXJtZdcsjAfBgNVHSMEGDAWgBSkN2+VN1dBCA8ay0zz\r\nxK1dw8K+RjANBgkqhkiG9w0BAQsFAAOCAgEAEmn8EknSOTrH90b3ec1yKJ5EeHKY\r\nqY5OeoFS4NHWpVcJ4wpo80r+zzxFJjD4kdoUDi4fcsOh4XC8OnFk6QW0fVcJbrp3\r\nuSyInD19aom7FNz+qWpPdcIg2ZNsCJ64TR6NR8EjZtqjLMR4+J\/H7Aqb+VL2mFTC\r\nvQlMQsmmTa6fC4IYAd8woyfPF+Y56z81hZPNNaWQhvby52bzO27kLKCJmN2eZN\/Q\r\nmmNIwQZnEb8UdCoEuzS8iElPGGniB4x\/uXwhkE9kIJhmjHhIFVm1hWTLWYO3hcmP\r\nWBnVjplhx3Un9i1gTXeCrmpetCEGtRoHuIFQ8BruS+XFja3eReqHuz4Dhwtd3L6y\r\nm+Ni2PkiVe4wTGKCD37h9FTTbKiNt1ImR07+Zpy5edG2i\/LD3LzH00rs\/e41IfcB\r\nlmxd5QuI7Cjp7kdHzGB6oMtYBawMOFIRYgfGeCNE\/7EgSFsW6wExfEZz1hNO7Cm1\r\nvQe8pe9s\/b3tGovvLZQf0F6CRBd6VtpyxztmWQRIsUNgXl3LMCnawP0JmnR6wQDi\r\nq9H0SIzeK52YoEFxQNnItqvNLLhjOFI1TEd16AaILPyvQaPOKOj0U4KqnBd+BR2s\r\njdabaA9YPN09PiTIFbONBkwq1ZyGcF9gHCiRkW6jISuc17tf5w49sdeERzDttmwi\r\n5qiGBh41yjM59Ks=\r\n-----END CERTIFICATE-----"",""version"":1,""data"":[{""name"":""Costa"",""slug"":""costa"",""status"":{""code"":""DISABLED"",""reason"":""TEST""}},{""name"":""Farmfoods"",""slug"":""farmfoods"",""status"":{""code"":""DISABLED"",""reason"":""TEST""}},{""name"":""HelloFresh"",""slug"":""hello-fresh"",""status"":{""code"":""DISABLED"",""reason"":""TEST""}},{""name"":""Nike"",""slug"":""nike-usa"",""status"":{""code"":""DISABLED"",""reason"":""TEST""}}]}";
var webhookBodyBytes = Encoding.UTF8.GetBytes(webhookBody);
var isValid = await Verify(webhookHeaders, webhookBodyBytes);
if (!isValid)
{
Console.WriteLine("Signature is invalid");
}
else
{
Console.WriteLine("Signature is valid");
}
}
}
The code correctly validated the provided certificate from the webhook response and from the downloaded CA file, ...but it sill fails to validate the payload since I think the payload string is not the same as the original payload returned from the webhook.
Now, if you're using ASP.NET Core, you can use the following code to get the original body from the webhook call. This code is adapted from here.
var ms = new MemoryStream();
var bodyStream = HttpContext.Request.Body;
bodyStream.Position = 0;
await bodyStream.CopyToAsync(ms);
var webhookBodyBytes = ms.ToArray();