@le-space/orbitdb-identity-provider-webauthn-did 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/package.json +4 -2
- package/src/keystore/derived-signing-key.js +198 -0
- package/src/keystore/provider.js +35 -8
- package/src/varsig/assertion.js +1 -30
- package/src/varsig/credential.js +1 -30
- package/src/verification.js +12 -10
- package/src/webauthn/debug-log.js +101 -0
- package/src/webauthn/provider.js +14 -30
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.2
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Restore debug logging in the browser. `weald`, which `@libp2p/logger` uses,
|
|
10
|
+
does `import { ms as humanize } from 'ms'` — a named export that only exists
|
|
11
|
+
in `ms@3`+. All three demos pinned `ms` to `2.1.3`, which is CommonJS with no
|
|
12
|
+
named export, so the import resolved to `undefined` and the first debug call
|
|
13
|
+
threw `TypeError: (0, import_ms2.ms) is not a function`. Enabling `DEBUG` in a
|
|
14
|
+
browser therefore crashed credential creation outright. Dropping the pin lets
|
|
15
|
+
`weald` resolve the version it declares.
|
|
16
|
+
|
|
17
|
+
This is why `webauthn-logging-e2e` had been failing continuously: it is the
|
|
18
|
+
only suite that sets `localStorage.debug`, so it was the only one to reach the
|
|
19
|
+
broken path.
|
|
20
|
+
|
|
21
|
+
- Clear the production dependency advisories. `iso-web > iso-kv > conf > ajv`
|
|
22
|
+
carried four high `fast-uri` advisories and one moderate `ajv` one; the scoped
|
|
23
|
+
overrides `conf>ajv` and `ajv>fast-uri` resolve it. Scoped deliberately: a
|
|
24
|
+
blanket `ajv` override also hits ESLint, which needs ajv 6 and dies on the
|
|
25
|
+
removed `missingRefs` option.
|
|
26
|
+
`pnpm audit --prod` now reports nothing, so the enforced CI gate moves from
|
|
27
|
+
`critical` to `moderate`.
|
|
28
|
+
- Stop writing WebAuthn credentials to the console. `logWebAuthnResponse()` dumped
|
|
29
|
+
the whole credential — `rawId`, `attestationObject`, `clientDataJSON`,
|
|
30
|
+
`signature`, `getPublicKey()` and `getClientExtensionResults()` — on every
|
|
31
|
+
`navigator.credentials.create()` and `.get()`, unconditionally and in
|
|
32
|
+
production code paths. Two things made that more than noise: extension results
|
|
33
|
+
can carry the PRF output, which is what both the keystore encryption and (since
|
|
34
|
+
0.4.1) the OrbitDB signing key derive from, and `rawId` is a stable per-user,
|
|
35
|
+
per-RP identifier that landed in any console capture or session replay the
|
|
36
|
+
embedding app happened to run. Replaced by a single shared helper that logs
|
|
37
|
+
shape only — byte lengths, presence flags and the names of the extensions that
|
|
38
|
+
returned results — behind the package debug logger, so it is silent unless
|
|
39
|
+
`DEBUG=orbitdb-identity-provider-webauthn-did*` is set. Closes #22.
|
|
40
|
+
- Keep the demos' P2P component out of the server render. Turning on `ssr`
|
|
41
|
+
so prerendering emitted real content also meant `+page.svelte` was evaluated
|
|
42
|
+
in Node, and it imported the libp2p stack at module scope — reaching
|
|
43
|
+
`@libp2p/webrtc` and its native `node-datachannel` binding, which the demos
|
|
44
|
+
do not build. The dev server returned 500 for every request. The component is
|
|
45
|
+
loaded in the browser only now; the shared shell still renders server-side, so
|
|
46
|
+
prerendering keeps producing a real title and description.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Route the remaining informational `console.log` output through the debug
|
|
51
|
+
logger. A library should not print unconditionally; `console.error` and
|
|
52
|
+
`console.warn` stay as they are, so genuine failures remain visible. `src/`
|
|
53
|
+
went from 61 `console.*` calls to 16, none of them `console.log`.
|
|
54
|
+
- Deduplicate three byte-identical copies of the WebAuthn debug helper into
|
|
55
|
+
`src/webauthn/debug-log.js`.
|
|
56
|
+
|
|
57
|
+
## 0.4.1
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- Derive the OrbitDB signing key from the passkey PRF output instead of letting
|
|
62
|
+
the keystore generate a random one. `Identities.createIdentity` takes
|
|
63
|
+
`keystore.getKey(id) || keystore.createKey(id)`, and `createKey` is random, so
|
|
64
|
+
the same passkey previously produced a different `publicKey` and
|
|
65
|
+
`signatures.id` — and therefore a different identity document — on every
|
|
66
|
+
device. Seeding the keystore during `getId()`, the last point before OrbitDB
|
|
67
|
+
asks for the key, makes the whole document reproducible: one block to keep
|
|
68
|
+
retrievable for a DID instead of one per device, and a device that loses its
|
|
69
|
+
keystore while keeping the passkey reconstructs the same identity rather than
|
|
70
|
+
minting another. Needs no change to `@orbitdb/core`.
|
|
71
|
+
- `createCredential()` now requests the PRF extension even when the keystore is
|
|
72
|
+
not encrypted, and stores the input. Without a stored input the output would
|
|
73
|
+
differ per call, so a credential registered without one can never get a
|
|
74
|
+
reproducible identity. Requesting it is harmless where unsupported.
|
|
75
|
+
|
|
76
|
+
### Notes
|
|
77
|
+
|
|
78
|
+
- Falls back cleanly. No PRF support, no stored input, or a refused assertion
|
|
79
|
+
leaves the keystore to generate its own key — 0.4.0 behaviour, stable per
|
|
80
|
+
device but not across devices. Opt out with
|
|
81
|
+
`deriveSigningKeyFromPrf: false`.
|
|
82
|
+
- An existing keystore key is never replaced. Swapping it under a device that
|
|
83
|
+
already has history would mint a second document for the DID, which is what
|
|
84
|
+
this avoids. Installs upgrading from 0.4.0 keep their key; fresh installs get
|
|
85
|
+
a reproducible identity.
|
|
86
|
+
- Costs one extra assertion the first time an identity is created on a device.
|
|
87
|
+
The key is persisted, so later loads do not repeat it.
|
|
88
|
+
|
|
5
89
|
## 0.4.0
|
|
6
90
|
|
|
7
91
|
### Breaking
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/orbitdb-identity-provider-webauthn-did",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "WebAuthn-based DID identity provider for OrbitDB for hardware-secured wallets and biometric Passkey authentication",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -143,7 +143,9 @@
|
|
|
143
143
|
"iso-base": "npm:@le-space/iso-base",
|
|
144
144
|
"iso-did": "npm:@le-space/iso-did@2.1.2",
|
|
145
145
|
"iso-web": "^3.1.2",
|
|
146
|
-
"iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.2.0"
|
|
146
|
+
"iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.2.0",
|
|
147
|
+
"conf>ajv": "^8.18.0",
|
|
148
|
+
"ajv>fast-uri": "^3.1.4"
|
|
147
149
|
},
|
|
148
150
|
"onlyBuiltDependencies": [
|
|
149
151
|
"@ipshipyard/node-datachannel",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module DerivedSigningKey
|
|
3
|
+
* @description
|
|
4
|
+
* Derives the OrbitDB signing key deterministically from the passkey's PRF
|
|
5
|
+
* output instead of letting the keystore generate a random one.
|
|
6
|
+
*
|
|
7
|
+
* `Identities.createIdentity` in `@orbitdb/core` does:
|
|
8
|
+
*
|
|
9
|
+
* const privateKey = await keystore.getKey(id) || await keystore.createKey(id)
|
|
10
|
+
* const publicKey = keystore.getPublic(privateKey)
|
|
11
|
+
* const idSignature = await signMessage(privateKey, id)
|
|
12
|
+
*
|
|
13
|
+
* `createKey` generates a random secp256k1 key, so `publicKey` and
|
|
14
|
+
* `signatures.id` differ on every device even for the same passkey — one
|
|
15
|
+
* identity document per device. Seeding the keystore with a PRF-derived key
|
|
16
|
+
* before OrbitDB asks for it makes all of that reproducible: the same passkey
|
|
17
|
+
* yields the same document everywhere, so there is exactly one block to keep
|
|
18
|
+
* retrievable and a device that loses its keystore reconstructs the identity
|
|
19
|
+
* rather than minting another one.
|
|
20
|
+
*
|
|
21
|
+
* Needs no change to `@orbitdb/core` — it only has to happen before
|
|
22
|
+
* `getKey(id)` is called, which `getId()` is.
|
|
23
|
+
*
|
|
24
|
+
* See issue #18, option 1.
|
|
25
|
+
*/
|
|
26
|
+
import { logger } from '@libp2p/logger';
|
|
27
|
+
import { privateKeyFromRaw } from '@libp2p/crypto/keys';
|
|
28
|
+
|
|
29
|
+
import { CRYPTO_ALGORITHMS } from '../constants.js';
|
|
30
|
+
import { buildCredentialRequestOptions } from '../webauthn/config.js';
|
|
31
|
+
|
|
32
|
+
const log = logger('orbitdb-identity-provider-webauthn-did:derived-key');
|
|
33
|
+
|
|
34
|
+
// Bumping this rotates every derived key, so treat it as a breaking change.
|
|
35
|
+
const DERIVATION_INFO = 'orbitdb-identity-provider-webauthn-did:signing-key:v1';
|
|
36
|
+
|
|
37
|
+
const SECP256K1_KEY_BYTES = 32;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* HKDF-SHA256 a 32-byte candidate from the PRF seed.
|
|
41
|
+
* @param {Uint8Array} seed - PRF output.
|
|
42
|
+
* @param {string} info - Domain separation string.
|
|
43
|
+
* @returns {Promise<Uint8Array>} 32 bytes.
|
|
44
|
+
*/
|
|
45
|
+
async function hkdf(seed, info) {
|
|
46
|
+
const baseKey = await crypto.subtle.importKey(
|
|
47
|
+
'raw',
|
|
48
|
+
seed,
|
|
49
|
+
CRYPTO_ALGORITHMS.HKDF,
|
|
50
|
+
false,
|
|
51
|
+
['deriveBits']
|
|
52
|
+
);
|
|
53
|
+
const bits = await crypto.subtle.deriveBits(
|
|
54
|
+
{
|
|
55
|
+
name: CRYPTO_ALGORITHMS.HKDF,
|
|
56
|
+
hash: CRYPTO_ALGORITHMS.SHA_256,
|
|
57
|
+
salt: new Uint8Array(0),
|
|
58
|
+
info: new TextEncoder().encode(info),
|
|
59
|
+
},
|
|
60
|
+
baseKey,
|
|
61
|
+
SECP256K1_KEY_BYTES * 8
|
|
62
|
+
);
|
|
63
|
+
return new Uint8Array(bits);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Turn a PRF seed into a valid secp256k1 private key.
|
|
68
|
+
*
|
|
69
|
+
* Values outside the curve order are invalid. That is vanishingly unlikely for
|
|
70
|
+
* a 256-bit hash, but retrying with a counter keeps the derivation total and
|
|
71
|
+
* deterministic rather than probabilistic.
|
|
72
|
+
*
|
|
73
|
+
* @param {Uint8Array} seed - PRF output.
|
|
74
|
+
* @param {string} did - Identity DID, mixed in for domain separation.
|
|
75
|
+
* @returns {Promise<Uint8Array>} A valid 32-byte secp256k1 private key.
|
|
76
|
+
*/
|
|
77
|
+
export async function deriveSigningKeyBytes(seed, did) {
|
|
78
|
+
for (let counter = 0; counter < 256; counter++) {
|
|
79
|
+
const candidate = await hkdf(seed, `${DERIVATION_INFO}:${did}:${counter}`);
|
|
80
|
+
try {
|
|
81
|
+
privateKeyFromRaw(candidate);
|
|
82
|
+
return candidate;
|
|
83
|
+
} catch {
|
|
84
|
+
log('derived scalar %d was not a valid secp256k1 key, retrying', counter);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Unreachable short of a broken hash.
|
|
88
|
+
throw new Error('could not derive a valid secp256k1 key from the PRF seed');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Ask the authenticator for the credential's PRF output.
|
|
93
|
+
*
|
|
94
|
+
* Returns null whenever a stable PRF output cannot be obtained — no stored
|
|
95
|
+
* PRF input, no PRF support, or a refused assertion. Callers must treat null
|
|
96
|
+
* as "carry on without a derived key" rather than as an error.
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} credential - Stored WebAuthn credential info.
|
|
99
|
+
* @param {Object} [options]
|
|
100
|
+
* @param {string} [options.rpId] - Relying party id.
|
|
101
|
+
* @returns {Promise<Uint8Array|null>} PRF output, or null.
|
|
102
|
+
*/
|
|
103
|
+
export async function getPrfOutput(credential, { rpId } = {}) {
|
|
104
|
+
// Without the original input the output would differ per call, which is
|
|
105
|
+
// worse than not deriving at all.
|
|
106
|
+
if (!credential?.prfInput) {
|
|
107
|
+
log('credential has no stored prfInput; cannot derive a stable key');
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof navigator === 'undefined' || !navigator.credentials?.get) {
|
|
112
|
+
log('no WebAuthn available; cannot derive a key');
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const rawCredentialId =
|
|
117
|
+
credential.rawCredentialId instanceof Uint8Array
|
|
118
|
+
? credential.rawCredentialId
|
|
119
|
+
: new Uint8Array(credential.rawCredentialId ?? []);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const assertion = await navigator.credentials.get(
|
|
123
|
+
buildCredentialRequestOptions({
|
|
124
|
+
challenge: await crypto.subtle.digest(
|
|
125
|
+
CRYPTO_ALGORITHMS.SHA_256,
|
|
126
|
+
new TextEncoder().encode(DERIVATION_INFO)
|
|
127
|
+
),
|
|
128
|
+
credentialId: rawCredentialId,
|
|
129
|
+
rpId: rpId ?? globalThis.window?.location?.hostname,
|
|
130
|
+
userVerification: 'required',
|
|
131
|
+
extensions: { prf: { eval: { first: credential.prfInput } } },
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const results = assertion?.getClientExtensionResults?.()?.prf?.results;
|
|
136
|
+
if (!results?.first) {
|
|
137
|
+
log('authenticator returned no PRF output');
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return new Uint8Array(results.first);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
log('PRF assertion failed: %s', error.message);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Seed the keystore with a PRF-derived signing key for this DID.
|
|
149
|
+
*
|
|
150
|
+
* An existing key is never replaced. Overwriting one would change `publicKey`
|
|
151
|
+
* and `signatures.id`, producing a second identity document for the same DID —
|
|
152
|
+
* precisely what this is meant to avoid. Installs that already hold a random
|
|
153
|
+
* key therefore keep it and stay on 0.4.0 behaviour, which is stable per
|
|
154
|
+
* device; only fresh installs get a reproducible identity.
|
|
155
|
+
*
|
|
156
|
+
* @param {Object} params
|
|
157
|
+
* @param {Object} params.keystore - OrbitDB keystore.
|
|
158
|
+
* @param {string} params.did - Identity DID.
|
|
159
|
+
* @param {Object} params.credential - Stored WebAuthn credential info.
|
|
160
|
+
* @param {string} [params.rpId] - Relying party id.
|
|
161
|
+
* @returns {Promise<'derived'|'existing'|'unavailable'>} What happened.
|
|
162
|
+
*/
|
|
163
|
+
export async function ensureDerivedSigningKey({
|
|
164
|
+
keystore,
|
|
165
|
+
did,
|
|
166
|
+
credential,
|
|
167
|
+
rpId,
|
|
168
|
+
}) {
|
|
169
|
+
if (!keystore || !did || !credential) return 'unavailable';
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
if (await keystore.getKey(did)) {
|
|
173
|
+
log('keystore already holds a key for this DID; leaving it alone');
|
|
174
|
+
return 'existing';
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
log('could not read the keystore: %s', error.message);
|
|
178
|
+
return 'unavailable';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const seed = await getPrfOutput(credential, { rpId });
|
|
182
|
+
if (!seed) {
|
|
183
|
+
log(
|
|
184
|
+
'no PRF output; falling back to a keystore-generated key, so this identity is stable per device but not reproducible across devices'
|
|
185
|
+
);
|
|
186
|
+
return 'unavailable';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const privateKey = await deriveSigningKeyBytes(seed, did);
|
|
191
|
+
await keystore.addKey(did, { privateKey });
|
|
192
|
+
log('seeded the keystore with a PRF-derived signing key');
|
|
193
|
+
return 'derived';
|
|
194
|
+
} catch (error) {
|
|
195
|
+
log('failed to seed the derived key: %s', error.message);
|
|
196
|
+
return 'unavailable';
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/keystore/provider.js
CHANGED
|
@@ -7,6 +7,7 @@ import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
|
7
7
|
import { varint } from 'multiformats';
|
|
8
8
|
import { base58btc } from 'multiformats/bases/base58';
|
|
9
9
|
import * as KeystoreEncryption from './encryption.js';
|
|
10
|
+
import { ensureDerivedSigningKey } from './derived-signing-key.js';
|
|
10
11
|
import { WebAuthnDIDProvider } from '../webauthn/provider.js';
|
|
11
12
|
import {
|
|
12
13
|
identityProofKey,
|
|
@@ -56,8 +57,12 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
56
57
|
keystoreKeyType = KEY_TYPES.SECP256K1,
|
|
57
58
|
encryptKeystore = false,
|
|
58
59
|
keystoreEncryptionMethod = KEYSTORE_ENCRYPTION_METHODS.PRF,
|
|
60
|
+
deriveSigningKeyFromPrf = true,
|
|
59
61
|
}) {
|
|
60
62
|
this.credential = webauthnCredential;
|
|
63
|
+
// Costs one extra assertion the first time an identity is created on a
|
|
64
|
+
// device. Set false to keep a keystore-generated key instead.
|
|
65
|
+
this.deriveSigningKeyFromPrf = deriveSigningKeyFromPrf;
|
|
61
66
|
this.webauthnProvider = new WebAuthnDIDProvider(webauthnCredential);
|
|
62
67
|
this.type = IDENTITY_TYPES.WEBAUTHN; // Set instance property
|
|
63
68
|
this.useKeystoreDID = useKeystoreDID; // Flag to use Ed25519 DID from keystore
|
|
@@ -74,9 +79,15 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
74
79
|
|
|
75
80
|
/**
|
|
76
81
|
* Resolve the identity DID.
|
|
82
|
+
*
|
|
83
|
+
* OrbitDB passes its own keystore in `options` and calls this before
|
|
84
|
+
* `keystore.getKey(id)`, which is the only window in which the signing key
|
|
85
|
+
* can be seeded deterministically. See derived-signing-key.js.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} [options] - Options supplied by OrbitDB.
|
|
77
88
|
* @returns {Promise<string>} DID string.
|
|
78
89
|
*/
|
|
79
|
-
async getId() {
|
|
90
|
+
async getId(options = {}) {
|
|
80
91
|
identityLog('getId() called');
|
|
81
92
|
|
|
82
93
|
// If useKeystoreDID flag is set, create Ed25519 DID from keystore
|
|
@@ -95,6 +106,22 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
95
106
|
|
|
96
107
|
// Default: Return P-256 DID from WebAuthn credential
|
|
97
108
|
const did = await WebAuthnDIDProvider.createDID(this.credential);
|
|
109
|
+
|
|
110
|
+
// Seed the signing key before OrbitDB reaches for it. Never fatal: without
|
|
111
|
+
// PRF the keystore generates its own key and the identity stays stable per
|
|
112
|
+
// device, just not reproducible across them.
|
|
113
|
+
if (this.deriveSigningKeyFromPrf !== false) {
|
|
114
|
+
const keystore = options.keystore ?? this.keystore;
|
|
115
|
+
if (keystore) {
|
|
116
|
+
const outcome = await ensureDerivedSigningKey({
|
|
117
|
+
keystore,
|
|
118
|
+
did,
|
|
119
|
+
credential: this.credential,
|
|
120
|
+
});
|
|
121
|
+
identityLog('Derived signing key: %s', outcome);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
98
125
|
identityLog(
|
|
99
126
|
'getId() returning P-256 DID: %s',
|
|
100
127
|
did.substring(0, 32) + '...'
|
|
@@ -321,11 +348,11 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
321
348
|
*/
|
|
322
349
|
async createEncryptedKeystore() {
|
|
323
350
|
if (!this.encryptKeystore) {
|
|
324
|
-
|
|
351
|
+
identityLog('ℹ️ Keystore encryption not enabled, skipping');
|
|
325
352
|
return;
|
|
326
353
|
}
|
|
327
354
|
|
|
328
|
-
|
|
355
|
+
identityLog(
|
|
329
356
|
'🔐 Creating encrypted keystore with method:',
|
|
330
357
|
this.keystoreEncryptionMethod
|
|
331
358
|
);
|
|
@@ -417,7 +444,7 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
417
444
|
}
|
|
418
445
|
|
|
419
446
|
// Store encrypted keystore
|
|
420
|
-
|
|
447
|
+
identityLog(
|
|
421
448
|
'💾 Storing encrypted keystore with credentialId:',
|
|
422
449
|
this.credential.credentialId?.substring(0, 16) + '...'
|
|
423
450
|
);
|
|
@@ -430,7 +457,7 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
430
457
|
publicKey: publicKeyBytes,
|
|
431
458
|
keyType,
|
|
432
459
|
};
|
|
433
|
-
|
|
460
|
+
identityLog('✅ Encrypted keystore stored successfully');
|
|
434
461
|
|
|
435
462
|
identityLog('Encrypted keystore created and stored successfully');
|
|
436
463
|
} catch (error) {
|
|
@@ -618,15 +645,15 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
618
645
|
// If encryption is enabled, create and unlock encrypted keystore
|
|
619
646
|
if (encryptKeystore && keystore) {
|
|
620
647
|
try {
|
|
621
|
-
|
|
648
|
+
identityLog(
|
|
622
649
|
'🔐 Creating encrypted keystore with',
|
|
623
650
|
keystoreEncryptionMethod,
|
|
624
651
|
'...'
|
|
625
652
|
);
|
|
626
653
|
await provider.createEncryptedKeystore();
|
|
627
|
-
|
|
654
|
+
identityLog('🔓 Unlocking encrypted keystore...');
|
|
628
655
|
await provider.unlockEncryptedKeystore();
|
|
629
|
-
|
|
656
|
+
identityLog('✅ Encrypted keystore created and unlocked successfully');
|
|
630
657
|
identityLog('Encrypted keystore created and unlocked');
|
|
631
658
|
} catch (error) {
|
|
632
659
|
// Log error visibly so users know encryption failed
|
package/src/varsig/assertion.js
CHANGED
|
@@ -19,40 +19,11 @@ import {
|
|
|
19
19
|
VarsigVerificationError,
|
|
20
20
|
WebAuthnAuthenticationError,
|
|
21
21
|
} from '../errors.js';
|
|
22
|
+
import { logWebAuthnResponse } from '../webauthn/debug-log.js';
|
|
22
23
|
|
|
23
24
|
const isTestMode = () =>
|
|
24
25
|
typeof window !== 'undefined' && window.__PLAYWRIGHT__ === true;
|
|
25
26
|
|
|
26
|
-
function logWebAuthnResponse(label, credential) {
|
|
27
|
-
const response = credential?.response;
|
|
28
|
-
const getPublicKeyResult =
|
|
29
|
-
typeof response?.getPublicKey === 'function'
|
|
30
|
-
? response.getPublicKey()
|
|
31
|
-
: null;
|
|
32
|
-
|
|
33
|
-
console.group(`[WebAuthn Debug] ${label}`);
|
|
34
|
-
console.log('credential', credential);
|
|
35
|
-
console.log('credential.id', credential?.id);
|
|
36
|
-
console.log('credential.type', credential?.type);
|
|
37
|
-
console.log('credential.rawId', credential?.rawId);
|
|
38
|
-
console.log('credential.response', response);
|
|
39
|
-
console.log('response.clientDataJSON', response?.clientDataJSON);
|
|
40
|
-
console.log('response.attestationObject', response?.attestationObject);
|
|
41
|
-
console.log('response.authenticatorData', response?.authenticatorData);
|
|
42
|
-
console.log('response.signature', response?.signature);
|
|
43
|
-
console.log('response.userHandle', response?.userHandle);
|
|
44
|
-
console.log(
|
|
45
|
-
'response.getPublicKey exists',
|
|
46
|
-
typeof response?.getPublicKey === 'function'
|
|
47
|
-
);
|
|
48
|
-
console.log('response.getPublicKey()', getPublicKeyResult);
|
|
49
|
-
console.log(
|
|
50
|
-
'client extension results',
|
|
51
|
-
credential?.getClientExtensionResults?.() || null
|
|
52
|
-
);
|
|
53
|
-
console.groupEnd();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
27
|
/**
|
|
57
28
|
* Run a WebAuthn assertion for a payload.
|
|
58
29
|
* @param {Object} credential - WebAuthn credential info.
|
package/src/varsig/credential.js
CHANGED
|
@@ -5,40 +5,11 @@ import { DIDKey } from 'iso-did';
|
|
|
5
5
|
import { parseAttestationObject } from 'iso-passkeys';
|
|
6
6
|
import { toArrayBuffer } from './utils.js';
|
|
7
7
|
import { buildAuthenticatorSelection } from '../webauthn/config.js';
|
|
8
|
+
import { logWebAuthnResponse } from '../webauthn/debug-log.js';
|
|
8
9
|
|
|
9
10
|
const isTestMode = () =>
|
|
10
11
|
typeof window !== 'undefined' && window.__PLAYWRIGHT__ === true;
|
|
11
12
|
|
|
12
|
-
function logWebAuthnResponse(label, credential) {
|
|
13
|
-
const response = credential?.response;
|
|
14
|
-
const getPublicKeyResult =
|
|
15
|
-
typeof response?.getPublicKey === 'function'
|
|
16
|
-
? response.getPublicKey()
|
|
17
|
-
: null;
|
|
18
|
-
|
|
19
|
-
console.group(`[WebAuthn Debug] ${label}`);
|
|
20
|
-
console.log('credential', credential);
|
|
21
|
-
console.log('credential.id', credential?.id);
|
|
22
|
-
console.log('credential.type', credential?.type);
|
|
23
|
-
console.log('credential.rawId', credential?.rawId);
|
|
24
|
-
console.log('credential.response', response);
|
|
25
|
-
console.log('response.clientDataJSON', response?.clientDataJSON);
|
|
26
|
-
console.log('response.attestationObject', response?.attestationObject);
|
|
27
|
-
console.log('response.authenticatorData', response?.authenticatorData);
|
|
28
|
-
console.log('response.signature', response?.signature);
|
|
29
|
-
console.log('response.userHandle', response?.userHandle);
|
|
30
|
-
console.log(
|
|
31
|
-
'response.getPublicKey exists',
|
|
32
|
-
typeof response?.getPublicKey === 'function'
|
|
33
|
-
);
|
|
34
|
-
console.log('response.getPublicKey()', getPublicKeyResult);
|
|
35
|
-
console.log(
|
|
36
|
-
'client extension results',
|
|
37
|
-
credential?.getClientExtensionResults?.() || null
|
|
38
|
-
);
|
|
39
|
-
console.groupEnd();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
13
|
/**
|
|
43
14
|
* Extract public key info from a WebAuthn attestation object.
|
|
44
15
|
* @param {Uint8Array} attestationObject - Raw attestation object bytes.
|
package/src/verification.js
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* without relying on external network calls or IPFS gateway timeouts.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { logger } from '@libp2p/logger';
|
|
11
|
+
|
|
12
|
+
const log = logger('orbitdb-identity-provider-webauthn-did:verification');
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Verify database update events using pragmatic approach
|
|
12
16
|
* @param {Object} database - The OrbitDB database instance
|
|
@@ -19,7 +23,7 @@ export async function verifyDatabaseUpdate(
|
|
|
19
23
|
identityHash,
|
|
20
24
|
expectedWebAuthnDID
|
|
21
25
|
) {
|
|
22
|
-
|
|
26
|
+
log('🔄 Verifying database update event');
|
|
23
27
|
|
|
24
28
|
// Simple logic: if an update is happening in our database and our database
|
|
25
29
|
// identity matches the expected WebAuthn DID, then the update is from us
|
|
@@ -72,7 +76,7 @@ export async function verifyIdentityStorage(
|
|
|
72
76
|
identity,
|
|
73
77
|
timeoutMs = 5000
|
|
74
78
|
) {
|
|
75
|
-
|
|
79
|
+
log('🔍 Verifying identity storage...');
|
|
76
80
|
|
|
77
81
|
try {
|
|
78
82
|
// Try to retrieve the identity from the store with a timeout
|
|
@@ -131,10 +135,8 @@ export async function verifyDataEntries(
|
|
|
131
135
|
const { matchFn, checkLog = true } = options;
|
|
132
136
|
const verificationResults = new Map();
|
|
133
137
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
);
|
|
137
|
-
console.log(`🎯 Expected WebAuthn DID: ${expectedWebAuthnDID}`);
|
|
138
|
+
log(`🔍 Starting verification of ${dataEntries.length} data entries...`);
|
|
139
|
+
log(`🎯 Expected WebAuthn DID: ${expectedWebAuthnDID}`);
|
|
138
140
|
|
|
139
141
|
try {
|
|
140
142
|
// Check if our database identity matches the expected WebAuthn DID
|
|
@@ -142,7 +144,7 @@ export async function verifyDataEntries(
|
|
|
142
144
|
const databaseIdentityMatches =
|
|
143
145
|
databaseIdentity?.id === expectedWebAuthnDID;
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
log('🔑 Database identity check:', {
|
|
146
148
|
databaseDID: databaseIdentity?.id,
|
|
147
149
|
expectedDID: expectedWebAuthnDID,
|
|
148
150
|
matches: databaseIdentityMatches,
|
|
@@ -150,7 +152,7 @@ export async function verifyDataEntries(
|
|
|
150
152
|
|
|
151
153
|
for (const entry of dataEntries) {
|
|
152
154
|
try {
|
|
153
|
-
|
|
155
|
+
log(`📝 Verifying entry: ${entry.id}`);
|
|
154
156
|
|
|
155
157
|
// Method 1: Check if we can access the entry in our database
|
|
156
158
|
const entryInDb = await database.get(entry.id);
|
|
@@ -204,7 +206,7 @@ export async function verifyDataEntries(
|
|
|
204
206
|
|
|
205
207
|
verificationResults.set(entry.id, result);
|
|
206
208
|
|
|
207
|
-
|
|
209
|
+
log(
|
|
208
210
|
`${verificationSuccess ? '✅' : '❌'} Entry ${entry.id}: ${verificationSuccess ? 'VERIFIED' : 'FAILED'}`
|
|
209
211
|
);
|
|
210
212
|
} catch (error) {
|
|
@@ -232,7 +234,7 @@ export async function verifyDataEntries(
|
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
|
|
237
|
+
log(
|
|
236
238
|
`✅ Verification completed: ${verificationResults.size} entries processed`
|
|
237
239
|
);
|
|
238
240
|
return verificationResults;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module WebAuthnDebugLog
|
|
3
|
+
* @description
|
|
4
|
+
* Describes a WebAuthn credential or assertion for debugging without putting
|
|
5
|
+
* its contents anywhere.
|
|
6
|
+
*
|
|
7
|
+
* This replaces three copies of a helper that wrote the whole credential to
|
|
8
|
+
* `console` on every `navigator.credentials.create()` and `.get()` —
|
|
9
|
+
* unconditionally, in production code paths. Two reasons that mattered:
|
|
10
|
+
*
|
|
11
|
+
* - `getClientExtensionResults()` was logged verbatim. Since 0.4.1 the PRF
|
|
12
|
+
* extension is requested on every credential, so that object can carry the
|
|
13
|
+
* PRF output, which is the material both the keystore encryption and the
|
|
14
|
+
* OrbitDB signing key are derived from.
|
|
15
|
+
* - `rawId` is a stable per-user, per-RP identifier. Emitting it on every call
|
|
16
|
+
* put it into any console capture, error reporter or session replay the
|
|
17
|
+
* embedding app happens to run.
|
|
18
|
+
*
|
|
19
|
+
* What is logged here is shape, not content: byte lengths, presence flags, and
|
|
20
|
+
* which extensions came back. Enough to tell "the authenticator returned no PRF
|
|
21
|
+
* result" from "the assertion never happened", which is what these logs were
|
|
22
|
+
* ever used for.
|
|
23
|
+
*
|
|
24
|
+
* See issue #22.
|
|
25
|
+
*/
|
|
26
|
+
import { logger } from '@libp2p/logger';
|
|
27
|
+
|
|
28
|
+
const log = logger('orbitdb-identity-provider-webauthn-did:webauthn');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Byte length of an ArrayBuffer or view, or null when absent.
|
|
32
|
+
* @param {ArrayBuffer|ArrayBufferView|null|undefined} value - Buffer-ish value.
|
|
33
|
+
* @returns {number|null} Length in bytes, or null.
|
|
34
|
+
*/
|
|
35
|
+
function byteLength(value) {
|
|
36
|
+
if (!value) return null;
|
|
37
|
+
if (typeof value.byteLength === 'number') return value.byteLength;
|
|
38
|
+
if (typeof value.length === 'number') return value.length;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Names of the extensions that returned results, without their values.
|
|
44
|
+
*
|
|
45
|
+
* The names alone are diagnostic — they say whether PRF engaged. The values are
|
|
46
|
+
* key material and never belong in a log.
|
|
47
|
+
*
|
|
48
|
+
* @param {PublicKeyCredential} credential - Credential or assertion.
|
|
49
|
+
* @returns {string[]|null} Extension names, or null if unavailable.
|
|
50
|
+
*/
|
|
51
|
+
function extensionNames(credential) {
|
|
52
|
+
try {
|
|
53
|
+
const results = credential?.getClientExtensionResults?.();
|
|
54
|
+
return results ? Object.keys(results) : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Describe a WebAuthn response by shape alone.
|
|
62
|
+
*
|
|
63
|
+
* Every value here is a length, a boolean or an extension name. Nothing
|
|
64
|
+
* derived from credential bytes, and no extension values. Exported so the
|
|
65
|
+
* redaction can be asserted in tests rather than assumed.
|
|
66
|
+
*
|
|
67
|
+
* @param {PublicKeyCredential} credential - Credential or assertion.
|
|
68
|
+
* @returns {Object} Redacted description.
|
|
69
|
+
*/
|
|
70
|
+
export function describeWebAuthnResponse(credential) {
|
|
71
|
+
const response = credential?.response;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
type: credential?.type ?? null,
|
|
75
|
+
rawIdBytes: byteLength(credential?.rawId),
|
|
76
|
+
clientDataJSONBytes: byteLength(response?.clientDataJSON),
|
|
77
|
+
// Registration only.
|
|
78
|
+
attestationObjectBytes: byteLength(response?.attestationObject),
|
|
79
|
+
hasGetPublicKey: typeof response?.getPublicKey === 'function',
|
|
80
|
+
// Assertion only.
|
|
81
|
+
authenticatorDataBytes: byteLength(response?.authenticatorData),
|
|
82
|
+
signatureBytes: byteLength(response?.signature),
|
|
83
|
+
hasUserHandle: Boolean(response?.userHandle),
|
|
84
|
+
extensions: extensionNames(credential),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Log the shape of a WebAuthn response.
|
|
90
|
+
*
|
|
91
|
+
* Goes through the package debug logger, so it is off unless
|
|
92
|
+
* `DEBUG=orbitdb-identity-provider-webauthn-did*` is set, and it never writes
|
|
93
|
+
* credential contents.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} label - What produced the response.
|
|
96
|
+
* @param {PublicKeyCredential} credential - Credential or assertion.
|
|
97
|
+
*/
|
|
98
|
+
export function logWebAuthnResponse(label, credential) {
|
|
99
|
+
if (!log.enabled) return;
|
|
100
|
+
log('%s returned: %o', label, describeWebAuthnResponse(credential));
|
|
101
|
+
}
|
package/src/webauthn/provider.js
CHANGED
|
@@ -27,39 +27,10 @@ import {
|
|
|
27
27
|
buildAuthenticatorSelection,
|
|
28
28
|
buildCredentialRequestOptions,
|
|
29
29
|
} from './config.js';
|
|
30
|
+
import { logWebAuthnResponse } from './debug-log.js';
|
|
30
31
|
|
|
31
32
|
const webauthnLog = logger('orbitdb-identity-provider-webauthn-did:webauthn');
|
|
32
33
|
|
|
33
|
-
function logWebAuthnResponse(label, credential) {
|
|
34
|
-
const response = credential?.response;
|
|
35
|
-
const getPublicKeyResult =
|
|
36
|
-
typeof response?.getPublicKey === 'function'
|
|
37
|
-
? response.getPublicKey()
|
|
38
|
-
: null;
|
|
39
|
-
|
|
40
|
-
console.group(`[WebAuthn Debug] ${label}`);
|
|
41
|
-
console.log('credential', credential);
|
|
42
|
-
console.log('credential.id', credential?.id);
|
|
43
|
-
console.log('credential.type', credential?.type);
|
|
44
|
-
console.log('credential.rawId', credential?.rawId);
|
|
45
|
-
console.log('credential.response', response);
|
|
46
|
-
console.log('response.clientDataJSON', response?.clientDataJSON);
|
|
47
|
-
console.log('response.attestationObject', response?.attestationObject);
|
|
48
|
-
console.log('response.authenticatorData', response?.authenticatorData);
|
|
49
|
-
console.log('response.signature', response?.signature);
|
|
50
|
-
console.log('response.userHandle', response?.userHandle);
|
|
51
|
-
console.log(
|
|
52
|
-
'response.getPublicKey exists',
|
|
53
|
-
typeof response?.getPublicKey === 'function'
|
|
54
|
-
);
|
|
55
|
-
console.log('response.getPublicKey()', getPublicKeyResult);
|
|
56
|
-
console.log(
|
|
57
|
-
'client extension results',
|
|
58
|
-
credential?.getClientExtensionResults?.() || null
|
|
59
|
-
);
|
|
60
|
-
console.groupEnd();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
34
|
/**
|
|
64
35
|
* WebAuthn DID Provider Core Implementation
|
|
65
36
|
*/
|
|
@@ -199,6 +170,19 @@ export class WebAuthnDIDProvider {
|
|
|
199
170
|
// Note: largeBlob write happens after credential creation
|
|
200
171
|
}
|
|
201
172
|
|
|
173
|
+
// Request PRF even when the keystore is not encrypted. The stored input is
|
|
174
|
+
// what lets the OrbitDB signing key be derived reproducibly later, so a
|
|
175
|
+
// credential registered without it can never get a reproducible identity.
|
|
176
|
+
// Requesting the extension is harmless where it is unsupported: the
|
|
177
|
+
// authenticator simply returns no PRF results.
|
|
178
|
+
if (!prfInput) {
|
|
179
|
+
const prfConfig = KeystoreEncryption.addPRFToCredentialOptions(
|
|
180
|
+
credentialOptions.publicKey
|
|
181
|
+
);
|
|
182
|
+
credentialOptions.publicKey = prfConfig.credentialOptions;
|
|
183
|
+
prfInput = prfConfig.prfInput;
|
|
184
|
+
}
|
|
185
|
+
|
|
202
186
|
try {
|
|
203
187
|
const credential = await navigator.credentials.create(credentialOptions);
|
|
204
188
|
|