diff --git a/src/Webhook/WebhookVerifier.php b/src/Webhook/WebhookVerifier.php index f8bb89a7..fcc6e77a 100644 --- a/src/Webhook/WebhookVerifier.php +++ b/src/Webhook/WebhookVerifier.php @@ -2,8 +2,18 @@ namespace Fingerprint\ServerAPI\Webhook; +/** + * Verifies Fingerprint webhook signature. + */ final class WebhookVerifier { + /** + * Checks whether the webhook signature header is valid for the given data and secret. + * + * @param string $header comma-separated list of versioned signatures + * @param string $data raw webhook request body + * @param string $secret webhook signing secret + */ public static function IsValidWebhookSignature(string $header, string $data, string $secret): bool { $signatures = explode(',', $header); @@ -20,6 +30,9 @@ public static function IsValidWebhookSignature(string $header, string $data, str return false; } + /** + * Compares the given signature against an HMAC-SHA256 hash of the data. + */ private static function checkSignature(string $signature, string $data, string $secret): bool { $hash = hash_hmac('sha256', $data, $secret); diff --git a/test/WebhookVerifierTest.php b/test/WebhookVerifierTest.php index db0a5896..73577339 100644 --- a/test/WebhookVerifierTest.php +++ b/test/WebhookVerifierTest.php @@ -5,45 +5,69 @@ use Fingerprint\ServerAPI\Webhook\WebhookVerifier; use PHPUnit\Framework\TestCase; +/** + * Tests for webhook signature verification. + */ class WebhookVerifierTest extends TestCase { - private $secret = 'secret'; - private $data = 'data'; + private string $secret = 'secret'; + private string $data = 'data'; - public function testWithValidSignature() + /** + * Verifies that a valid v1 signature is accepted. + */ + public function testWithValidSignature(): void { + /** @noinspection SpellCheckingInspection */ $validHeader = 'v1=1b2c16b75bd2a870c114153ccda5bcfca63314bc722fa160d690de133ccbb9db'; $result = WebhookVerifier::IsValidWebhookSignature($validHeader, $this->data, $this->secret); $this->assertTrue($result, 'With valid signature'); } - public function testWithInvalidHeader() + /** + * Verifies that a non-v1 signature version is rejected. + */ + public function testWithInvalidHeader(): void { $result = WebhookVerifier::IsValidWebhookSignature('v2=invalid', $this->data, $this->secret); $this->assertFalse($result, 'With invalid header'); } - public function testWithHeaderWithoutVersion() + /** + * Verifies that a header without a version prefix is rejected. + */ + public function testWithHeaderWithoutVersion(): void { $result = WebhookVerifier::IsValidWebhookSignature('invalid', $this->data, $this->secret); $this->assertFalse($result, 'With header without version'); } - public function testWithEmptyHeader() + /** + * Verifies that an empty header is rejected. + */ + public function testWithEmptyHeader(): void { $result = WebhookVerifier::IsValidWebhookSignature('', $this->data, $this->secret); $this->assertFalse($result, 'With empty header'); } - public function testWithEmptySecret() + /** + * Verifies that an empty secret never matches. + */ + public function testWithEmptySecret(): void { + /** @noinspection SpellCheckingInspection */ $validHeader = 'v1=1b2c16b75bd2a870c114153ccda5bcfca63314bc722fa160d690de133ccbb9db'; $result = WebhookVerifier::IsValidWebhookSignature($validHeader, $this->data, ''); $this->assertFalse($result, 'With empty secret'); } - public function testWithEmptyData() + /** + * Verifies that empty data does not match a signature computed with non-empty data. + */ + public function testWithEmptyData(): void { + /** @noinspection SpellCheckingInspection */ $validHeader = 'v1=1b2c16b75bd2a870c114153ccda5bcfca63314bc722fa160d690de133ccbb9db'; $result = WebhookVerifier::IsValidWebhookSignature($validHeader, '', $this->secret); $this->assertFalse($result, 'With empty data');