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
encryptKeystoreis enabled, the private key is encrypted with AES-GCM and only rehydrated in memory after a WebAuthn unlock (PRF, largeBlob, or hmac-secret).
- Discoverable credentials are enabled by default across the shared WebAuthn config.
- Authentication/assertion requests omit
allowCredentialsby 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
largeBloband 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.
npm install @le-space/orbitdb-identity-provider-webauthn-didNote: 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.
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
- assertion requests target a specific credential ID via
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,
}),
});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 onlyrawId,authenticatorData,clientDataJSON,signature, and maybeuserHandle - that assertion is not enough on its own to reconstruct the DID
To address this, the demos now attempt to:
- write identity metadata to
largeBlobafter passkey creation - recover that metadata later through discoverable authentication
- fall back to local browser storage if
largeBlobis unavailable or empty
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.
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();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.
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`.
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
Svelte demos:
- Live demo directory: https://le-space.github.io/orbitdb-identity-provider-webauthn-did/
examples/ed25519-encrypted-keystore-demo/- Ed25519 keystore DID; keystore encrypted at rest with WebAuthn (PRF when available, otherwise largeBlob/hmac-secret). Live demo: https://le-space.github.io/orbitdb-identity-provider-webauthn-did/ed25519-encrypted-keystore-demo/examples/webauthn-varsig-demo/- Varsig provider with passkey signing for each entry. Includes discoverable credential diagnostics,Use Existing Passkey,largeBlobvarsig metadata recovery, and local fallback recovery. Live demo: https://le-space.github.io/orbitdb-identity-provider-webauthn-did/webauthn-varsig-demo/examples/webauthn-todo-demo/- WebAuthn DID (no keystore signing; identity-only). Includes discoverable credential diagnostics,Use Existing Passkey,largeBlobidentity metadata recovery, and local fallback recovery. Live demo: https://le-space.github.io/orbitdb-identity-provider-webauthn-did/webauthn-todo-demo/
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
docs/API.mddocs/ED25519-KEYSTORE-DID.mddocs/WEBAUTHN-ENCRYPTED-KEYSTORE-INTEGRATION.mddocs/WEBAUTHN-DID-AND-ORBITDB-IDENTITY.mddocs/STANDALONE-API-PLAN.mddocs/EXAMPLE-SEQUENCES.mddocs/E2E-TEST-SUMMARY.mdSECURITY.mdCODE_OF_CONDUCT.md
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.
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
largeBlobfirst for identity metadata recovery. - If
largeBlobis 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
largeBlobor some other persisted mapping.
This package builds on the iso-repo
toolkit by Hugo Dias — iso-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:
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.
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.
iso-web appears in pnpm.overrides but resolves to the genuine upstream
package. The entry only pins a version.
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.
MIT. See LICENSE.