@le-space/orbitdb-identity-provider-webauthn-did 0.4.1 → 0.5.0
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 +100 -0
- package/README.md +57 -0
- package/package.json +5 -3
- package/src/keystore/provider.js +7 -11
- package/src/varsig/assertion.js +12 -34
- package/src/varsig/credential.js +1 -30
- package/src/varsig/utils.js +44 -11
- package/src/verification.js +12 -10
- package/src/webauthn/debug-log.js +101 -0
- package/src/webauthn/provider.js +1 -30
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,106 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.0
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
|
|
9
|
+
- The varsig signing context moves into the signed payload, which **changes the
|
|
10
|
+
bytes a credential signs**. Entries and identities produced by 0.4.x and
|
|
11
|
+
earlier no longer verify, and peers running different major versions cannot
|
|
12
|
+
replicate with each other — the whole set has to move together.
|
|
13
|
+
|
|
14
|
+
The challenge used to be `SHA-256(label ‖ payload)`. That separated the three
|
|
15
|
+
signing contexts, but it meant the challenge was no longer the hash of the
|
|
16
|
+
payload, and the varsig WebAuthn draft has a verifier re-hash the payload and
|
|
17
|
+
compare it against the challenge (step 5). Nothing outside this package could
|
|
18
|
+
reproduce that value.
|
|
19
|
+
|
|
20
|
+
The label itself is not the problem: one credential signs an identity, a public
|
|
21
|
+
key and an oplog entry, and a signature over one must not be replayable as a
|
|
22
|
+
signature over another. That separation cannot live in the varsig header
|
|
23
|
+
either, because a WebAuthn signature only covers
|
|
24
|
+
`authenticatorData ‖ SHA-256(clientDataJSON)` — anything carried beside the
|
|
25
|
+
challenge is unauthenticated. So the context moves into the signed bytes:
|
|
26
|
+
`bindContext()` frames it length-prefixed ahead of the payload, and the
|
|
27
|
+
challenge becomes a plain SHA-256 multihash of those bytes. Both properties
|
|
28
|
+
now hold — the three contexts still yield different challenges, and a spec
|
|
29
|
+
verifier can reproduce the digest.
|
|
30
|
+
|
|
31
|
+
The length prefix is load-bearing. Without it, context `a` with payload `bc`
|
|
32
|
+
and context `ab` with payload `c` are the same bytes, so a chosen payload
|
|
33
|
+
could absorb the label and sidestep the separation.
|
|
34
|
+
|
|
35
|
+
This is a bet on a draft: ChainAgnostic/varsig#11 is still open with
|
|
36
|
+
`webauthn-varsig-header = TODO`. If the draft settles differently, the wire
|
|
37
|
+
format changes again.
|
|
38
|
+
|
|
39
|
+
- `iso-webauthn-varsig` moves to 0.3.0, which corrects the Ed25519 curve code in
|
|
40
|
+
the varsig header from `0xed01` to `0xed`. `0xed01` is how the multicodec
|
|
41
|
+
`ed25519-pub` is written in prose — but that is its _varint encoding_, and
|
|
42
|
+
using it as the code meant emitting 60673, which no other implementation reads
|
|
43
|
+
as Ed25519. Ed25519 varsigs written by earlier versions do not decode.
|
|
44
|
+
**P-256 bytes are unchanged by this second break**, so P-256 deployments are
|
|
45
|
+
affected only by the challenge change above.
|
|
46
|
+
|
|
47
|
+
### Migration
|
|
48
|
+
|
|
49
|
+
Nothing carries over automatically. Re-register credentials and re-create any
|
|
50
|
+
database whose entries were signed under 0.4.x, and upgrade every peer —
|
|
51
|
+
including relays that verify identities — in one step rather than gradually.
|
|
52
|
+
|
|
53
|
+
## 0.4.2
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- Restore debug logging in the browser. `weald`, which `@libp2p/logger` uses,
|
|
58
|
+
does `import { ms as humanize } from 'ms'` — a named export that only exists
|
|
59
|
+
in `ms@3`+. All three demos pinned `ms` to `2.1.3`, which is CommonJS with no
|
|
60
|
+
named export, so the import resolved to `undefined` and the first debug call
|
|
61
|
+
threw `TypeError: (0, import_ms2.ms) is not a function`. Enabling `DEBUG` in a
|
|
62
|
+
browser therefore crashed credential creation outright. Dropping the pin lets
|
|
63
|
+
`weald` resolve the version it declares.
|
|
64
|
+
|
|
65
|
+
This is why `webauthn-logging-e2e` had been failing continuously: it is the
|
|
66
|
+
only suite that sets `localStorage.debug`, so it was the only one to reach the
|
|
67
|
+
broken path.
|
|
68
|
+
|
|
69
|
+
- Clear the production dependency advisories. `iso-web > iso-kv > conf > ajv`
|
|
70
|
+
carried four high `fast-uri` advisories and one moderate `ajv` one; the scoped
|
|
71
|
+
overrides `conf>ajv` and `ajv>fast-uri` resolve it. Scoped deliberately: a
|
|
72
|
+
blanket `ajv` override also hits ESLint, which needs ajv 6 and dies on the
|
|
73
|
+
removed `missingRefs` option.
|
|
74
|
+
`pnpm audit --prod` now reports nothing, so the enforced CI gate moves from
|
|
75
|
+
`critical` to `moderate`.
|
|
76
|
+
- Stop writing WebAuthn credentials to the console. `logWebAuthnResponse()` dumped
|
|
77
|
+
the whole credential — `rawId`, `attestationObject`, `clientDataJSON`,
|
|
78
|
+
`signature`, `getPublicKey()` and `getClientExtensionResults()` — on every
|
|
79
|
+
`navigator.credentials.create()` and `.get()`, unconditionally and in
|
|
80
|
+
production code paths. Two things made that more than noise: extension results
|
|
81
|
+
can carry the PRF output, which is what both the keystore encryption and (since
|
|
82
|
+
0.4.1) the OrbitDB signing key derive from, and `rawId` is a stable per-user,
|
|
83
|
+
per-RP identifier that landed in any console capture or session replay the
|
|
84
|
+
embedding app happened to run. Replaced by a single shared helper that logs
|
|
85
|
+
shape only — byte lengths, presence flags and the names of the extensions that
|
|
86
|
+
returned results — behind the package debug logger, so it is silent unless
|
|
87
|
+
`DEBUG=orbitdb-identity-provider-webauthn-did*` is set. Closes #22.
|
|
88
|
+
- Keep the demos' P2P component out of the server render. Turning on `ssr`
|
|
89
|
+
so prerendering emitted real content also meant `+page.svelte` was evaluated
|
|
90
|
+
in Node, and it imported the libp2p stack at module scope — reaching
|
|
91
|
+
`@libp2p/webrtc` and its native `node-datachannel` binding, which the demos
|
|
92
|
+
do not build. The dev server returned 500 for every request. The component is
|
|
93
|
+
loaded in the browser only now; the shared shell still renders server-side, so
|
|
94
|
+
prerendering keeps producing a real title and description.
|
|
95
|
+
|
|
96
|
+
### Changed
|
|
97
|
+
|
|
98
|
+
- Route the remaining informational `console.log` output through the debug
|
|
99
|
+
logger. A library should not print unconditionally; `console.error` and
|
|
100
|
+
`console.warn` stay as they are, so genuine failures remain visible. `src/`
|
|
101
|
+
went from 61 `console.*` calls to 16, none of them `console.log`.
|
|
102
|
+
- Deduplicate three byte-identical copies of the WebAuthn debug helper into
|
|
103
|
+
`src/webauthn/debug-log.js`.
|
|
104
|
+
|
|
5
105
|
## 0.4.1
|
|
6
106
|
|
|
7
107
|
### Added
|
package/README.md
CHANGED
|
@@ -305,6 +305,63 @@ Practical implication:
|
|
|
305
305
|
|
|
306
306
|
- 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.
|
|
307
307
|
|
|
308
|
+
## Upstream Packages and Temporary Forks
|
|
309
|
+
|
|
310
|
+
This package builds on the [`iso-repo`](https://github.com/hugomrdias/iso-repo)
|
|
311
|
+
toolkit by **[Hugo Dias](https://github.com/hugomrdias)** — `iso-base`,
|
|
312
|
+
`iso-did` and `iso-passkeys` do the WebAuthn parsing, DID encoding and base
|
|
313
|
+
conversion that this provider is built on top of. It is excellent work and this
|
|
314
|
+
package would be considerably larger without it. Thank you.
|
|
315
|
+
|
|
316
|
+
Four of the dependencies resolve to `@le-space` builds rather than the
|
|
317
|
+
published originals. Two different reasons, and they should not be confused:
|
|
318
|
+
|
|
319
|
+
### Temporary forks
|
|
320
|
+
|
|
321
|
+
Published only to unblock this package, and meant to disappear. Each carries a
|
|
322
|
+
small delta that belongs upstream.
|
|
323
|
+
|
|
324
|
+
| Dependency | Upstream | Why it is forked |
|
|
325
|
+
| -------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
326
|
+
| `iso-passkeys` | [iso-passkeys](https://github.com/hugomrdias/iso-repo/tree/main/packages/iso-passkeys) | Re-exports `parseAttestationObject` and `unwrapEC2Signature`, which already exist upstream but are not part of the public API |
|
|
327
|
+
| `iso-base` | [iso-base](https://github.com/hugomrdias/iso-repo/tree/main/packages/iso-base) | Kept in step with the forks above |
|
|
328
|
+
| `iso-did` | [iso-did](https://github.com/hugomrdias/iso-repo/tree/main/packages/iso-did) | Kept in step with the forks above |
|
|
329
|
+
|
|
330
|
+
The `iso-passkeys` delta is two lines and purely additive — nothing is changed
|
|
331
|
+
or removed, two existing internal functions are simply exported. It is proposed
|
|
332
|
+
upstream as
|
|
333
|
+
[hugomrdias/iso-repo#543](https://github.com/hugomrdias/iso-repo/pull/543); once
|
|
334
|
+
that lands, these three forks and their entries in `pnpm.overrides` can go. The
|
|
335
|
+
remaining entries in that block — `iso-web`, `conf>ajv`, `ajv>fast-uri` — are
|
|
336
|
+
unrelated and stay.
|
|
337
|
+
|
|
338
|
+
### Not a fork
|
|
339
|
+
|
|
340
|
+
`iso-webauthn-varsig` is a **new** package rather than a modified copy of an
|
|
341
|
+
existing one. It implements WebAuthn varsig signing for OrbitDB oplog entries
|
|
342
|
+
and has no upstream equivalent. It follows `iso-repo` conventions and lives in
|
|
343
|
+
the same lineage, which is why it is named the way it is.
|
|
344
|
+
|
|
345
|
+
It follows the non-recursive varsig layout, which shipped in 0.2.0.
|
|
346
|
+
|
|
347
|
+
One thing is still open, and it is on the spec side rather than here: the
|
|
348
|
+
WebAuthn varsig header. `webauthn-varsig-header` is `TODO` in
|
|
349
|
+
[ChainAgnostic/varsig#11](https://github.com/ChainAgnostic/varsig/pull/11), so
|
|
350
|
+
the `0x300001` marker this package writes is a private-use codepoint chosen
|
|
351
|
+
here rather than an allocated one. Expect the wire format to change once that
|
|
352
|
+
is settled.
|
|
353
|
+
|
|
354
|
+
### Not forked at all
|
|
355
|
+
|
|
356
|
+
`iso-web` appears in `pnpm.overrides` but resolves to the genuine upstream
|
|
357
|
+
package. The entry only pins a version.
|
|
358
|
+
|
|
359
|
+
### Licensing
|
|
360
|
+
|
|
361
|
+
All `iso-repo` packages are MIT, © Hugo Dias. The forks keep the original
|
|
362
|
+
`license` and `author` fields, so authorship travels with them; only the
|
|
363
|
+
package name changes.
|
|
364
|
+
|
|
308
365
|
## License
|
|
309
366
|
|
|
310
367
|
MIT. See `LICENSE`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/orbitdb-identity-provider-webauthn-did",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"iso-did": "npm:@le-space/iso-did@2.1.2",
|
|
91
91
|
"iso-passkeys": "npm:@le-space/iso-passkeys",
|
|
92
92
|
"iso-web": "^3.1.2",
|
|
93
|
-
"iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.
|
|
93
|
+
"iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.3.0",
|
|
94
94
|
"multiformats": "^14.0.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
@@ -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.
|
|
146
|
+
"iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.3.0",
|
|
147
|
+
"conf>ajv": "^8.18.0",
|
|
148
|
+
"ajv>fast-uri": "^3.1.4"
|
|
147
149
|
},
|
|
148
150
|
"onlyBuiltDependencies": [
|
|
149
151
|
"@ipshipyard/node-datachannel",
|
package/src/keystore/provider.js
CHANGED
|
@@ -77,10 +77,6 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
77
77
|
return IDENTITY_TYPES.WEBAUTHN;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
* Resolve the identity DID.
|
|
82
|
-
* @returns {Promise<string>} DID string.
|
|
83
|
-
*/
|
|
84
80
|
/**
|
|
85
81
|
* Resolve the identity DID.
|
|
86
82
|
*
|
|
@@ -352,11 +348,11 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
352
348
|
*/
|
|
353
349
|
async createEncryptedKeystore() {
|
|
354
350
|
if (!this.encryptKeystore) {
|
|
355
|
-
|
|
351
|
+
identityLog('ℹ️ Keystore encryption not enabled, skipping');
|
|
356
352
|
return;
|
|
357
353
|
}
|
|
358
354
|
|
|
359
|
-
|
|
355
|
+
identityLog(
|
|
360
356
|
'🔐 Creating encrypted keystore with method:',
|
|
361
357
|
this.keystoreEncryptionMethod
|
|
362
358
|
);
|
|
@@ -448,7 +444,7 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
448
444
|
}
|
|
449
445
|
|
|
450
446
|
// Store encrypted keystore
|
|
451
|
-
|
|
447
|
+
identityLog(
|
|
452
448
|
'💾 Storing encrypted keystore with credentialId:',
|
|
453
449
|
this.credential.credentialId?.substring(0, 16) + '...'
|
|
454
450
|
);
|
|
@@ -461,7 +457,7 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
461
457
|
publicKey: publicKeyBytes,
|
|
462
458
|
keyType,
|
|
463
459
|
};
|
|
464
|
-
|
|
460
|
+
identityLog('✅ Encrypted keystore stored successfully');
|
|
465
461
|
|
|
466
462
|
identityLog('Encrypted keystore created and stored successfully');
|
|
467
463
|
} catch (error) {
|
|
@@ -649,15 +645,15 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
649
645
|
// If encryption is enabled, create and unlock encrypted keystore
|
|
650
646
|
if (encryptKeystore && keystore) {
|
|
651
647
|
try {
|
|
652
|
-
|
|
648
|
+
identityLog(
|
|
653
649
|
'🔐 Creating encrypted keystore with',
|
|
654
650
|
keystoreEncryptionMethod,
|
|
655
651
|
'...'
|
|
656
652
|
);
|
|
657
653
|
await provider.createEncryptedKeystore();
|
|
658
|
-
|
|
654
|
+
identityLog('🔓 Unlocking encrypted keystore...');
|
|
659
655
|
await provider.unlockEncryptedKeystore();
|
|
660
|
-
|
|
656
|
+
identityLog('✅ Encrypted keystore created and unlocked successfully');
|
|
661
657
|
identityLog('Encrypted keystore created and unlocked');
|
|
662
658
|
} catch (error) {
|
|
663
659
|
// Log error visibly so users know encryption failed
|
package/src/varsig/assertion.js
CHANGED
|
@@ -12,47 +12,23 @@ import {
|
|
|
12
12
|
verifyP256Signature,
|
|
13
13
|
verifyWebAuthnAssertion,
|
|
14
14
|
} from 'iso-webauthn-varsig';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
bindContext,
|
|
17
|
+
buildChallengeBytes,
|
|
18
|
+
toArrayBuffer,
|
|
19
|
+
toBytes,
|
|
20
|
+
} from './utils.js';
|
|
16
21
|
import { buildCredentialRequestOptions } from '../webauthn/config.js';
|
|
17
22
|
import { KEY_TYPES, WEBAUTHN_CLIENT_DATA_TYPES } from '../constants.js';
|
|
18
23
|
import {
|
|
19
24
|
VarsigVerificationError,
|
|
20
25
|
WebAuthnAuthenticationError,
|
|
21
26
|
} from '../errors.js';
|
|
27
|
+
import { logWebAuthnResponse } from '../webauthn/debug-log.js';
|
|
22
28
|
|
|
23
29
|
const isTestMode = () =>
|
|
24
30
|
typeof window !== 'undefined' && window.__PLAYWRIGHT__ === true;
|
|
25
31
|
|
|
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
32
|
/**
|
|
57
33
|
* Run a WebAuthn assertion for a payload.
|
|
58
34
|
* @param {Object} credential - WebAuthn credential info.
|
|
@@ -73,7 +49,10 @@ async function runWebAuthnAssertionForPayload(
|
|
|
73
49
|
) {
|
|
74
50
|
const rpId = window.location.hostname;
|
|
75
51
|
const origin = window.location.origin;
|
|
76
|
-
|
|
52
|
+
// The context is framed into the signed bytes, not into the challenge
|
|
53
|
+
// preimage, so the challenge stays a plain hash of what was signed.
|
|
54
|
+
const signedBytes = bindContext(domainLabel, payloadBytes);
|
|
55
|
+
const challengeBytes = await buildChallengeBytes(signedBytes);
|
|
77
56
|
|
|
78
57
|
if (isTestMode()) {
|
|
79
58
|
return {
|
|
@@ -237,8 +216,7 @@ async function verifyVarsigForPayload(
|
|
|
237
216
|
}
|
|
238
217
|
const clientData = parseClientDataJSON(decoded.clientDataJSON);
|
|
239
218
|
const expectedChallenge = await buildChallengeBytes(
|
|
240
|
-
domainLabel,
|
|
241
|
-
payloadBytes
|
|
219
|
+
bindContext(domainLabel, payloadBytes)
|
|
242
220
|
);
|
|
243
221
|
const expectedChallengeEncoded = bytesToBase64url(expectedChallenge);
|
|
244
222
|
|
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/varsig/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Byte and challenge helpers for varsig signing.
|
|
3
3
|
*/
|
|
4
|
-
import { concat } from 'iso-webauthn-varsig';
|
|
4
|
+
import { concat, varintEncode } from 'iso-webauthn-varsig';
|
|
5
5
|
|
|
6
6
|
const encoder = new TextEncoder();
|
|
7
7
|
|
|
@@ -27,18 +27,51 @@ function toBytes(data) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Frame a payload with the context it is signed for.
|
|
31
|
+
*
|
|
32
|
+
* The same credential signs three different things — an identity, a public key
|
|
33
|
+
* and an oplog entry — and a signature over one must not be reusable as a
|
|
34
|
+
* signature over another. The separation has to live inside the bytes that get
|
|
35
|
+
* hashed: the WebAuthn signature covers `authenticatorData ‖ SHA-256(clientDataJSON)`
|
|
36
|
+
* and nothing else, so a context field carried alongside the varsig would not
|
|
37
|
+
* be bound to the signature at all.
|
|
38
|
+
*
|
|
39
|
+
* It used to be mixed into the challenge preimage instead
|
|
40
|
+
* (`SHA-256(label ‖ payload)`), which separated the domains but meant the
|
|
41
|
+
* challenge was no longer the hash of the payload — so verifiers following the
|
|
42
|
+
* varsig spec could never reproduce it.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} context - Domain label, e.g. 'orbitdb-entry:'.
|
|
32
45
|
* @param {Uint8Array} payloadBytes - Payload bytes.
|
|
33
|
-
* @returns {
|
|
46
|
+
* @returns {Uint8Array} The bytes that are signed over.
|
|
34
47
|
*/
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
function bindContext(context, payloadBytes) {
|
|
49
|
+
const contextBytes = encoder.encode(context);
|
|
50
|
+
// Length-prefixed so the frame is unambiguous: without it, a context of
|
|
51
|
+
// 'a' with payload 'bc' and a context of 'ab' with payload 'c' would
|
|
52
|
+
// produce identical bytes.
|
|
53
|
+
return concat([
|
|
54
|
+
varintEncode(contextBytes.length),
|
|
55
|
+
contextBytes,
|
|
56
|
+
payloadBytes,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a WebAuthn challenge as a multihash of the payload.
|
|
62
|
+
*
|
|
63
|
+
* Per the varsig WebAuthn draft the challenge carries the payload hash as a
|
|
64
|
+
* multihash, so a verifier can re-hash the payload and compare. `0x12 0x20` is
|
|
65
|
+
* the SHA-256 prefix and its 32-byte length.
|
|
66
|
+
*
|
|
67
|
+
* @param {Uint8Array} payloadBytes - The bytes being signed, context included.
|
|
68
|
+
* @returns {Promise<Uint8Array>} Multihash of the payload.
|
|
69
|
+
*/
|
|
70
|
+
async function buildChallengeBytes(payloadBytes) {
|
|
71
|
+
const hash = new Uint8Array(
|
|
72
|
+
await crypto.subtle.digest('SHA-256', payloadBytes)
|
|
40
73
|
);
|
|
41
|
-
return new Uint8Array(hash);
|
|
74
|
+
return concat([new Uint8Array([0x12, 0x20]), hash]);
|
|
42
75
|
}
|
|
43
76
|
|
|
44
|
-
export { encoder, toArrayBuffer, toBytes, buildChallengeBytes };
|
|
77
|
+
export { encoder, toArrayBuffer, toBytes, bindContext, buildChallengeBytes };
|
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
|
*/
|