diff --git a/README.md b/README.md index 3339368..fc5eef2 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,9 @@ Framework-specific (quick starts): See the full indexed list at [`examples/README.md`](examples/README.md). +Webhooks: +- Verifying webhook signatures – [`webhooks/verify_signature.php`](examples/webhooks/verify_signature.php) + ## Contributing Bug reports and pull requests are welcome on [GitHub](https://github.com/railsware/mailtrap-php). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md). diff --git a/examples/webhooks/verify_signature.php b/examples/webhooks/verify_signature.php new file mode 100644 index 0000000..3a4d723 --- /dev/null +++ b/examples/webhooks/verify_signature.php @@ -0,0 +1,22 @@ +assertTrue( + WebhookSignature::verify( + self::FIXTURE_PAYLOAD, + self::FIXTURE_EXPECTED_SIGNATURE, + self::FIXTURE_SIGNING_SECRET + ) + ); + } + + // --- 2. Wrong secret --------------------------------------------------- + public function testReturnsFalseWithWrongSigningSecret(): void + { + $this->assertFalse( + WebhookSignature::verify( + self::FIXTURE_PAYLOAD, + self::FIXTURE_EXPECTED_SIGNATURE, + 'ffffffffffffffffffffffffffffffff' + ) + ); + } + + // --- 3. Payload tampered (one byte changed) ---------------------------- + public function testReturnsFalseWhenPayloadIsTampered(): void + { + $tampered = str_replace('delivery', 'Delivery', self::FIXTURE_PAYLOAD); + + $this->assertFalse( + WebhookSignature::verify( + $tampered, + self::FIXTURE_EXPECTED_SIGNATURE, + self::FIXTURE_SIGNING_SECRET + ) + ); + } + + // --- 4. Signature with wrong length ------------------------------------ + public function testReturnsFalseWithoutRaisingWhenSignatureTooShort(): void + { + $tooShort = substr(self::FIXTURE_EXPECTED_SIGNATURE, 0, 31); + + $this->assertFalse( + WebhookSignature::verify( + self::FIXTURE_PAYLOAD, + $tooShort, + self::FIXTURE_SIGNING_SECRET + ) + ); + } + + // --- 5. Signature with non-hex characters ------------------------------ + public function testReturnsFalseWithoutRaisingForNonHexSignature(): void + { + $notHex = str_repeat('z', WebhookSignature::SIGNATURE_HEX_LENGTH); + + $this->assertFalse( + WebhookSignature::verify( + self::FIXTURE_PAYLOAD, + $notHex, + self::FIXTURE_SIGNING_SECRET + ) + ); + } + + // --- 6. Empty signature string ----------------------------------------- + public function testReturnsFalseForEmptySignature(): void + { + $this->assertFalse( + WebhookSignature::verify( + self::FIXTURE_PAYLOAD, + '', + self::FIXTURE_SIGNING_SECRET + ) + ); + } + + // --- 7. Empty signing_secret ------------------------------------------- + public function testReturnsFalseForEmptySigningSecret(): void + { + $this->assertFalse( + WebhookSignature::verify( + self::FIXTURE_PAYLOAD, + self::FIXTURE_EXPECTED_SIGNATURE, + '' + ) + ); + } + + // --- 8. Empty payload + non-empty signature ---------------------------- + public function testReturnsFalseForEmptyPayload(): void + { + $this->assertFalse( + WebhookSignature::verify( + '', + self::FIXTURE_EXPECTED_SIGNATURE, + self::FIXTURE_SIGNING_SECRET + ) + ); + } + + // --- 9. Known-good cross-SDK fixture ----------------------------------- + public function testMatchesHardcodedHmacSha256DigestForSharedFixture(): void + { + // Recompute the digest in-place so a regression in PHP's hash + // extension or the fixture itself fails loudly: this is the + // byte-for-byte contract every other Mailtrap SDK must satisfy. + $computed = hash_hmac('sha256', self::FIXTURE_PAYLOAD, self::FIXTURE_SIGNING_SECRET); + + $this->assertSame(self::FIXTURE_EXPECTED_SIGNATURE, $computed); + $this->assertTrue( + WebhookSignature::verify( + self::FIXTURE_PAYLOAD, + self::FIXTURE_EXPECTED_SIGNATURE, + self::FIXTURE_SIGNING_SECRET + ) + ); + } +}