@le-space/orbitdb-identity-provider-webauthn-did 0.10.0 → 0.11.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 +19 -0
- package/README.md +24 -0
- package/package.json +3 -2
- package/src/index.js +6 -0
- package/src/keystore/index.js +4 -0
- package/src/keystore/stale-identity-cache.js +97 -0
- package/types/index.d.ts +23 -0
- package/types/keystore.d.ts +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
Another session of the same passkey may still write: OrbitDB's verified-identity cache, worked
|
|
6
|
+
around in the provider so that no application carries a copy.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`withoutStaleIdentityCache(identities)`** and **`createIdentities(options)`** (main entry and
|
|
11
|
+
`./keystore`): `@orbitdb/core` 4 caches a verified identity under `signatures.id` and, on a hit,
|
|
12
|
+
compares the whole identity. This provider keeps the id signature the same in every session of a
|
|
13
|
+
passkey (the PRF-derived signing key) while the WebAuthn proof in `signatures.publicKey` is new per
|
|
14
|
+
session — so whichever session's entries are verified first owns the cache, and every other
|
|
15
|
+
session of the same passkey is refused: _"Key … is not allowed to write to the log"_. Seen in
|
|
16
|
+
belege's restore (belege#259) and in simple-todo's archived01, a phone restoring a laptop's
|
|
17
|
+
archive (simple-todo#79). The wrapper re-checks a refused webauthn identity the way an uncached
|
|
18
|
+
one would be: `isIdentity`, the id signature over the public key, this provider's binding. Other
|
|
19
|
+
providers' identities and anything failing those checks stay refused. Upstream
|
|
20
|
+
orbitdb/orbitdb#1258; to be removed once OrbitDB keys its cache by identity hash (#90).
|
|
21
|
+
|
|
3
22
|
## 0.10.0
|
|
4
23
|
|
|
5
24
|
An identity that is nobody's passkey, so that every security key of an owner can write as the same
|
package/README.md
CHANGED
|
@@ -98,6 +98,30 @@ const identity = await identities.createIdentity({
|
|
|
98
98
|
});
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Identities for a passkey: `createIdentities`
|
|
102
|
+
|
|
103
|
+
OrbitDB keeps a cache of verified identities keyed by `signatures.id`. This provider keeps that
|
|
104
|
+
signature the same in every session of a passkey (the signing key is derived from the PRF output),
|
|
105
|
+
while the WebAuthn proof in `signatures.publicKey` is a fresh assertion per session. So whichever
|
|
106
|
+
session's entries a peer verifies first owns its cache, and every other session of the same passkey
|
|
107
|
+
is refused: _"Key … is not allowed to write to the log"_ — a phone restoring a laptop's backup, a
|
|
108
|
+
browser that gets its own earlier entries back from a relay ([orbitdb/orbitdb#1258](https://github.com/orbitdb/orbitdb/issues/1258),
|
|
109
|
+
[#90](https://github.com/Le-Space/orbitdb-identity-provider-webauthn-did/issues/90)).
|
|
110
|
+
|
|
111
|
+
Until OrbitDB keys its cache by identity hash, build the identities with the provider's wrapper:
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
import { createIdentities } from '@le-space/orbitdb-identity-provider-webauthn-did';
|
|
115
|
+
|
|
116
|
+
const identities = await createIdentities({ ipfs, keystore }); // Identities(options), wrapped
|
|
117
|
+
const orbitdb = await createOrbitDB({ ipfs, identities, identity });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
or wrap an instance you already have with `withoutStaleIdentityCache(identities)`. Where OrbitDB
|
|
121
|
+
says no, a webauthn identity is checked again the way an uncached one would be — the shape, the id
|
|
122
|
+
signature over the public key, and this provider's binding of the key to the DID. Other providers'
|
|
123
|
+
identities, and anything that fails those checks, stay refused.
|
|
124
|
+
|
|
101
125
|
### Discoverable Recovery Notes
|
|
102
126
|
|
|
103
127
|
For the DID-based flow:
|
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.11.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",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"version": "git add -A",
|
|
53
53
|
"postversion": "git push && git push --tags",
|
|
54
54
|
"test:two-peer": "playwright test tests/webauthn-two-peer-replication.test.js --project=chromium --reporter=line",
|
|
55
|
-
"test:node": "playwright test --config=playwright.node.config.js"
|
|
55
|
+
"test:node": "playwright test --config=playwright.node.config.js",
|
|
56
|
+
"test:wallet-fork": "PASSKEY_WALLET_FORK_URL=${PASSKEY_WALLET_FORK_URL:-https://sepolia.drpc.org} playwright test --config=playwright.fork.config.js"
|
|
56
57
|
},
|
|
57
58
|
"keywords": [
|
|
58
59
|
"orbitdb",
|
package/src/index.js
CHANGED
|
@@ -308,6 +308,12 @@ export {
|
|
|
308
308
|
recoverPublicKeyCandidates,
|
|
309
309
|
} from './webauthn/recovery.js';
|
|
310
310
|
export { restoreIdentityFromAuthenticator } from './webauthn/restore.js';
|
|
311
|
+
// OrbitDB's identity cache refuses another session of the same passkey
|
|
312
|
+
// (orbitdb/orbitdb#1258); this is the provider's answer until it is fixed there.
|
|
313
|
+
export {
|
|
314
|
+
withoutStaleIdentityCache,
|
|
315
|
+
createIdentities,
|
|
316
|
+
} from './keystore/stale-identity-cache.js';
|
|
311
317
|
// The two halves of it, for a caller that already holds one: a PRF output, or
|
|
312
318
|
// a credential it wants the output for.
|
|
313
319
|
export {
|
package/src/keystore/index.js
CHANGED
|
@@ -6,4 +6,8 @@ export { default } from './encryption.js';
|
|
|
6
6
|
export * from './provider.js';
|
|
7
7
|
export * from './session-keystore.js';
|
|
8
8
|
export { createSecretSigner } from './secret-signer.js';
|
|
9
|
+
export {
|
|
10
|
+
withoutStaleIdentityCache,
|
|
11
|
+
createIdentities,
|
|
12
|
+
} from './stale-identity-cache.js';
|
|
9
13
|
export { PrfUnavailableError } from '../errors.js';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrbitDB's verified-identity cache keyed by the wrong thing, worked around.
|
|
3
|
+
*
|
|
4
|
+
* `Identities.verifyIdentity` (@orbitdb/core 4) remembers a verified identity
|
|
5
|
+
* under `signatures.id` and, on a hit, answers `isEqual(identity, cached)`.
|
|
6
|
+
* With this provider the PRF-derived signing key gives the *same*
|
|
7
|
+
* `signatures.id` in every session of a passkey, while `signatures.publicKey`
|
|
8
|
+
* — the WebAuthn assertion that binds the key to the DID — is new per session.
|
|
9
|
+
* So once one session's identity is in the cache, every other session of the
|
|
10
|
+
* same passkey is refused: "Could not append entry: Key … is not allowed to
|
|
11
|
+
* write to the log". Whichever session's entries are verified first owns the
|
|
12
|
+
* cache: after a join (a restore from backup), after a sync (a peer that held
|
|
13
|
+
* the list), or simply the browser's own earlier entries coming back.
|
|
14
|
+
*
|
|
15
|
+
* Seen in Le-Space/belege's restore (belege#259) and in simple-todo's
|
|
16
|
+
* archived01 (a phone restoring a laptop's archive, Le-Space/simple-todo#79).
|
|
17
|
+
* Upstream: orbitdb/orbitdb#1258. The right fix is a cache keyed by identity
|
|
18
|
+
* hash; until OrbitDB has it, this is the provider's own answer, so that no
|
|
19
|
+
* application has to carry a copy.
|
|
20
|
+
*
|
|
21
|
+
* Where OrbitDB says no, an identity of this provider is checked again the way
|
|
22
|
+
* an uncached one would be: the shape (`isIdentity`), the id signature over
|
|
23
|
+
* the public key (`identities.verify`), and this provider's binding of the key
|
|
24
|
+
* to the DID (`verifyIdentity`, the check GHSA-326j-4cc3-4rrg added). Other
|
|
25
|
+
* providers' identities, and anything that fails those checks, stay refused.
|
|
26
|
+
*/
|
|
27
|
+
import { OrbitDBWebAuthnIdentityProviderFunction } from './provider.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* OrbitDB's own shape check (`identities/identity.js`, `isIdentity`), kept
|
|
31
|
+
* here rather than imported: this file is reachable from the `./keystore`
|
|
32
|
+
* entry, which does not load `@orbitdb/core` at import time — the peer
|
|
33
|
+
* dependency is the application's, and @orbitdb/core 4.0.0 cannot even be
|
|
34
|
+
* imported from a bare install (it uses `it-drain` without declaring it).
|
|
35
|
+
*
|
|
36
|
+
* @param {any} identity
|
|
37
|
+
*/
|
|
38
|
+
function isIdentity(identity) {
|
|
39
|
+
return Boolean(
|
|
40
|
+
identity &&
|
|
41
|
+
identity.id &&
|
|
42
|
+
identity.hash &&
|
|
43
|
+
identity.bytes &&
|
|
44
|
+
identity.publicKey &&
|
|
45
|
+
identity.signatures &&
|
|
46
|
+
identity.signatures.id &&
|
|
47
|
+
identity.signatures.publicKey &&
|
|
48
|
+
identity.type
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wrap an `Identities` instance so that another session of the same passkey
|
|
54
|
+
* is verified rather than refused. Idempotent in effect; wrapping twice only
|
|
55
|
+
* checks twice.
|
|
56
|
+
*
|
|
57
|
+
* @template {{ verifyIdentity: (identity: any) => Promise<boolean>, verify: (signature: string, publicKey: string, data: string) => Promise<boolean> }} T
|
|
58
|
+
* @param {T} identities - from `Identities(...)` (or `createIdentities` below)
|
|
59
|
+
* @returns {T} the same instance, its `verifyIdentity` replaced
|
|
60
|
+
*/
|
|
61
|
+
export function withoutStaleIdentityCache(identities) {
|
|
62
|
+
if (
|
|
63
|
+
!identities ||
|
|
64
|
+
typeof identities.verifyIdentity !== 'function' ||
|
|
65
|
+
typeof identities.verify !== 'function'
|
|
66
|
+
) {
|
|
67
|
+
throw new TypeError(
|
|
68
|
+
'withoutStaleIdentityCache needs an OrbitDB Identities instance (verifyIdentity, verify)'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const cached = identities.verifyIdentity;
|
|
72
|
+
identities.verifyIdentity = async (identity) => {
|
|
73
|
+
if (await cached(identity)) return true;
|
|
74
|
+
if (
|
|
75
|
+
!isIdentity(identity) ||
|
|
76
|
+
identity.type !== OrbitDBWebAuthnIdentityProviderFunction.type
|
|
77
|
+
) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const { id, publicKey, signatures } = identity;
|
|
81
|
+
if (!(await identities.verify(signatures.id, publicKey, id))) return false;
|
|
82
|
+
return OrbitDBWebAuthnIdentityProviderFunction.verifyIdentity(identity);
|
|
83
|
+
};
|
|
84
|
+
return identities;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `Identities(options)` from @orbitdb/core, already wrapped: what an
|
|
89
|
+
* application that builds identities for a passkey should call. The peer
|
|
90
|
+
* dependency is loaded when this is called, not when the module is.
|
|
91
|
+
*
|
|
92
|
+
* @param {Record<string, unknown>} [options] - `{ ipfs, keystore, storage, path }`, as OrbitDB takes them
|
|
93
|
+
*/
|
|
94
|
+
export async function createIdentities(options) {
|
|
95
|
+
const { Identities } = await import('@orbitdb/core');
|
|
96
|
+
return withoutStaleIdentityCache(await Identities(options));
|
|
97
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -659,3 +659,26 @@ declare const defaultExport: {
|
|
|
659
659
|
};
|
|
660
660
|
|
|
661
661
|
export default defaultExport;
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* OrbitDB's verified-identity cache is keyed by `signatures.id`, which this
|
|
665
|
+
* provider keeps the same in every session of a passkey while the WebAuthn
|
|
666
|
+
* proof changes; another session of the same passkey is then refused
|
|
667
|
+
* (orbitdb/orbitdb#1258). Wraps an `Identities` instance so that such an
|
|
668
|
+
* identity is checked again the way an uncached one would be.
|
|
669
|
+
*/
|
|
670
|
+
export function withoutStaleIdentityCache<
|
|
671
|
+
T extends {
|
|
672
|
+
verifyIdentity: (identity: unknown) => Promise<boolean>;
|
|
673
|
+
verify: (
|
|
674
|
+
signature: string,
|
|
675
|
+
publicKey: string,
|
|
676
|
+
data: string
|
|
677
|
+
) => Promise<boolean>;
|
|
678
|
+
},
|
|
679
|
+
>(identities: T): T;
|
|
680
|
+
|
|
681
|
+
/** `Identities(options)` from `@orbitdb/core`, already wrapped by `withoutStaleIdentityCache`. */
|
|
682
|
+
export function createIdentities(
|
|
683
|
+
options?: Record<string, unknown>
|
|
684
|
+
): Promise<unknown>;
|
package/types/keystore.d.ts
CHANGED
|
@@ -206,3 +206,26 @@ declare const defaultExport: {
|
|
|
206
206
|
};
|
|
207
207
|
|
|
208
208
|
export default defaultExport;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* OrbitDB's verified-identity cache is keyed by `signatures.id`, which this
|
|
212
|
+
* provider keeps the same in every session of a passkey while the WebAuthn
|
|
213
|
+
* proof changes; another session of the same passkey is then refused
|
|
214
|
+
* (orbitdb/orbitdb#1258). Wraps an `Identities` instance so that such an
|
|
215
|
+
* identity is checked again the way an uncached one would be.
|
|
216
|
+
*/
|
|
217
|
+
export function withoutStaleIdentityCache<
|
|
218
|
+
T extends {
|
|
219
|
+
verifyIdentity: (identity: unknown) => Promise<boolean>;
|
|
220
|
+
verify: (
|
|
221
|
+
signature: string,
|
|
222
|
+
publicKey: string,
|
|
223
|
+
data: string
|
|
224
|
+
) => Promise<boolean>;
|
|
225
|
+
},
|
|
226
|
+
>(identities: T): T;
|
|
227
|
+
|
|
228
|
+
/** `Identities(options)` from `@orbitdb/core`, already wrapped by `withoutStaleIdentityCache`. */
|
|
229
|
+
export function createIdentities(
|
|
230
|
+
options?: Record<string, unknown>
|
|
231
|
+
): Promise<unknown>;
|