Skip to content

Latest commit

 

History

162 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OrbitDB WebAuthn Identity Providers

CI

⚠️ Security: Experimental release. No formal audit. Use only after your own review.

This package provides:

  • Two WebAuthn-based OrbitDB identity providers.
  • A standalone WebAuthn toolkit export (@le-space/orbitdb-identity-provider-webauthn-did/standalone) for reuse outside OrbitDB identity wiring.
  • A keystore helper export (@le-space/orbitdb-identity-provider-webauthn-did/keystore) for encrypted keystore utilities and provider wiring.
  • WebAuthn-Varsig: No insecure OrbitDB keystore at all. Each entry is signed by WebAuthn (varsig envelope), so keys never leave the authenticator, one Passkey (WebAuthn) prompt per write.
  • Keystore-based DID: Generates an Ed25519/secp256k1 keystore keypair for OrbitDB signing in browser memory. When encryptKeystore is enabled, the private key is encrypted with AES-GCM and only rehydrated in memory after a WebAuthn unlock (PRF, largeBlob, or hmac-secret).

Current WebAuthn Model

  • Discoverable credentials are enabled by default across the shared WebAuthn config.
  • Authentication/assertion requests omit allowCredentials by default, so the browser/authenticator can resolve the credential discoverably.
  • You can switch this centrally with configureWebAuthn({ discoverableCredentials: true|false }).
  • Registration is still the point where this package extracts the credential public key from attestation.
  • Later navigator.credentials.get() assertions do not reliably return the public key again, so identity reconstruction still needs metadata from somewhere else.

In this repo today, metadata recovery works in two layers:

  • Preferred recovery path: store identity metadata in WebAuthn largeBlob and recover it later through discoverable authentication.
  • Fallback recovery path: store the same metadata in browser localStorage.

This means discoverable credentials remove the need to pre-select the credential for authentication, but they do not by themselves eliminate the need for identity metadata persistence.

Recommendation (security-first):

  • Best security: Varsig provider (hardware-backed key for every write).
  • Best balance: Keystore provider with WebAuthn-encrypted keystore (fewer prompts, faster writes, key material in memory during session).

Note: WebAuthn varsig support in this repo relies on our forked @le-space/iso-* packages of Hugo Dias iso-repo (notably @le-space/iso-did and @le-space/iso-webauthn-varsig) to align with the updated varsig flow.

Install

npm install @le-space/orbitdb-identity-provider-webauthn-did

Note: Ed25519 keystore DIDs are handled without patching OrbitDB. The provider generates the requested libp2p key type and stores it through OrbitDB's public keystore API.

WebAuthn Configuration

The package now exposes a central WebAuthn policy API:

import {
  configureWebAuthn,
  getWebAuthnConfig,
  resetWebAuthnConfig,
} from '@le-space/orbitdb-identity-provider-webauthn-did';

configureWebAuthn({ discoverableCredentials: true }); // default
console.log(getWebAuthnConfig());
resetWebAuthnConfig();

Behavior:

  • discoverableCredentials: true
    • registration requests resident/discoverable credentials
    • assertion requests omit allowCredentials
  • discoverableCredentials: false
    • assertion requests target a specific credential ID via allowCredentials

Memory Keystore Quick Start

import {
  WebAuthnDIDProvider,
  OrbitDBWebAuthnIdentityProviderFunction,
} from '@le-space/orbitdb-identity-provider-webauthn-did';

const credential = await WebAuthnDIDProvider.createCredential({
  userId: 'alice@example.com',
  displayName: 'Alice',
});

const identity = await identities.createIdentity({
  provider: OrbitDBWebAuthnIdentityProviderFunction({
    webauthnCredential: credential,
  }),
});

Discoverable Recovery Notes

For the DID-based flow:

  • createCredential() can extract the public key from attestation and derive a DID from it.
  • later discoverable navigator.credentials.get() proves possession of the credential, but usually returns only rawId, authenticatorData, clientDataJSON, signature, and maybe userHandle
  • that assertion is not enough on its own to reconstruct the DID

To address this, the demos now attempt to:

  1. write identity metadata to largeBlob after passkey creation
  2. recover that metadata later through discoverable authentication
  3. fall back to local browser storage if largeBlob is unavailable or empty

Hardware-Secured Varsig Quick Start

import {
  WebAuthnVarsigProvider,
  createWebAuthnVarsigIdentity,
} from '@le-space/orbitdb-identity-provider-webauthn-did';

const credential = await WebAuthnVarsigProvider.createCredential({
  userId: 'alice@example.com',
  displayName: 'Alice',
});

const identity = await createWebAuthnVarsigIdentity({ credential });

For varsig, the same recovery limitation applies: a discoverable assertion identifies the credential but does not re-export the public key. The demos therefore use the same largeBlob-first, local fallback recovery approach for varsig credential metadata.

