Add the Bird Control Plane gateway, signed webhook ingestion, policy-gated writes, fail-closed production auto-activation, and RSA-OAEP bootstrap credential flow.
107 lines
3.5 KiB
PHP
107 lines
3.5 KiB
PHP
<?php
|
|
|
|
app_require('modules/bird/classes/bird_control_plane_security.php');
|
|
|
|
use bird\classes\bird_control_plane_security;
|
|
|
|
it('verifies the documented Bird webhook signature without changing the signed URL or body', function (): void {
|
|
$key = 'test-signing-key';
|
|
$timestamp = '1785312000';
|
|
$url = 'https://api.example.test/bird/webhooks/notifications?source=bird';
|
|
$body = '{"event":"conversation.updated","data":{"id":"c-1"}}';
|
|
$checksum = hash('sha256', $body, true);
|
|
$signature = base64_encode(hash_hmac(
|
|
'sha256',
|
|
$timestamp . "\n" . $url . "\n" . $checksum,
|
|
$key,
|
|
true
|
|
));
|
|
|
|
expect(bird_control_plane_security::verifyBirdWebhookSignature(
|
|
$key,
|
|
$timestamp,
|
|
$url,
|
|
$body,
|
|
$signature
|
|
))->toBeTrue()
|
|
->and(bird_control_plane_security::verifyBirdWebhookSignature(
|
|
$key,
|
|
$timestamp,
|
|
$url,
|
|
$body . "\n",
|
|
$signature
|
|
))->toBeFalse()
|
|
->and(bird_control_plane_security::verifyBirdWebhookSignature(
|
|
$key,
|
|
$timestamp,
|
|
'https://internal.example.test/bird/webhooks/notifications',
|
|
$body,
|
|
$signature
|
|
))->toBeFalse();
|
|
});
|
|
|
|
it('fails closed for malformed signatures and stale webhook timestamps', function (): void {
|
|
expect(bird_control_plane_security::verifyBirdWebhookSignature(
|
|
'key',
|
|
'1785312000',
|
|
'https://api.example.test/bird/webhooks/notifications',
|
|
'{}',
|
|
'not-base64!'
|
|
))->toBeFalse()
|
|
->and(bird_control_plane_security::timestampWithinReplayWindow(
|
|
'1785312000',
|
|
300,
|
|
1785312200
|
|
))->toBeTrue()
|
|
->and(bird_control_plane_security::timestampWithinReplayWindow(
|
|
'1785312000000',
|
|
300,
|
|
1785312200
|
|
))->toBeTrue()
|
|
->and(bird_control_plane_security::timestampWithinReplayWindow(
|
|
'1785312000',
|
|
300,
|
|
1785312401
|
|
))->toBeFalse()
|
|
->and(bird_control_plane_security::timestampWithinReplayWindow(
|
|
'tomorrow',
|
|
300,
|
|
1785312000
|
|
))->toBeFalse();
|
|
});
|
|
|
|
it('accepts only an exact configured bearer token', function (): void {
|
|
expect(bird_control_plane_security::bearerToken(
|
|
['HTTP_AUTHORIZATION' => 'Bearer expected-token']
|
|
))->toBe('expected-token')
|
|
->and(bird_control_plane_security::verifyBearer('expected-token', 'expected-token'))->toBeTrue()
|
|
->and(bird_control_plane_security::verifyBearer('expected-token', 'Expected-token'))->toBeFalse()
|
|
->and(bird_control_plane_security::verifyBearer('', 'anything'))->toBeFalse();
|
|
});
|
|
|
|
it('verifies timestamp-bound Bird Flow request signatures', function (): void {
|
|
$secret = 'flow-secret';
|
|
$timestamp = '1785312000';
|
|
$body = '{"event":{"platform":"sms"}}';
|
|
$signature = 'sha256=' . hash_hmac('sha256', $timestamp . "\n" . $body, $secret);
|
|
|
|
expect(bird_control_plane_security::verifyFlowSignature(
|
|
$secret,
|
|
$timestamp,
|
|
$body,
|
|
$signature
|
|
))->toBeTrue()
|
|
->and(bird_control_plane_security::verifyFlowSignature(
|
|
$secret,
|
|
$timestamp,
|
|
$body . ' ',
|
|
$signature
|
|
))->toBeFalse()
|
|
->and(bird_control_plane_security::verifyFlowSignature(
|
|
'',
|
|
$timestamp,
|
|
$body,
|
|
$signature
|
|
))->toBeFalse();
|
|
});
|