Skip to content

Latest commit

 

History

History
446 lines (368 loc) · 22.8 KB

File metadata and controls

446 lines (368 loc) · 22.8 KB

Changes

Unreleased

0.5.1

Fixed

  • The WebAuthn user handle is 64 random bytes instead of the UTF-8 of whatever the caller passes as userId. An authenticator keeps one discoverable credential per (rp.id, user.id) and replaces the previous one when both match — silently, with no prompt and nothing to undo. Deriving the handle from a typed name therefore meant two people registering as "anna" on a shared device destroyed each other's passkey, and with it the DID and every entry signed under it. The same happened to one person re-entering their usual name after clearing storage. WebAuthn L2 §5.4.3 also forbids putting personally identifying information in the handle, which a name or e-mail plainly is, and the handle is stored in the authenticator indefinitely.

    userId keeps its rightful place as user.name, the label the credential picker shows. It is a label only: it identifies nothing, two credentials may carry the same one, and nothing in this package looks a credential up by it. Closes #45.

    Nothing needs migrating. Recovery goes through discoverable credentials (readLargeBlobMetadata calls get() with no allowCredentials), or through an explicit credential ID where those are switched off — no path resolves a credential by handle, so credentials registered under the old scheme keep working untouched. The derived did:key comes from the credential's public key and does not change.

    Consumers should expect a behavioural difference: re-registering under a name that was used before now adds a passkey instead of replacing one. That is the point — a replaced passkey is data loss, a second entry is a choice — but it means the picker can show several, so user.name and displayName should be distinguishing enough to pick from. The new random handle is returned as credential.userHandle (base64url); the authenticator keeps its own copy, so storing it is optional.

  • Actually write the secret key into largeBlob. The keystore record carried secretKey: sk, // Will be moved to largeBlob — and nothing ever moved it. That was the only occurrence of the field in src/, and storeEncryptedKeystore() serialises a fixed whitelist that does not include it, so the key was dropped at persist time. Choosing largeBlob therefore produced a keystore that worked for the session that created it and could never be unlocked again: on the next load retrieveSKFromLargeBlob() read a blob nobody had written and failed with "No largeBlob data found in credential".

    The key never reached disk in the clear — the whitelist saw to that — but the feature was inert. writeSKToLargeBlob() now performs the assertion that stores it (a blob can only be written during an assertion, never at registration, so this costs one extra prompt) and throws unless the authenticator reports written: true. Failing there beats persisting a record that cannot be opened.

  • Carry the authenticator's own answer on the credential. createCredential() now records extensionSupport — what the authenticator agreed to during the ceremony — because the raw PublicKeyCredential does not survive past that function, so the answer is read there or lost. Client support is not authenticator support: measured on a macOS platform authenticator in Brave, the client advertises hmacCreateSecret while the authenticator refuses it and PRF works. Reading only the client is what left the demo offering hmac-secret and then failing at the ceremony, which is the failure originally reported in #9.

    The encrypted-keystore demo consumes it: methods the authenticator refused read "Browser yes, this passkey no" rather than "Supported", become unselectable, and a selection it cannot honour falls back automatically. It also no longer logs encrypted: Yes (largeBlob) off the back of the requested options — that line reported intent as though it were outcome, so a failed write looked identical to a successful one. It now names the request and the authenticator's capability separately, so a mismatch is visible.

  • Detect WebAuthn extensions by asking the browser instead of inspecting a prototype. checkExtensionSupport() tested 'largeBlob' in PublicKeyCredential.prototype — but extensions are never properties of that interface, they arrive through getClientExtensionResults(). The test therefore returned false in every browser ever shipped, including those with complete support, and hmacSecret was hard-coded false beside it. Both together meant the encrypted-keystore demo disabled its own headline feature everywhere and could not be switched on at all.

    Measured on Chrome 148: the prototype probe answered false for largeBlob, prf and hmacCreateSecret while getClientCapabilities() reported all three true. Every extension name behaves the same way, so no browser could ever have passed. Closes #9.

    checkExtensionSupport() now reads getClientCapabilities() and reports prf alongside the other two. It also returns known, distinguishing "the browser says no" from "the browser cannot say" — callers must not treat the second as a refusal, or they disable a feature that may well work.

