I have been struggling with the PayPal Event header validation (https://developer.paypal.com/api/rest/webhooks/). What at first seemed like a quick and easy way to validate a payload has become very frustrating. I believe the premise is simple. PayPal is going to send my endpoint a transaction event. To provide a means to verify, they include in their headers, a transmission Id and Time. They know my webhookId, so they create a transmission signature based on the following format: ||| The crc32 is the payload.
In order to decrypt the transmission signature, they include in the request header, Paypal-Auth-Algo, letting me know that their encryption method (SHA256withRSA), and where to obtain the public key (Paypal-Cert-Url) that is used in decryption. With that information I can get the certificate, extract the public key from it using RSA, then decrypt the tranmission signature using the public key. The decrypted signature can then be compared with the tranmission id, transmission time, webhookid and the payload (crs32) that was received. The only issue that seems to be missing is what RSAEncryptionPadding they used.
Going through all that, I keep throwing exceptions at the critical point regardless of the EncryptionPadding I try.
rsaPublicKey.Decrypt(signature, RASEncryptionPadding.OaepSHA256);
It appears that there is something wrong with the Certificate or I am simply not processing it correctly.
Here is my code, Get the Public Key:
public static async Task<string?> GetPublicKey(string Paypal-Cert-Url)
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response = await client.GetAsync(Paypal-Cert-Url);
if (response.IsSuccessStatusCode)
{
var key = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrEmpty(key))
{
key = key.Replace("-----BEGIN CERTIFICATE-----", "")
.Replace("-----END CERTIFICATE-----", "")
.Replace("\n", "")
.Replace("\r", "");
return key;
}
}
}
return null;
}
And to do the match:
public static bool VerifySignature(string validationString, string? certificateContent, string transmissionSig)
{
byte[] certificateBytes = Convert.FromBase64String(certificateContent);
X509Certificate2 certificate = new X509Certificate2(certificateBytes);
if (certificate != null)
{
RSA? rsaPublicKey = certificate.GetRSAPublicKey();
bool isSignatureValid = false;
if (rsaPublicKey != null)
{
try
{
byte[] signature = Convert.FromBase64String(transmissionSig);
byte[] encryptedSignature = rsaPublicKey.Decrypt(signature, RSAEncryptionPadding.OaepSHA256);
string matchSignature = Encoding.UTF8.GetString(encryptedSignature);
isSignatureValid = matchSignature == transmissionSig;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
if (isSignatureValid)
{
return true;
}
}
}
return false;
}
The problem is the Decrypt() step. It is throwing a Key does not exist error. I have been trying to do this in the sandbox using the webhook simulator to generate the payload for my webhook. The code is c#.