Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/actions/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('Actions', () => {
});

const makeSigHeader = (payload: unknown, secret: string) => {
const timestamp = Date.now() * 1000;
const timestamp = Date.now();
const unhashedString = `${timestamp}.${JSON.stringify(payload)}`;
const signatureHash = crypto
.createHmac('sha256', secret)
Expand Down
69 changes: 68 additions & 1 deletion src/common/crypto/signature-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('SignatureProvider', () => {
beforeEach(() => {
payload = mockWebhook;
secret = 'secret';
timestamp = Date.now() * 1000;
timestamp = Date.now();
const unhashedString = `${timestamp}.${JSON.stringify(payload)}`;
signatureHash = crypto
.createHmac('sha256', secret)
Expand All @@ -23,6 +23,73 @@ describe('SignatureProvider', () => {
});

describe('verifyHeader', () => {
it.each([
'not-a-timestamp',
'',
'123junk',
'Infinity',
'1e12',
'1.5',
'9007199254740992',
])('rejects malformed timestamp %s', async (value) => {
const hash = await signatureProvider.computeSignature(
value,
payload,
secret,
);
await expect(
signatureProvider.verifyHeader({
payload,
secret,
sigHeader: `t=${value},v1=${hash}`,
}),
).rejects.toThrow('Invalid timestamp');
});

it.each([-180000, 0, 180000])(
'accepts timestamp offset %s within tolerance',
async (offset) => {
const now = 1791600000000;
jest.spyOn(Date, 'now').mockReturnValue(now);
const value = String(now + offset);
const hash = await signatureProvider.computeSignature(
value,
payload,
secret,
);
await expect(
signatureProvider.verifyHeader({
payload,
secret,
sigHeader: `t=${value},v1=${hash}`,
tolerance: 180000,
}),
).resolves.toBe(true);
},
);

it.each([-180001, 180001])(
'rejects timestamp offset %s outside tolerance',
async (offset) => {
const now = 1791600000000;
jest.spyOn(Date, 'now').mockReturnValue(now);
const value = String(now + offset);
const hash = await signatureProvider.computeSignature(
value,
payload,
secret,
);
await expect(
signatureProvider.verifyHeader({
payload,
secret,
sigHeader: `t=${value},v1=${hash}`,
tolerance: 180000,
}),
).rejects.toThrow('Timestamp outside the tolerance zone');
},
);

it('returns true when the signature is valid', async () => {
const sigHeader = `t=${timestamp}, v1=${signatureHash}`;
const options = { payload, sigHeader, secret };
Expand Down
7 changes: 6 additions & 1 deletion src/common/crypto/signature-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ export class SignatureProvider {
);
}

if (parseInt(timestamp, 10) < Date.now() - tolerance) {
const timestampMs = Number(timestamp);
if (!/^\d+$/.test(timestamp) || !Number.isSafeInteger(timestampMs)) {
throw new SignatureVerificationException('Invalid timestamp');
}

if (Math.abs(Date.now() - timestampMs) > tolerance) {
throw new SignatureVerificationException(
'Timestamp outside the tolerance zone',
);
Expand Down
2 changes: 1 addition & 1 deletion src/webhooks/webhooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('Webhook signatures', () => {
beforeEach(() => {
payload = mockWebhook;
secret = 'secret';
timestamp = Date.now() * 1000;
timestamp = Date.now();
unhashedString = `${timestamp}.${JSON.stringify(payload)}`;
signatureHash = crypto
.createHmac('sha256', secret)
Expand Down
Loading