Added

  • extensionSupportFromCredential(credential) reads what the authenticator actually agreed to, from the registration response. This is the authoritative answer and the one worth persisting: a browser may support largeBlob while the security key in front of it does not, and only the ceremony settles that. addLargeBlobToCredentialOptions() takes an optional support level for the same reason — it stays 'required' by default, because under 'preferred' an unsupported authenticator yields a credential whose secret was silently never written.

Changed

  • The encrypted-keystore demo offers PRF as a keystore encryption method and prefers it, matching the library's own default since 0.4.0. It had hard-coded largeBlob, so it overrode that default and never exercised PRF at all. Methods the browser cannot vouch for are now labelled "Unknown" rather than "Not supported", and stay selectable.

0.5.0

Breaking

  • The varsig signing context moves into the signed payload, which changes the bytes a credential signs. Entries and identities produced by 0.4.x and earlier no longer verify, and peers running different major versions cannot replicate with each other — the whole set has to move together.

    The challenge used to be SHA-256(label ‖ payload). That separated the three signing contexts, but it meant the challenge was no longer the hash of the payload, and the varsig WebAuthn draft has a verifier re-hash the payload and compare it against the challenge (step 5). Nothing outside this package could reproduce that value.

    The label itself is not the problem: one credential signs an identity, a public key and an oplog entry, and a signature over one must not be replayable as a signature over another. That separation cannot live in the varsig header either, because a WebAuthn signature only covers authenticatorData ‖ SHA-256(clientDataJSON) — anything carried beside the challenge is unauthenticated. So the context moves into the signed bytes: bindContext() frames it length-prefixed ahead of the payload, and the challenge becomes a plain SHA-256 multihash of those bytes. Both properties now hold — the three contexts still yield different challenges, and a spec verifier can reproduce the digest.

    The length prefix is load-bearing. Without it, context a with payload bc and context ab with payload c are the same bytes, so a chosen payload could absorb the label and sidestep the separation.

    This is a bet on a draft: ChainAgnostic/varsig#11 is still open with webauthn-varsig-header = TODO. If the draft settles differently, the wire format changes again.

  • iso-webauthn-varsig moves to 0.3.0, which corrects the Ed25519 curve code in the varsig header from 0xed01 to 0xed. 0xed01 is how the multicodec ed25519-pub is written in prose — but that is its varint encoding, and using it as the code meant emitting 60673, which no other implementation reads as Ed25519. Ed25519 varsigs written by earlier versions do not decode. P-256 bytes are unchanged by this second break, so P-256 deployments are affected only by the challenge change above.

Migration

Nothing carries over automatically. Re-register credentials and re-create any database whose entries were signed under 0.4.x, and upgrade every peer — including relays that verify identities — in one step rather than gradually.

0.4.2

