first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import { timingSafeEqual } from 'crypto';
|
||||
|
||||
import { verifySignature } from '../webhook-signature-verification';
|
||||
|
||||
jest.mock('crypto', () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
...jest.requireActual('crypto'),
|
||||
timingSafeEqual: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('webhook-signature-verification', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('verifySignature', () => {
|
||||
it('should return true when signatures match', () => {
|
||||
const expectedSignature = 'sha256=abc123';
|
||||
const actualSignature = 'sha256=abc123';
|
||||
|
||||
(timingSafeEqual as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => expectedSignature,
|
||||
getActualSignature: () => actualSignature,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(timingSafeEqual).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when signatures do not match', () => {
|
||||
const expectedSignature = 'sha256=abc123';
|
||||
const actualSignature = 'sha256=xyz789';
|
||||
|
||||
(timingSafeEqual as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => expectedSignature,
|
||||
getActualSignature: () => actualSignature,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(timingSafeEqual).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should verify signature even when timestamp is skipped', () => {
|
||||
const expectedSignature = 'sha256=abc123';
|
||||
const actualSignature = 'sha256=xyz789';
|
||||
|
||||
(timingSafeEqual as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => expectedSignature,
|
||||
getActualSignature: () => actualSignature,
|
||||
getTimestamp: () => null,
|
||||
skipIfNoTimestamp: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(timingSafeEqual).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when signatures have different lengths', () => {
|
||||
const expectedSignature = 'sha256=abc123';
|
||||
const actualSignature = 'sha256=abc1234';
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => expectedSignature,
|
||||
getActualSignature: () => actualSignature,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(timingSafeEqual).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when expected signature is missing', () => {
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => null,
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when expected signature is undefined', () => {
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => undefined as unknown as string,
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when actual signature is missing', () => {
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => null,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when actual signature is undefined', () => {
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => undefined as unknown as string,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when skipIfNoExpectedSignature is true and no expected signature', () => {
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => null,
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
skipIfNoExpectedSignature: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate timestamp when provided and within window', () => {
|
||||
const currentTimeSec = Math.floor(Date.now() / 1000);
|
||||
const recentTimestamp = currentTimeSec - 60; // 1 minute ago
|
||||
|
||||
(timingSafeEqual as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => recentTimestamp,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is too old', () => {
|
||||
const currentTimeSec = Math.floor(Date.now() / 1000);
|
||||
const oldTimestamp = currentTimeSec - 400; // More than 5 minutes ago
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => oldTimestamp,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is too far in future', () => {
|
||||
const currentTimeSec = Math.floor(Date.now() / 1000);
|
||||
const futureTimestamp = currentTimeSec + 400; // More than 5 minutes in future
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => futureTimestamp,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle timestamp as string', () => {
|
||||
const currentTimeSec = Math.floor(Date.now() / 1000);
|
||||
const recentTimestamp = String(currentTimeSec - 60);
|
||||
|
||||
(timingSafeEqual as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => recentTimestamp,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should convert milliseconds timestamp to seconds', () => {
|
||||
const currentTimeMs = Date.now();
|
||||
const recentTimestampMs = currentTimeMs - 60 * 1000; // 1 minute ago in ms
|
||||
|
||||
(timingSafeEqual as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => recentTimestampMs,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should use custom maxTimestampAgeSeconds', () => {
|
||||
const currentTimeSec = Math.floor(Date.now() / 1000);
|
||||
const timestamp = currentTimeSec - 120; // 2 minutes ago
|
||||
|
||||
(timingSafeEqual as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => timestamp,
|
||||
maxTimestampAgeSeconds: 60, // 1 minute window
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when skipIfNoTimestamp is true and timestamp is null', () => {
|
||||
(timingSafeEqual as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => null,
|
||||
skipIfNoTimestamp: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is null and skipIfNoTimestamp is false', () => {
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => null,
|
||||
skipIfNoTimestamp: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is invalid (NaN)', () => {
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
getTimestamp: () => 'invalid-timestamp',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully and return false', () => {
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => {
|
||||
throw new Error('Test error');
|
||||
},
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle timingSafeEqual errors gracefully', () => {
|
||||
(timingSafeEqual as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Buffer length mismatch');
|
||||
});
|
||||
|
||||
const result = verifySignature({
|
||||
getExpectedSignature: () => 'sha256=abc123',
|
||||
getActualSignature: () => 'sha256=abc123',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user