 
				PayPal has deprecated their .NET Checkout SDK. However by forking the code and making some improvements; their is new life for the once retired project. In this article, we do a quick summary of the new features added to the Elcamino.PayPalCheckoutSDK and how to verify a PayPal webhook request.
New Features
v2.0 - v2.2
- Small perf increase in VerifyWebhookEvent.ValidateReceivedEventAsync
- Adding VerifyWebhookEvent.ValidateReceivedEventAsync to validate webhook events client side
- #10 fix for token expiration tracking in AccessToken
- Adding .net6 and .net7 support
- Adding System.Text.Json serialization options
- Dropping .netstandard support
- ElCamino.PayPalHttp 2.0 (HttpClient performance improvements)
v1.9
- .netstandard support
- Adding ElCamino.PayPalHttp 1.9 regex performance improvements using .net 7 code generation.
- Full support for Paypal Webhooks https://developer.paypal.com/docs/api/webhooks/v1/
- Full billing support via products, plans and subscriptions https://developer.paypal.com/docs/api/subscriptions/v1/
- Code cleanup, mostly member formatting
Let's verify a webhook
Add a reference to nuget Elcamino.PayPalCheckoutSDK
dotnet add package ElCamino.PayPalCheckoutSdk --version 2.2.0
or
<PackageReference Include="ElCamino.PayPalCheckoutSdk" Version="2.2.0" />
Here is a sample MVC action that would accept a Post method, validate the payload
        public async Task<IActionResult> Post()
        {
            using StreamReader reader = new StreamReader(Request.Body);
            string requestWebEventBody = await reader.ReadToEndAsync();
            var verifySignature = new VerifyWebhookSignature()
            {
                CertUrl = Request.Headers[PayPalCheckoutSdk.HeaderNameConstants.VerifySignature.CertUrl],
                AuthAlgo = Request.Headers[PayPalCheckoutSdk.HeaderNameConstants.VerifySignature.AuthAlgo],
                TransmissionId = Request.Headers[PayPalCheckoutSdk.HeaderNameConstants.VerifySignature.TransmissionId],
                TransmissionSig = Request.Headers[PayPalCheckoutSdk.HeaderNameConstants.VerifySignature.TransmissionSig],
                TransmissionTime = Request.Headers[PayPalCheckoutSdk.HeaderNameConstants.VerifySignature.TransmissionTime],
                WebhookId = "<your webhook id>",
                WebhookEventRequestBody = requestWebEventBody
            };
            bool isValid = await VerifyWebhookEvent.ValidateReceivedEventAsync(verifySignature);
            //Do something based on webhook data being validated or not
            //Further process the payload based on https://developer.paypal.com/docs/api/webhooks/v1/
            return NoContent();
What does VerifyWebhookEvent.ValidateReceivedEventAsync(verifySignature) do?
- Makes sure all of the signature attributes are available
- Calculates a CRC32 checksum using the request body.
- Generates the expected signature.
- Loads the remote trusted certificate from CertUrl.
- Get the local certificate thumbprint from the embedded resource. This certificate was distributed with an earlier version of the PayPal SDK.
- Create and configure the X509Chain object to validate remote certificate chain.
- Check remote certificate (from CertUrl) contains the local thumbprint in the certificate chain.
- Finally, verify the received signature matches the expected signature.
    public static class VerifyWebhookEvent
    {
        private const string WithRSAToken = "withRSA";
        private static string? _publicLocalCertificateThumbprint;
        public static async Task<bool> ValidateReceivedEventAsync(VerifyWebhookSignature verifySignature)
        {
            if (string.IsNullOrWhiteSpace(verifySignature.TransmissionTime))
            {
                throw new ArgumentNullException(nameof(verifySignature), $"{nameof(verifySignature.TransmissionTime)} is null or empty");
            }
            if (string.IsNullOrWhiteSpace(verifySignature.TransmissionId))
            {
                throw new ArgumentNullException(nameof(verifySignature), $"{nameof(verifySignature.TransmissionId)} is null or empty");
            }
            if (string.IsNullOrWhiteSpace(verifySignature.TransmissionSig))
            {
                throw new ArgumentNullException(nameof(verifySignature), $"{nameof(verifySignature.TransmissionSig)} is null or empty");
            }
            if (string.IsNullOrWhiteSpace(verifySignature.AuthAlgo))
            {
                throw new ArgumentNullException(nameof(verifySignature), $"{nameof(verifySignature.AuthAlgo)} is null or empty");
            }
            if (string.IsNullOrWhiteSpace(verifySignature.CertUrl))
            {
                throw new ArgumentNullException(nameof(verifySignature), $"{nameof(verifySignature.CertUrl)} is null or empty");
            }
            if (string.IsNullOrWhiteSpace(verifySignature.WebhookEventRequestBody))
            {
                throw new ArgumentNullException(nameof(verifySignature), $"{nameof(verifySignature.WebhookEventRequestBody)} is null or empty");
            }
            if (string.IsNullOrWhiteSpace(verifySignature.WebhookId))
            {
                throw new ArgumentNullException(nameof(verifySignature), $"{nameof(verifySignature.WebhookId)} is null or empty");
            }
            // Convert the provided auth alrogithm header into a known hash alrogithm name.
            if (!verifySignature.AuthAlgo.EndsWith(WithRSAToken))
            {
                throw new ArgumentException($"{nameof(verifySignature.AuthAlgo)} must end with {WithRSAToken}", nameof(verifySignature));
            }
            string hashAlgorithm = verifySignature.AuthAlgo.Replace(WithRSAToken, "");
            string? oid = CryptoConfig.MapNameToOID(hashAlgorithm);
            if (string.IsNullOrWhiteSpace(oid))
            {
                throw new Exception($"Invalid OID from {hashAlgorithm}");
            }
            // Calculate a CRC32 checksum using the request body.
            byte[] bytes = Encoding.UTF8.GetBytes(verifySignature.WebhookEventRequestBody);
            uint crc32 = Crc32Algorithm.Compute(bytes);
            // Generate the expected signature.
            var expectedSignature = string.Format("{0}|{1}|{2}|{3}", verifySignature.TransmissionId, verifySignature.TransmissionTime, verifySignature.WebhookId, crc32);
            var expectedSignatureBytes = Encoding.UTF8.GetBytes(expectedSignature);
            // Load the remote trusted certificate.
            using System.Net.Http.HttpClient httpClient = new System.Net.Http.HttpClient();
            byte[] certificateBytes = await httpClient.GetByteArrayAsync(verifySignature.CertUrl);
            X509Certificate2Collection remoteCertificateCollection = new X509Certificate2Collection();
            remoteCertificateCollection.Import(certificateBytes);
            //Get the local cert thumbprint from the embedded resource 
            if (string.IsNullOrWhiteSpace(_publicLocalCertificateThumbprint))
            {
                using var publicCertStream = typeof(Event).Assembly.GetManifestResourceStream("PayPalCheckoutSdk.Webhooks.DigiCertSHA2ExtendedValidationServerCA.crt");
                byte[] resourceBytes = new byte[publicCertStream!.Length];
                _ = await publicCertStream.ReadAsync(resourceBytes);
                X509Certificate2 publicLocalCertificate = new X509Certificate2(resourceBytes);
                _publicLocalCertificateThumbprint = publicLocalCertificate.Thumbprint;
            }
            // Create and configure the X509Chain object to validate remote certificate
            using X509Chain chain = new X509Chain();
            chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; 
            chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
            chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
            bool validateRemoteCert = chain.Build(remoteCertificateCollection[0]);
            if (!validateRemoteCert)
            {
                throw new Exception($"Invalid chain on remote certificate {verifySignature.CertUrl}");
            }
            // Validate the certificate chain.
            bool validateChain = chain.ChainElements.Any(a => a.Certificate.Thumbprint == (_publicLocalCertificateThumbprint?? string.Empty));
            if (!validateChain)
            {
                throw new Exception($"Invalid remote certificate, public key not found in chain {verifySignature.CertUrl}");
            }
            // Verify the received signature matches the expected signature.
            using var rsa = remoteCertificateCollection[0].GetRSAPublicKey() ?? throw new Exception($"GetRSAPublicKey() failed on remote certificate, {verifySignature.CertUrl}");
            var signatureBytes = Convert.FromBase64String(verifySignature.TransmissionSig);
            return rsa.VerifyData(expectedSignatureBytes, signatureBytes, HashAlgorithmName.FromOid(oid), RSASignaturePadding.Pkcs1);
        }
    }
Warning
This VerifyWebhookEvent.ValidateReceivedEventAsync() method is only available .NET 6 and higher.
Summary
This was a quick summary of a great .NET PayPal resource to accelerate your integration with the latest versions. Also, we took a look a validation a webhook payload with only few lines of code with a breakdown of the verify signature process.
 the cloud way
the cloud way
       
        								 
        								
Open a discussion Tweet