Standalone Toolkit (without OrbitDB identity provider wiring)

Use the standalone export when you want WebAuthn signer and worker-keystore features independently from OrbitDB identity provider setup.

import {
  createWebAuthnSigner,
  WebAuthnHardwareSignerService,
  createWorkerKeystoreClient,
} from '@le-space/orbitdb-identity-provider-webauthn-did/standalone';

// Create a hardware-backed WebAuthn varsig signer
const signer = await createWebAuthnSigner({
  userId: 'alice@example.com',
  displayName: 'Alice',
});

// Optional: bridge to UCAN signer surface
const ucantoSigner = signer.toUcantoSigner();

// Optional: persisted hardware signer lifecycle
const hardwareService = new WebAuthnHardwareSignerService();
await hardwareService.initialize({
  userId: 'alice@example.com',
  displayName: 'Alice',
});

// Optional: worker-based Ed25519 keystore client
const workerClient = createWorkerKeystoreClient();

Domain Label Guidance (OrbitDB vs UCAN)

toUcantoSigner() supports an optional domainLabel override:

  • OrbitDB entry signing: use the default domain label (orbitdb-entry:).
  • UCAN flows that require a protocol-specific challenge prefix: pass it explicitly (for example ucan-webauthn-v1:).
// OrbitDB-style default (no override)
const orbitdbUcantoSigner = signer.toUcantoSigner();

// UCAN-specific override
const ucanUcantoSigner = signer.toUcantoSigner({
  domainLabel: 'ucan-webauthn-v1:',
});

The verifier side and app protocol should define which domain label is required. IPFS deployment does not change this requirement.

Keystore-based DID (WebAuthn + OrbitDB keystore)

sequenceDiagram
  autonumber
  participant User
  participant App
  participant WebAuthn
  participant Auth as Authenticator
  participant KS as OrbitDB Keystore
  participant Enc as KeystoreEncryption
  participant DB as OrbitDB

  User->>App: Create credential
  App->>WebAuthn: create()
  WebAuthn->>Auth: Create passkey
  Auth-->>WebAuthn: Attestation
  WebAuthn-->>App: Credential

  App->>KS: getKey() or add generated Ed25519 key
  KS-->>App: Keystore keypair

  opt encryptKeystore=true
    App->>Enc: generateSecretKey()
    Enc-->>App: sk
    App->>Enc: encrypt keystore private key (AES-GCM)
    alt prf
      App->>WebAuthn: get() with PRF
      WebAuthn->>Auth: User verification
      Auth-->>WebAuthn: PRF output
      WebAuthn-->>App: PRF bytes
      App->>Enc: wrap sk with PRF
    else largeBlob
      App->>WebAuthn: get() with largeBlob write
      WebAuthn->>Auth: User verification
      Auth-->>WebAuthn: Store sk in largeBlob
      WebAuthn-->>App: largeBlob stored
    else hmac-secret
      App->>WebAuthn: get() with hmac-secret
      WebAuthn->>Auth: User verification
      Auth-->>WebAuthn: HMAC output
      WebAuthn-->>App: HMAC bytes
      App->>Enc: wrap sk with HMAC
    end
  end

  App->>DB: db.put()
  DB->>KS: sign entry with keystore key
  KS-->>DB: Entry signature

  Note over App,KS: Keystore private key is encrypted at rest when `encryptKeystore=true`.
Loading

Varsig (no keystore)

sequenceDiagram
  autonumber
  participant User
  participant App
  participant WebAuthn
  participant Auth as Authenticator
  participant Var as Varsig Provider
  participant DB as OrbitDB

  User->>App: Create credential
  App->>WebAuthn: create()
  WebAuthn->>Auth: Create passkey
  Auth-->>WebAuthn: Attestation
  WebAuthn-->>App: Credential

  User->>App: Create varsig identity
  App->>Var: createIdentity()
  Var->>WebAuthn: get()
  WebAuthn->>Auth: User verification
  Auth-->>WebAuthn: Assertion
  WebAuthn-->>Var: Assertion
  Var->>Var: encode varsig envelope
  Var-->>App: Identity

  User->>App: Add entry
  App->>DB: db.put()
  DB->>Var: signIdentity(payload)
  Var->>WebAuthn: get()
  WebAuthn->>Auth: User verification
  Auth-->>WebAuthn: Assertion
  WebAuthn-->>Var: Assertion
  Var->>Var: encode varsig envelope
  Var-->>DB: Varsig signature
Loading

Examples

Svelte demos:

Scripted examples:

  • examples/ed25519-keystore-did-example.js - Keystore DID flow.
  • examples/encrypted-keystore-example.js - Keystore encryption flow.
  • examples/simple-encryption-integration.js - Keystore + database content encryption.

