forked from laniatech/Checkout-NET-SDK
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12 from dlmelendez/rel/2.1
v2.1
- Loading branch information
Showing
9 changed files
with
161 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+1.18 KB
PayPalCheckoutSdk/Webhooks/DigiCertSHA2ExtendedValidationServerCA.crt
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Security.Cryptography; | ||
using System.Security.Cryptography.X509Certificates; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Force.Crc32; | ||
|
||
namespace PayPalCheckoutSdk.Webhooks | ||
{ | ||
public static class VerifyWebhookEvent | ||
{ | ||
private const string WithRSAToken = "withRSA"; | ||
|
||
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); | ||
|
||
// Get the cert from the cache and load the 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); | ||
|
||
using var publicCertStream = typeof(Event).Assembly.GetManifestResourceStream("PayPalCheckoutSdk.Webhooks.DigiCertSHA2ExtendedValidationServerCA.crt"); | ||
byte[] resourceBytes = new byte[publicCertStream!.Length]; | ||
_ = await publicCertStream.ReadAsync(resourceBytes, 0, resourceBytes.Length); | ||
|
||
X509Certificate2 publicLocalCertificate = new X509Certificate2(resourceBytes); | ||
// Create and configure the X509Chain object | ||
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 == publicLocalCertificate.Thumbprint); | ||
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); | ||
|
||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -144,15 +144,15 @@ public async Task TestResendNegative() | |
public async Task TestVerifySignatureEventNegative() | ||
{ | ||
VerifyWebhookSignatureRequest simulateRequest = new VerifyWebhookSignatureRequest(); | ||
|
||
var verifySignature = new VerifyWebhookSignature() | ||
{ | ||
CertUrl = "https://example.com/65432123456-3467678768768", | ||
AuthAlgo = Guid.NewGuid().ToString("N"), | ||
TransmissionId = Guid.NewGuid().ToString("N"), | ||
TransmissionSig = Guid.NewGuid().ToString("N"), | ||
TransmissionTime = DateTime.UtcNow.ToPaypalString(), | ||
WebhookId = "3RD03749806828333", | ||
CertUrl = "https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-2d7ab011", | ||
AuthAlgo = "SHA256withRSA", | ||
TransmissionId = "d0a19f40-e46b-11ed-af6d-5d1995803275", | ||
TransmissionSig = "Tg9131sVOAPVYn5XjQsR8C/tcOWuPkc//VkifmPX7TZD24\u002BkuRLIZ\u002BzfbMxkeuS0er1EzLHw4MRG83xkYoEGLe9QWD4nfvg/HIAvXDNgEZCG2BrPIwPaiFrA9G0SX22\u002BvpOiy4\u002BbWNrKAFZdt/gobEairdaqVe1unsCxCJQT6czTFiOBBAn85yDCSIhctk6RbEkprxjwTrgLDf1Cq41AgxZ72RwVuZlJHbMQF5Dl/cRQ9pU38I0HOq0DRXiiaJwrp7UJXkLRdu3ge4ivN3Th1Wq8D\u002BL/0xYrub9lFB0TKI2a7XBKlaua9aT7XrtuwZeI1cNz/jr0luz7K6JYdiZSlQ==", | ||
TransmissionTime = "2023-04-26T19:52:03Z", | ||
WebhookId = "3RY87287BU229431J", | ||
WebhookEvent = new Event() { Id = Guid.NewGuid().ToString(), EventType = "PAYMENT.CAPTURE.COMPLETED" } | ||
}; | ||
simulateRequest.RequestBody(verifySignature); | ||
|
@@ -163,5 +163,27 @@ public async Task TestVerifySignatureEventNegative() | |
|
||
Assert.False(createResult.ValidSignature); | ||
} | ||
|
||
[Fact] | ||
public async Task TestVerifySignatureEventCert() | ||
{ | ||
var verifySignature = new VerifyWebhookSignature() | ||
{ | ||
CertUrl = "https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-2d7ab011", | ||
AuthAlgo = "SHA256withRSA", | ||
TransmissionId = "d0a19f40-e46b-11ed-af6d-5d1995803275", | ||
TransmissionSig = "Tg9131sVOAPVYn5XjQsR8C/tcOWuPkc//VkifmPX7TZD24\u002BkuRLIZ\u002BzfbMxkeuS0er1EzLHw4MRG83xkYoEGLe9QWD4nfvg/HIAvXDNgEZCG2BrPIwPaiFrA9G0SX22\u002BvpOiy4\u002BbWNrKAFZdt/gobEairdaqVe1unsCxCJQT6czTFiOBBAn85yDCSIhctk6RbEkprxjwTrgLDf1Cq41AgxZ72RwVuZlJHbMQF5Dl/cRQ9pU38I0HOq0DRXiiaJwrp7UJXkLRdu3ge4ivN3Th1Wq8D\u002BL/0xYrub9lFB0TKI2a7XBKlaua9aT7XrtuwZeI1cNz/jr0luz7K6JYdiZSlQ==", | ||
TransmissionTime = "2023-04-26T19:52:03Z", | ||
WebhookId = "3RY87287BU229431J", | ||
WebhookEventRequestBody = "{\r\n \"create_time\": \"2023-04-26T19:51:57.324Z\",\r\n \"event_type\": \"BILLING.SUBSCRIPTION.ACTIVATED\",\r\n \"event_version\": \"1.0\",\r\n \"id\": \"WH-2GA799794C526164B-4HC631101W386110B\",\r\n \"links\": [\r\n {\r\n \"encType\": null,\r\n \"href\": \"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-2GA799794C526164B-4HC631101W386110B\",\r\n \"mediaType\": null,\r\n \"method\": \"GET\",\r\n \"rel\": \"self\",\r\n \"title\": null\r\n },\r\n {\r\n \"encType\": null,\r\n \"href\": \"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-2GA799794C526164B-4HC631101W386110B/resend\",\r\n \"mediaType\": null,\r\n \"method\": \"POST\",\r\n \"rel\": \"resend\",\r\n \"title\": null\r\n }\r\n ],\r\n \"resource\": {\r\n \"quantity\": \"1\",\r\n \"subscriber\": {\r\n \"email_address\": \"[email protected]\",\r\n \"payer_id\": \"P2DZ2LMXC5SY2\",\r\n \"name\": {\r\n \"given_name\": \"test\",\r\n \"surname\": \"buyer\"\r\n },\r\n \"shipping_address\": {\r\n \"address\": {\r\n \"address_line_1\": \"1 Main St\",\r\n \"admin_area_2\": \"San Jose\",\r\n \"admin_area_1\": \"CA\",\r\n \"postal_code\": \"95131\",\r\n \"country_code\": \"US\"\r\n }\r\n }\r\n },\r\n \"create_time\": \"2023-04-26T19:51:53Z\",\r\n \"plan_overridden\": false,\r\n \"shipping_amount\": {\r\n \"currency_code\": \"USD\",\r\n \"value\": \"0.0\"\r\n },\r\n \"start_time\": \"2023-04-26T19:51:38Z\",\r\n \"update_time\": \"2023-04-26T19:51:53Z\",\r\n \"billing_info\": {\r\n \"outstanding_balance\": {\r\n \"currency_code\": \"USD\",\r\n \"value\": \"0.0\"\r\n },\r\n \"cycle_executions\": [\r\n {\r\n \"tenure_type\": \"TRIAL\",\r\n \"sequence\": 1,\r\n \"cycles_completed\": 1,\r\n \"cycles_remaining\": 0,\r\n \"current_pricing_scheme_version\": 1,\r\n \"total_cycles\": 1\r\n },\r\n {\r\n \"tenure_type\": \"REGULAR\",\r\n \"sequence\": 2,\r\n \"cycles_completed\": 0,\r\n \"cycles_remaining\": 0,\r\n \"current_pricing_scheme_version\": 1,\r\n \"total_cycles\": 0\r\n }\r\n ],\r\n \"next_billing_time\": \"2023-05-10T10:00:00Z\",\r\n \"failed_payments_count\": 0\r\n },\r\n \"links\": [\r\n {\r\n \"href\": \"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-B9YUUVSYXFX3/cancel\",\r\n \"rel\": \"cancel\",\r\n \"method\": \"POST\",\r\n \"encType\": \"application/json\"\r\n },\r\n {\r\n \"href\": \"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-B9YUUVSYXFX3\",\r\n \"rel\": \"edit\",\r\n \"method\": \"PATCH\",\r\n \"encType\": \"application/json\"\r\n },\r\n {\r\n \"href\": \"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-B9YUUVSYXFX3\",\r\n \"rel\": \"self\",\r\n \"method\": \"GET\",\r\n \"encType\": \"application/json\"\r\n },\r\n {\r\n \"href\": \"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-B9YUUVSYXFX3/suspend\",\r\n \"rel\": \"suspend\",\r\n \"method\": \"POST\",\r\n \"encType\": \"application/json\"\r\n },\r\n {\r\n \"href\": \"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-B9YUUVSYXFX3/capture\",\r\n \"rel\": \"capture\",\r\n \"method\": \"POST\",\r\n \"encType\": \"application/json\"\r\n }\r\n ],\r\n \"id\": \"I-B9YUUVSYXFX3\",\r\n \"plan_id\": \"P-11V96330AW144370PMBGAOEA\",\r\n \"status\": \"ACTIVE\",\r\n \"status_update_time\": \"2023-04-26T19:51:53Z\"\r\n },\r\n \"resource_type\": \"subscription\",\r\n \"resource_version\": \"2.0\",\r\n \"summary\": \"Subscription activated\"\r\n }", | ||
}; | ||
|
||
bool verified = await VerifyWebhookEvent.ValidateReceivedEventAsync(verifySignature); | ||
// | ||
// Unfortunately, we it is too difficult to setup a positive test. | ||
// We are mainly checking for any exceptions during validation | ||
Assert.False(verified); | ||
|
||
} | ||
} | ||
} |