Fixed

  • Restore debug logging in the browser. weald, which @libp2p/logger uses, does import { ms as humanize } from 'ms' — a named export that only exists in ms@3+. All three demos pinned ms to 2.1.3, which is CommonJS with no named export, so the import resolved to undefined and the first debug call threw TypeError: (0, import_ms2.ms) is not a function. Enabling DEBUG in a browser therefore crashed credential creation outright. Dropping the pin lets weald resolve the version it declares.

    This is why webauthn-logging-e2e had been failing continuously: it is the only suite that sets localStorage.debug, so it was the only one to reach the broken path.

  • Clear the production dependency advisories. iso-web > iso-kv > conf > ajv carried four high fast-uri advisories and one moderate ajv one; the scoped overrides conf>ajv and ajv>fast-uri resolve it. Scoped deliberately: a blanket ajv override also hits ESLint, which needs ajv 6 and dies on the removed missingRefs option. pnpm audit --prod now reports nothing, so the enforced CI gate moves from critical to moderate.

  • Stop writing WebAuthn credentials to the console. logWebAuthnResponse() dumped the whole credential — rawId, attestationObject, clientDataJSON, signature, getPublicKey() and getClientExtensionResults() — on every navigator.credentials.create() and .get(), unconditionally and in production code paths. Two things made that more than noise: extension results can carry the PRF output, which is what both the keystore encryption and (since 0.4.1) the OrbitDB signing key derive from, and rawId is a stable per-user, per-RP identifier that landed in any console capture or session replay the embedding app happened to run. Replaced by a single shared helper that logs shape only — byte lengths, presence flags and the names of the extensions that returned results — behind the package debug logger, so it is silent unless DEBUG=orbitdb-identity-provider-webauthn-did* is set. Closes #22.

  • Keep the demos' P2P component out of the server render. Turning on ssr so prerendering emitted real content also meant +page.svelte was evaluated in Node, and it imported the libp2p stack at module scope — reaching @libp2p/webrtc and its native node-datachannel binding, which the demos do not build. The dev server returned 500 for every request. The component is loaded in the browser only now; the shared shell still renders server-side, so prerendering keeps producing a real title and description.

Changed

  • Route the remaining informational console.log output through the debug logger. A library should not print unconditionally; console.error and console.warn stay as they are, so genuine failures remain visible. src/ went from 61 console.* calls to 16, none of them console.log.
  • Deduplicate three byte-identical copies of the WebAuthn debug helper into src/webauthn/debug-log.js.

0.4.1

Added

  • Derive the OrbitDB signing key from the passkey PRF output instead of letting the keystore generate a random one. Identities.createIdentity takes keystore.getKey(id) || keystore.createKey(id), and createKey is random, so the same passkey previously produced a different publicKey and signatures.id — and therefore a different identity document — on every device. Seeding the keystore during getId(), the last point before OrbitDB asks for the key, makes the whole document reproducible: one block to keep retrievable for a DID instead of one per device, and a device that loses its keystore while keeping the passkey reconstructs the same identity rather than minting another. Needs no change to @orbitdb/core.
  • createCredential() now requests the PRF extension even when the keystore is not encrypted, and stores the input. Without a stored input the output would differ per call, so a credential registered without one can never get a reproducible identity. Requesting it is harmless where unsupported.

Notes

  • Falls back cleanly. No PRF support, no stored input, or a refused assertion leaves the keystore to generate its own key — 0.4.0 behaviour, stable per device but not across devices. Opt out with deriveSigningKeyFromPrf: false.
  • An existing keystore key is never replaced. Swapping it under a device that already has history would mint a second document for the DID, which is what this avoids. Installs upgrading from 0.4.0 keep their key; fresh installs get a reproducible identity.
  • Costs one extra assertion the first time an identity is created on a device. The key is persisted, so later loads do not repeat it.

0.4.0

Breaking

  • WebAuthn credentials now yield the authenticator's actual public key, so the derived did:key changes for anyone who registered against 0.3.x or earlier. Existing OrbitDB identities keyed on the old DID will not match, and databases gated on it become unwritable under the new DID. extractPublicKey() never took its intended path: cbor-web returns byte strings as views into the enclosing buffer, so reading credentialIdLength through authData.buffer without honouring byteOffset read bytes from inside rpIdHash and yielded 43690 for every credential. The COSE slice was then empty, cbor threw Insufficient data, and the catch silently returned a synthetic key derived from SHA-256(credentialId).

