Verification runs with browser Web Crypto. Field values are not submitted or persisted.
Verify the delivery contract before enabling delivery.
Inspect the exact signed input and verify a deterministic public test vector in this browser. This readiness kit defines receiver interoperability; it does not provide webhook subscriptions or outbound delivery.
The public vector checks signature compatibility. Its timestamp is evaluated at the recorded vector time, never by disabling production freshness.
No endpoint registration, outbound send, retry or delivery receipt is available here.
Nothing hidden between raw body and decision.
Change any field to test failure behavior, then restore the canonical vector. All computation remains on this device.
Eight fixtures. Exact receiver decisions.
Run the published positive and negative cases in this browser. A passing run demonstrates fixture compatibility only; no endpoint or delivery is tested.
- Cases
- 8 deterministic
- Contract
- v1.0.0
- Execution
- Browser · local only
8 local cases are ready to run.
| Case | Purpose | Expected | Actual | State |
|---|---|---|---|---|
01canonical-validAccept the canonical vector | Confirms exact HMAC-SHA256 construction over the recorded timestamp and raw body. | valid | not_run | Not run |
02empty-secretReject an empty secret | Confirms fail-closed secret validation before digest construction. | invalid_secret | not_run | Not run |
03zero-timestampReject a zero timestamp | Confirms that the receiver accepts only positive integer Unix timestamps. | invalid_timestamp | not_run | Not run |
04stale-timestampReject a stale timestamp | Confirms enforcement immediately outside the candidate 300-second tolerance. | timestamp_outside_tolerance | not_run | Not run |
05unsupported-versionReject an unsupported signature version | Confirms strict parsing of the lowercase v1 signature prefix. | invalid_signature_header | not_run | Not run |
06uppercase-digestReject an uppercase digest | Confirms the canonical lowercase hexadecimal encoding contract. | invalid_signature_header | not_run | Not run |
07body-byte-mutationReject a raw-body byte mutation | Confirms that whitespace and exact raw-body bytes remain signature material. | signature_mismatch | not_run | Not run |
08digest-mutationReject a digest mutation | Confirms rejection of a well-formed but nonmatching signature value. | signature_mismatch | not_run | Not run |
This suite tests deterministic compatibility with the candidate receiver contract. It does not test endpoint identity, production secret custody, network delivery, retry behavior, replay storage, availability or implementation security outside these cases.
Verification has five explicit stages.
The browser workbench covers construction, calculation and comparison. Production receivers must also enforce freshness and replay controls.
- 01 · ReadPreserve the raw body
Read the exact request bytes before any JSON parsing or reformatting.
- 02 · ConstructBuild the signed message
Join the Unix timestamp, a period and the unchanged raw body.
- 03 · ComputeCalculate HMAC-SHA256
Use the endpoint secret and encode the digest as lowercase hexadecimal.
- 04 · CompareUse constant-time comparison
Compare the receiver digest with the value after the v1 prefix.
- 05 · ConstrainCheck freshness and replay
Apply the timestamp window and reject event IDs already processed.
Implement the same contract in your runtime.
These examples verify the signature shape and digest. Add the production controls listed below before accepting a delivered event.
node:cryptoimport { createHmac, timingSafeEqual } from 'node:crypto';
const timestamp = request.headers.get('PolicyWatcher-Timestamp');
const signature = request.headers.get('PolicyWatcher-Signature');
const eventId = request.headers.get('PolicyWatcher-Event-Id');
const rawBody = Buffer.from(await request.arrayBuffer());
if (!/^\d+$/.test(timestamp ?? '') || !/^v1=[a-f0-9]{64}$/.test(signature ?? '')) {
throw new Error('Invalid webhook headers');
}
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (age > 300) throw new Error('Stale webhook timestamp');
const expected = createHmac('sha256', process.env.POLICYWATCHER_WEBHOOK_SECRET)
.update(`${timestamp}.`, 'utf8')
.update(rawBody)
.digest('hex');
const supplied = signature.slice(3);
if (!timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(supplied, 'hex'))) {
throw new Error('Invalid webhook signature');
}
// Check eventId in a bounded replay store before processing.hmac · hashlibimport hashlib
import hmac
import os
import re
import time
timestamp = request.headers["PolicyWatcher-Timestamp"]
signature = request.headers["PolicyWatcher-Signature"]
event_id = request.headers["PolicyWatcher-Event-Id"]
raw_body = request.get_data(cache=False, as_text=False)
if not timestamp.isdigit() or re.fullmatch(r"v1=[a-f0-9]{64}", signature) is None:
raise ValueError("Invalid webhook headers")
if abs(int(time.time()) - int(timestamp)) > 300:
raise ValueError("Stale webhook timestamp")
message = timestamp.encode() + b"." + raw_body
expected = hmac.new(
os.environ["POLICYWATCHER_WEBHOOK_SECRET"].encode(),
message,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature[3:]):
raise ValueError("Invalid webhook signature")
# Check event_id in a bounded replay store before processing.Controls still owned by the integrator.
- Read the exact raw request bytes before JSON parsing or body transformation.
- Resolve a tenant-owned secret from a managed secret store; never use the public test secret.
- Reject timestamps outside the configured tolerance; the candidate default is 300 seconds.
- Compare signatures with a constant-time primitive.
- Store accepted event IDs or nonces for a bounded replay-protection window.
- Support overlapping active secrets during controlled key rotation.
- Record bounded delivery outcomes without logging secrets or raw private payloads.
Use forward polling for published change events.
The public feed exposes already-published events with an opaque cursor. It does not imply delivery or receipt.