diff --git a/README.md b/README.md index 4e9c182..838b7a9 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,33 @@ The mediator delivers the inner JWE to the VTA. Replies travel back via the wallet's mediator inbox, decrypted by `Pickup3Dispatcher`, and demuxed by `thid` to the waiting Promise. +### Mediator CORS (browser requirement) + +The wallet runs **in the browser**, so the mediator's WebSocket +(`wss://…/ws`) and REST (`/inbound`, `/authenticate`, …) endpoints must +allow the **origin the wallet page is served from** — either by echoing +that exact origin in `Access-Control-Allow-Origin`, or with `*`. This is +a mediator-side configuration; the wallet cannot work around it. + +Symptom of a missing/incorrect CORS allow-list: the REST auth handshake +succeeds (or appears to), but opening the live-delivery socket fails with + +``` +mediator-transport: WebSocket failed to open (close code 1006) +``` + +A browser rejects a cross-origin WebSocket upgrade *before* the socket +opens, which surfaces as an abnormal **1006** close with no HTTP status — +indistinguishable, from the client side, from a refused upgrade or a +proxy that strips the `Upgrade` header. If REST auth works from the same +page but the WS gives 1006, **check the mediator's CORS allow-list for +your wallet origin first.** + +For a self-hosted `affinidi-messaging-mediator`, set the allowed origins +in its config (e.g. `cors_allow_origin`) to include your wallet's origin +(`http://localhost:5173` in dev, your extension/PWA origin in prod), or +`*` for a permissive dev setup. Restart the mediator after changing it. + ## End-to-end validation Six smokes cover the main DIDComm + wallet links. Run from a browser diff --git a/package-lock.json b/package-lock.json index 9bf7248..0d0c304 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pnm-browser-plugin", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pnm-browser-plugin", - "version": "0.1.0", + "version": "0.1.1", "workspaces": [ "packages/core", "packages/pwa", @@ -6980,7 +6980,7 @@ }, "packages/core": { "name": "@openvtc/pnm-core", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { "@hpke/chacha20poly1305": "^1.8.0", @@ -7003,7 +7003,7 @@ }, "packages/extension": { "name": "@openvtc/pnm-extension", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@openvtc/pnm-core": "^0.1.0", "react": "^19.0.0", @@ -7023,7 +7023,7 @@ }, "packages/pwa": { "name": "@openvtc/pnm-pwa", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@openvtc/pnm-core": "^0.1.0", "@tanstack/react-query": "^5.62.0", diff --git a/package.json b/package.json index e58a554..fc0dc53 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pnm-browser-plugin", "private": true, - "version": "0.1.0", + "version": "0.1.1", "description": "Browser-side bridge between WebAuthn passkeys and VTA-managed DIDs. Ships as a PWA and as an MV3 browser extension on a shared TypeScript core.", "type": "module", "workspaces": [ diff --git a/packages/core/package.json b/packages/core/package.json index 64eab53..3072d14 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@openvtc/pnm-core", - "version": "0.1.0", + "version": "0.1.1", "description": "Browser-side bridge between WebAuthn passkeys and VTA-managed DIDs. Wire types, WebAuthn ceremony helpers, COSE→Multikey conversion, DID verificationMethod builder, REST + DIDComm transports, mediator client, SIOP / RP-login / provision-integration flows.", "license": "Apache-2.0", "repository": { diff --git a/packages/core/src/trust-tasks/sign.ts b/packages/core/src/trust-tasks/sign.ts index 5f89b1f..af1d5cf 100644 --- a/packages/core/src/trust-tasks/sign.ts +++ b/packages/core/src/trust-tasks/sign.ts @@ -31,8 +31,27 @@ export interface SignTrustTaskOptions { * attesting to a separate claim — e.g. a VP-framed bootstrap request * (the provision-integration flow) or a SIOP-shaped self-attestation. */ proofPurpose?: "assertionMethod" | "authentication"; + /** Milliseconds to back-date the proof's `created` timestamp, absorbing + * clock skew between this wallet and the verifier. + * + * VC Data-Integrity verifiers (the Rust `eddsa-jcs-2022` spec-conformance + * check on the VTA included) reject any proof whose `created` is in the + * verifier's future, with **no** skew tolerance. If the wallet's clock + * runs even slightly ahead of the verifier, an honest `created = now` + * fails with "Created date is in the future". Back-dating by a small + * margin keeps `created <= verifier_now` across normal NTP skew. + * + * Default 60_000 (60s). The timestamp is still UTC (`toISOString()`); + * this only shifts it earlier. Gross skew (clock minutes/hours off) is + * an environment problem a margin can't fix — keep the host on NTP. */ + clockSkewMs?: number; } +/** Default back-date applied to a DI proof's `created`. Comfortably inside + * the ±5min skew window the VTA already allows on `validUntil`, so it can't + * push `created` outside any window the verifier accepts. */ +const DEFAULT_CLOCK_SKEW_MS = 60_000; + /** * Attach an `eddsa-jcs-2022` Data Integrity proof to a Trust-Task envelope * and return the same envelope. The signed input is the concatenation of @@ -43,12 +62,16 @@ export async function signTrustTask({ envelope, signing, proofPurpose = "assertionMethod", + clockSkewMs = DEFAULT_CLOCK_SKEW_MS, }: SignTrustTaskOptions): Promise { const proofConfig: Record = { type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022", verificationMethod: signing.kid, - created: new Date().toISOString(), + // UTC, back-dated by `clockSkewMs` so a wallet clock running slightly + // ahead of the verifier doesn't trip the "Created date is in the + // future" spec-conformance rejection. See `clockSkewMs` docs above. + created: new Date(Date.now() - clockSkewMs).toISOString(), proofPurpose, }; diff --git a/packages/core/tests/provision.request.mjs b/packages/core/tests/provision.request.mjs index 4534c1c..dd12d0d 100644 --- a/packages/core/tests/provision.request.mjs +++ b/packages/core/tests/provision.request.mjs @@ -176,3 +176,25 @@ test("buildBootstrapRequest: validity window matches default", async () => { assert.ok(validUntil >= before + 14 * 60_000); assert.ok(validUntil <= after + 16 * 60_000); }); + +test("buildBootstrapRequest: proof.created is UTC and back-dated (never in the verifier's future)", async () => { + // Guards the clock-skew fix: a wallet clock running slightly ahead of the + // VTA must not produce a `created` the VC-DI spec-conformance check rejects + // as "Created date is in the future". We back-date by ~60s. + const before = Date.now(); + const { vp } = await buildBootstrapRequest({ + ephemeral: generateSigningIdentity(), + ask: { type: "AdminRotation", adminTemplate: { name: "vta-admin" } }, + }); + const created = vp.proof.created; + + // UTC, ISO-8601, `Z`-suffixed (toISOString contract). + assert.match(created, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/); + + const createdMs = Date.parse(created); + // Strictly in the past relative to when we called: a same-instant clock + // (let alone one behind ours) sees `created` <= its now. + assert.ok(createdMs < before, `created (${created}) must be back-dated below call time`); + // …but only modestly (the margin), not wildly historical. + assert.ok(before - createdMs <= 5 * 60_000, "back-date stays within a sane skew window"); +}); diff --git a/packages/extension/package.json b/packages/extension/package.json index c05ba41..be1d6da 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,7 +1,7 @@ { "name": "@openvtc/pnm-extension", "private": true, - "version": "0.1.0", + "version": "0.1.1", "description": "Manifest v3 browser extension shell sharing @openvtc/pnm-core.", "type": "module", "scripts": { diff --git a/packages/pwa/package.json b/packages/pwa/package.json index 2fa7275..96dbd4d 100644 --- a/packages/pwa/package.json +++ b/packages/pwa/package.json @@ -1,7 +1,7 @@ { "name": "@openvtc/pnm-pwa", "private": true, - "version": "0.1.0", + "version": "0.1.1", "description": "PWA shell for pnm-browser-plugin — operator-facing wallet UI.", "type": "module", "scripts": {