Fixed

  • Keep the identity document stable across reloads. signIdentity() reuses the proof it already produced instead of running a fresh WebAuthn assertion, which changed signatures.publicKey — and therefore the content address of the identity document — on every page load. Peers then dropped entries: verifiedIdentitiesCache in @orbitdb/core is keyed on the deterministic signatures.id, so two documents from one keystore collide on a single cache entry and isEqual() rejects whichever was not verified first. The symptom was a database replicating some entries and silently never receiving the rest.
  • Remove the timestamp field from the proof envelope and the 24-hour expiry check that read it. Both were wrong for a value embedded in content-addressed, permanent history: the timestamp changed the document hash on every call, and the expiry would have invalidated the identity behind every entry ever signed under it. Compatible in both directions — proofs that still carry a timestamp verify fine, and 0.3.1 verifying a proof without one computes NaN, which fails its > maxAge test.
  • Prefer response.getPublicKey() (WebAuthn L2) over parsing the attestation object, and correct the parser: honour byteOffset, validate the AT flag, bounds-check credentialIdLength, and decode only the first CBOR item so trailing extension data (the ED flag, set when PRF is requested) no longer throws. The synthetic fallback is now marked synthetic: true.
  • Fix an ambiguous locator in ed25519-keystore-did: getByLabel matches substrings, and the demo's worker toggle is labelled "Use worker-backed Ed25519 keystore".

Added

  • Two-peer OrbitDB replication tests: real libp2p over loopback TCP, Helia with bitswap, gossipsub, two OrbitDB instances, driven by a software WebAuthn authenticator with a real P-256 keypair, an incrementing signature counter and randomised signatures. Covers identity-document stability across reloads, replication of entries written before and after a reload, and that two devices sharing a passkey keep distinct, independently valid identities.
  • Attestation-parsing unit tests: credential ID lengths 16/20/32/64/128, trailing extension data, missing AT flag, non-P-256 COSE keys and truncated coordinates. 13 of the 14 fail against 0.3.1.

Changed

  • CI now runs all eleven test files. Five never ran: webauthn-unit, webauthn-verification, standalone-toolkit, ed25519-keystore-did and simple-encryption-integration. webauthn-unit imports through Vite's /@fs endpoint, which only the dev server exposes, so its step runs against dev rather than the preview build CI otherwise uses.
  • Publishing moves into CI. A v* tag runs the full suite and then publishes via npm Trusted Publishing (OIDC), with provenance. A manual run defaults to a check mode that verifies tag/version agreement and packaging without publishing.
  • test:ci, which preversion and prepublishOnly run, now points at the Node-context suites via playwright.node.config.js — 32 tests in about ten seconds. It previously ran webauthn-verification alone: five tests that check regexes against hardcoded DID literals and one fully mocked database object, none of which creates a credential, touches the keystore or opens an OrbitDB.
  • Restore the security-audit, package-validation and notify jobs. The enforced audit gate is --prod --audit-level=critical; auditing the full tree at moderate reports 86 advisories from the helia and libp2p dev tree, and even --prod reports 5 through iso-web > iso-kv > conf > ajv, so both wider audits run informational until that chain is bumped.

Also shipping in this release

Entries that had accumulated under Unreleased since 0.3.1:

  • Add SECURITY.md with vulnerability reporting and supported-version policy.
  • Rename changes.md to CHANGELOG.md and include it in the published package.
  • Add public TypeScript declarations for the root, standalone, verification, and keystore package entrypoints.
  • Add docs/API.md and expose @le-space/orbitdb-identity-provider-webauthn-did/keystore as a typed package subpath.
  • Remove the Vite node polyfill plugin from production dependencies, update the varsig support stack to iso-web@^3.1.2, and verify npm audit --omit=dev reports zero production advisories.
  • Re-enable the CI lint step and verify pnpm run lint passes.
  • Add shared public constants and catchable error classes for WebAuthn, keystore, and varsig flows.
  • Add root Prettier scripts, ignore rules, and a CI formatting check.
  • Add CODE_OF_CONDUCT.md and include it in the published package.