Mermaid sequences for scripts:

  • docs/EXAMPLE-SEQUENCES.md

Documentation

  • docs/API.md
  • docs/ED25519-KEYSTORE-DID.md
  • docs/WEBAUTHN-ENCRYPTED-KEYSTORE-INTEGRATION.md
  • docs/WEBAUTHN-DID-AND-ORBITDB-IDENTITY.md
  • docs/STANDALONE-API-PLAN.md
  • docs/EXAMPLE-SEQUENCES.md
  • docs/E2E-TEST-SUMMARY.md
  • SECURITY.md
  • CODE_OF_CONDUCT.md

Cross-Project Verification

This repo's own suite covers the units and runs two OrbitDB peers in-process. The strongest evidence that a release actually works, though, comes from outside it: the replication mode matrix in orbitdb-relay, at mocha/relay-replication-mode-matrix.mjs.

It drives six alice/bob pairings through a real relay:

Mode Pairing
alice-worker-bob-hardware-ed25519 worker keystore ↔ hardware Ed25519
alice-worker-bob-hardware-p256 worker keystore ↔ hardware P-256
alice-hardware-ed25519-bob-hardware-p256 the two hardware curves against each other
alice-worker-ed25519-bob-worker-ed25519 worker keystore, both sides
alice-hardware-ed25519-bob-hardware-ed25519 hardware Ed25519, both sides
alice-hardware-p256-bob-hardware-p256 hardware P-256, both sides

Three things make it worth more than an in-repo test. It runs out of process and across repos, against the published package rather than the working tree. It exercises identity verification between two distinct peers — different keystores, different identity documents — instead of one instance talking to itself. And the relay verifies identities itself, so a third independent verifier sits in the path.

That is exactly the surface a change to the signing format breaks. The 0.5.0 signing-context change was validated here before the consumers moved: all six pairings replicate under the new format.

If you change anything that touches signing, identity documents or verification, run that matrix — not just this repo's tests.

Identity Recovery Summary

Current identity recovery behavior in this repo:

  • Discoverable passkeys are the default.
  • Discoverable authentication can recover the credential ID used for assertion.
  • Discoverable authentication does not reliably re-expose the credential public key.
  • Because of that, OrbitDB identity recovery requires metadata persistence.
  • The demos now try largeBlob first for identity metadata recovery.
  • If largeBlob is not supported or has no metadata, the demos fall back to local browser storage.

Practical implication:

  • If you create a passkey on one browser profile and later open the app in a fresh profile, the passkey may still exist in the platform passkey manager, but the app can only reconstruct the OrbitDB identity if it can recover metadata from largeBlob or some other persisted mapping.

Upstream Packages and Temporary Forks

This package builds on the iso-repo toolkit by Hugo Diasiso-base, iso-did and iso-passkeys do the WebAuthn parsing, DID encoding and base conversion that this provider is built on top of. It is excellent work and this package would be considerably larger without it. Thank you.

Four of the dependencies resolve to @le-space builds rather than the published originals. Two different reasons, and they should not be confused:

Temporary forks

Published only to unblock this package, and meant to disappear. Each carries a small delta that belongs upstream.

Dependency Upstream Why it is forked
iso-passkeys iso-passkeys Re-exports parseAttestationObject and unwrapEC2Signature, which already exist upstream but are not part of the public API
iso-base iso-base Kept in step with the forks above
iso-did iso-did Kept in step with the forks above

The iso-passkeys delta is two lines and purely additive — nothing is changed or removed, two existing internal functions are simply exported. It is proposed upstream as hugomrdias/iso-repo#543; once that lands, these three forks and their entries in pnpm.overrides can go. The remaining entries in that block — iso-web, conf>ajv, ajv>fast-uri — are unrelated and stay.

Not a fork

iso-webauthn-varsig is a new package rather than a modified copy of an existing one. It implements WebAuthn varsig signing for OrbitDB oplog entries and has no upstream equivalent. It follows iso-repo conventions and lives in the same lineage, which is why it is named the way it is.

It follows the non-recursive varsig layout, which shipped in 0.2.0.

One thing is still open, and it is on the spec side rather than here: the WebAuthn varsig header. webauthn-varsig-header is TODO in ChainAgnostic/varsig#11, so the 0x300001 marker this package writes is a private-use codepoint chosen here rather than an allocated one. Expect the wire format to change once that is settled.

Not forked at all

iso-web appears in pnpm.overrides but resolves to the genuine upstream package. The entry only pins a version.

Licensing

All iso-repo packages are MIT, © Hugo Dias. The forks keep the original license and author fields, so authorship travels with them; only the package name changes.

License

MIT. See LICENSE.

About

WebAuthn-based DID identity provider for OrbitDB for hardware-secured identity creation, oplog signing and encrypted keystores. Biometric Passkey authentication

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages