From 63153354b09d0a651edfe334528a35613b65835e Mon Sep 17 00:00:00 2001 From: Jon Erickson Date: Fri, 17 Nov 2023 11:27:10 -0800 Subject: [PATCH 1/4] Adds support for mutual TLS --- src/CallWebhookJob.php | 18 ++++++++++++++++-- src/WebhookCall.php | 10 ++++++++++ tests/CallWebhookJobTest.php | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/CallWebhookJob.php b/src/CallWebhookJob.php index dd192bb..89a1c1c 100644 --- a/src/CallWebhookJob.php +++ b/src/CallWebhookJob.php @@ -33,6 +33,14 @@ class CallWebhookJob implements ShouldQueue public int $requestTimeout; + public ?string $cert = null; + + public ?string $certPassword = null; + + public ?string $sslKey = null; + + public ?string $sslKeyPassword = null; + public string $backoffStrategyClass; public ?string $signerClass = null; @@ -132,14 +140,20 @@ protected function createRequest(array $body): Response { $client = $this->getClient(); - return $client->request($this->httpVerb, $this->webhookUrl, array_merge([ + return $client->request($this->httpVerb, $this->webhookUrl, array_merge( + [ 'timeout' => $this->requestTimeout, 'verify' => $this->verifySsl, 'headers' => $this->headers, 'on_stats' => function (TransferStats $stats) { $this->transferStats = $stats; }, - ], $body, is_null($this->proxy) ? [] : ['proxy' => $this->proxy])); + ], + $body, + is_null($this->proxy) ? [] : ['proxy' => $this->proxy], + is_null($this->cert) ? [] : ['cert' => [$this->cert, $this->certPassword]], + is_null($this->sslKey) ? [] : ['ssl_key' => [$this->sslKey, $this->sslKeyPassword]] + )); } protected function shouldBeRemovedFromQueue(): bool diff --git a/src/WebhookCall.php b/src/WebhookCall.php index d4ac165..f76ab74 100644 --- a/src/WebhookCall.php +++ b/src/WebhookCall.php @@ -117,6 +117,16 @@ public function maximumTries(int $tries): self return $this; } + public function mutualTls(string $cert, string $sslKey, ?string $certPassword = null, ?string $sslKeyPassword = null): self + { + $this->callWebhookJob->cert = $cert; + $this->callWebhookJob->certPassword = $certPassword; + $this->callWebhookJob->sslKey = $sslKey; + $this->callWebhookJob->sslKeyPassword = $sslKeyPassword; + + return $this; + } + public function useBackoffStrategy(string $backoffStrategyClass): self { if (! is_subclass_of($backoffStrategyClass, BackoffStrategy::class)) { diff --git a/tests/CallWebhookJobTest.php b/tests/CallWebhookJobTest.php index e9ab523..2647058 100644 --- a/tests/CallWebhookJobTest.php +++ b/tests/CallWebhookJobTest.php @@ -184,6 +184,40 @@ function baseGetRequest(array $overrides = []): array ->assertRequestsMade([$baseRequest]); }); +it('will use mutual TLS without passphrases', function () { + baseWebhook() + ->mutualTls('foobar', 'barfoo') + ->dispatch(); + + $baseRequest = baseRequest(); + + $baseRequest['options']['cert'] = ['foobar', null]; + $baseRequest['options']['ssl_key'] = ['barfoo', null]; + + artisan('queue:work --once'); + + $this + ->testClient + ->assertRequestsMade([$baseRequest]); +}); + +it('will use mutual TLS with passphrases', function () { + baseWebhook() + ->mutualTls('foobar', 'barfoo', 'foobarpassword', 'barfoopassword') + ->dispatch(); + + $baseRequest = baseRequest(); + + $baseRequest['options']['cert'] = ['foobar', 'foobarpassword']; + $baseRequest['options']['ssl_key'] = ['barfoo', 'barfoopassword']; + + artisan('queue:work --once'); + + $this + ->testClient + ->assertRequestsMade([$baseRequest]); +}); + it('will use a proxy', function () { baseWebhook() ->useProxy('https://proxy.test') From df5fe93b7d9dc84ab666bfbbe76cfae6ac1fd846 Mon Sep 17 00:00:00 2001 From: Jon Erickson Date: Fri, 17 Nov 2023 11:35:03 -0800 Subject: [PATCH 2/4] Rename properties --- src/CallWebhookJob.php | 8 ++++---- src/WebhookCall.php | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/CallWebhookJob.php b/src/CallWebhookJob.php index 89a1c1c..21b86ed 100644 --- a/src/CallWebhookJob.php +++ b/src/CallWebhookJob.php @@ -35,11 +35,11 @@ class CallWebhookJob implements ShouldQueue public ?string $cert = null; - public ?string $certPassword = null; + public ?string $certPassphrase = null; public ?string $sslKey = null; - public ?string $sslKeyPassword = null; + public ?string $sslKeyPassphrase = null; public string $backoffStrategyClass; @@ -151,8 +151,8 @@ protected function createRequest(array $body): Response ], $body, is_null($this->proxy) ? [] : ['proxy' => $this->proxy], - is_null($this->cert) ? [] : ['cert' => [$this->cert, $this->certPassword]], - is_null($this->sslKey) ? [] : ['ssl_key' => [$this->sslKey, $this->sslKeyPassword]] + is_null($this->cert) ? [] : ['cert' => [$this->cert, $this->certPassphrase]], + is_null($this->sslKey) ? [] : ['ssl_key' => [$this->sslKey, $this->sslKeyPassphrase]] )); } diff --git a/src/WebhookCall.php b/src/WebhookCall.php index f76ab74..351398c 100644 --- a/src/WebhookCall.php +++ b/src/WebhookCall.php @@ -117,12 +117,12 @@ public function maximumTries(int $tries): self return $this; } - public function mutualTls(string $cert, string $sslKey, ?string $certPassword = null, ?string $sslKeyPassword = null): self + public function mutualTls(string $certPath, string $sslKeyPath, ?string $certPassphrase = null, ?string $sslKeyPassphrase = null): self { - $this->callWebhookJob->cert = $cert; - $this->callWebhookJob->certPassword = $certPassword; - $this->callWebhookJob->sslKey = $sslKey; - $this->callWebhookJob->sslKeyPassword = $sslKeyPassword; + $this->callWebhookJob->cert = $certPath; + $this->callWebhookJob->certPassphrase = $certPassphrase; + $this->callWebhookJob->sslKey = $sslKeyPath; + $this->callWebhookJob->sslKeyPassphrase = $sslKeyPassphrase; return $this; } From 7d2bd74ab2c8be82eb9a85f2c07e4d0f562ba698 Mon Sep 17 00:00:00 2001 From: Jon Erickson Date: Fri, 17 Nov 2023 11:43:11 -0800 Subject: [PATCH 3/4] Updated README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 27ab217..8077328 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,23 @@ WebhookCall::create() ... ``` +### Using mutual TLS authentication + +To safeguard the integrity of webhook data transmission, it's critical to authenticate the intended recipient of your webhook payload. +Mutual TLS authentication serves as a robust method for this purpose. Contrary to standard TLS, where only the client verifies the server, +mutual TLS requires both the webhook endpoint (acting as the client) and the webhook provider (acting as the server) to authenticate each other. +This is achieved through an exchange of certificates during the TLS handshake, ensuring that both parties confirm each other's identity. + +```php +WebhookCall::create() + ->mutualTls( + certPath: storage_path('path/to/cert.pem'), + certPassphrase: 'optional_cert_passphrase', + sslKeyPath: storage_path('path/to/key.pem'), + sslKeyPassphrase: 'optional_key_passphrase' + ) +``` + The proxy specification follows the [guzzlehttp proxy format](https://docs.guzzlephp.org/en/stable/request-options.html#proxy) ### Verifying the SSL certificate of the receiving app From 9fd4678885c6dfe3e336797a7aa23e1dbd5ac52e Mon Sep 17 00:00:00 2001 From: Jon Erickson Date: Fri, 17 Nov 2023 11:51:51 -0800 Subject: [PATCH 4/4] Add support for custom CA's --- README.md | 2 ++ src/CallWebhookJob.php | 2 +- src/WebhookCall.php | 2 +- tests/CallWebhookJobTest.php | 19 +++++++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8077328..e1447cb 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,8 @@ Mutual TLS authentication serves as a robust method for this purpose. Contrary t mutual TLS requires both the webhook endpoint (acting as the client) and the webhook provider (acting as the server) to authenticate each other. This is achieved through an exchange of certificates during the TLS handshake, ensuring that both parties confirm each other's identity. +> Note: If you need to include your own certificate authority, pass the certificate path to the `verifySsl()` method. + ```php WebhookCall::create() ->mutualTls( diff --git a/src/CallWebhookJob.php b/src/CallWebhookJob.php index 21b86ed..3f7c33f 100644 --- a/src/CallWebhookJob.php +++ b/src/CallWebhookJob.php @@ -47,7 +47,7 @@ class CallWebhookJob implements ShouldQueue public array $headers = []; - public bool $verifySsl; + public string|bool $verifySsl; public bool $throwExceptionOnFailure; diff --git a/src/WebhookCall.php b/src/WebhookCall.php index 351398c..06c7ec3 100644 --- a/src/WebhookCall.php +++ b/src/WebhookCall.php @@ -170,7 +170,7 @@ public function withHeaders(array $headers): self return $this; } - public function verifySsl(bool $verifySsl = true): self + public function verifySsl(bool|string $verifySsl = true): self { $this->callWebhookJob->verifySsl = $verifySsl; diff --git a/tests/CallWebhookJobTest.php b/tests/CallWebhookJobTest.php index 2647058..79bf0ac 100644 --- a/tests/CallWebhookJobTest.php +++ b/tests/CallWebhookJobTest.php @@ -218,6 +218,25 @@ function baseGetRequest(array $overrides = []): array ->assertRequestsMade([$baseRequest]); }); +it('will use mutual TLS with certificate authority', function () { + baseWebhook() + ->mutualTls('foobar', 'barfoo') + ->verifySsl('foofoo') + ->dispatch(); + + $baseRequest = baseRequest(); + + $baseRequest['options']['cert'] = ['foobar', null]; + $baseRequest['options']['ssl_key'] = ['barfoo', null]; + $baseRequest['options']['verify'] = 'foofoo'; + + artisan('queue:work --once'); + + $this + ->testClient + ->assertRequestsMade([$baseRequest]); +}); + it('will use a proxy', function () { baseWebhook() ->useProxy('https://proxy.test')