0.3.1

  • Bump package metadata to 0.3.1 and create the clean release tag after the post-0.3.0 CI fixes landed.
  • Update example lockfiles so all demos install successfully with pnpm install --frozen-lockfile.
  • Fix Playwright web server startup by passing Vite preview/dev arguments directly, avoiding CI timeouts waiting for the wrong port.
  • Update GitHub Actions to current action majors: actions/checkout@v7, pnpm/action-setup@v6, actions/setup-node@v6, and actions/upload-artifact@v7.
  • Verify the release commit with GitHub Actions: root frozen install, all three example frozen installs, all three example builds, WebAuthn focused tests, logging E2E, integration E2E, varsig E2E, encrypted-keystore tests, and Ed25519 encrypted-keystore E2E.
  • Verify the package tarball with npm pack --dry-run; the package reports @le-space/orbitdb-identity-provider-webauthn-did@0.3.1 with 27 published files.

0.3.0

  • Upgrade the OrbitDB stack to @orbitdb/core@^4.0.0.
  • Upgrade Helia to helia@^7.0.1 and add the current Helia service packages: @helia/libp2p, @helia/http, and @helia/bitswap.
  • Upgrade libp2p to libp2p@^3.3.4 and the current scoped packages, including @libp2p/gossipsub@^16.0.3, @libp2p/identify@^4.1.8, @libp2p/websockets@^10.1.15, @chainsafe/libp2p-noise@^17.0.0, and @chainsafe/libp2p-yamux@^8.0.1.
  • Confirm the gossipsub stream-registry fix from libp2p/js-libp2p#3531 is included via @libp2p/gossipsub@16.0.3.
  • Port all Svelte examples to the OrbitDB 4, Helia 7, and libp2p 3 stack: webauthn-todo-demo, ed25519-encrypted-keystore-demo, and webauthn-varsig-demo.
  • Update example libp2p configuration for the v3 connectionEncrypters option and current withLibp2p, withHTTP, and withBitswap Helia setup.
  • Update OrbitDB identity and keystore integration for the current OrbitDB 4 public APIs, including keystore key generation/storage.
  • Replace older dynamic codec/hash imports with static multiformats imports where needed.
  • Remove obsolete local OrbitDB patches.
  • Clean up known Vite/polyfill build warnings in the examples.
  • Fix varsig verification edge cases for replicated entries, mixed worker/hardware verification, and Node relay default exports.

0.2.10

  • Release metadata update after varsig verification and worker/hardware compatibility fixes.

0.2.9

  • Version metadata update after discoverable passkey recovery work.

0.2.8

  • Add discoverable passkey recovery flows.
  • Add worker-backed keystore demo coverage and fix the worker keystore demo build.
  • Export varsig verification, identity storage, and wrapWithVarsigVerification.
  • Stabilize Chromium E2E/unit coverage and align Ed25519 keystore tests.
  • Sync docs, format the codebase, and restrict CI Playwright runs to Chromium.

0.2.6

  • Release metadata update for the standalone compatibility series.

0.2.5

  • Add standalone compatibility fallback release.

0.2.4

  • Re-enable encrypted-keystore CI and stabilize demo checks.
  • Sync pnpm lockfile with package dependencies.
  • Run CI on pushes to all branches.
  • Merge the standalone WebAuthn toolkit feature branch.

0.2.3

  • Add reusable standalone WebAuthn worker and varsig toolkit exports.
  • Restore ucanto signer metadata and add issuance regression coverage.
  • Stabilize WebAuthn unit harness and mock credentials.
  • Finalize standalone toolkit integration and README updates.
  • Refresh pnpm lockfile.

0.2.2

  • Ship patch-package in dependencies so postinstall works for consumers.

0.2.1

  • Add WebAuthn varsig demo E2E coverage and test-mode stubs for CI.
  • Update CI to focus on Chromium-only runs and disable failing encrypted keystore tests.
  • Add @libp2p/crypto dependency and update lockfile.
  • Publish varsig demo build to Storacha and link in README.

0.2.0

  • Switch iso dependencies to the published @le-space fork and pin @le-space/iso-did@2.1.2.
  • Restore unscoped iso-web from npm to satisfy iso-did runtime deps.
  • Document forked iso packages used for WebAuthn varsig support.
  • Clarify Varsig vs keystore-based DID paths and reference the example demos.

0.1.0 (preview)

  • Initial preview release with WebAuthn DID and varsig provider.