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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
25 changes: 24 additions & 1 deletion packages/core/src/trust-tasks/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,12 +62,16 @@ export async function signTrustTask({
envelope,
signing,
proofPurpose = "assertionMethod",
clockSkewMs = DEFAULT_CLOCK_SKEW_MS,
}: SignTrustTaskOptions): Promise<TrustTaskEnvelope> {
const proofConfig: Record<string, unknown> = {
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,
};

Expand Down
22 changes: 22 additions & 0 deletions packages/core/tests/provision.request.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
2 changes: 1 addition & 1 deletion packages/extension/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion packages/pwa/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down