@le-space/orbitdb-identity-provider-webauthn-did 0.9.1 → 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 +49 -0
- package/README.md +27 -1
- package/docs/API.md +62 -2
- package/package.json +3 -2
- package/src/index.js +9 -0
- package/src/keystore/ed25519-did.js +26 -0
- package/src/keystore/index.js +5 -0
- package/src/keystore/provider.js +11 -2
- package/src/keystore/secret-signer.js +64 -0
- package/src/keystore/stale-identity-cache.js +97 -0
- package/src/standalone/index.js +1 -0
- package/src/standalone/worker/client.js +6 -26
- package/src/varsig/credential.js +27 -9
- package/src/webauthn/config.js +8 -10
- package/types/index.d.ts +39 -0
- package/types/keystore.d.ts +29 -0
- package/types/standalone.d.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
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
|
+
|
|
22
|
+
## 0.10.0
|
|
23
|
+
|
|
24
|
+
An identity that is nobody's passkey, so that every security key of an owner can write as the same
|
|
25
|
+
one; and the varsig registration's half of 0.9.1's fix, which #83 had entered under 0.9.1 after
|
|
26
|
+
0.9.1 was already published from #81.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`createSecretSigner(secret, { info })`**: an Ed25519 signer derived from at least 32 bytes of
|
|
31
|
+
secret with HKDF-SHA-256 under `info`, its DID the key's `did:key`. Kept in a vault
|
|
32
|
+
(`createVault`), the secret gives every key that opens a slot the same identity — the root of a
|
|
33
|
+
database's `OrbitDBAccessController`, which OrbitDB fixes at creation and a passkey identity
|
|
34
|
+
would hold for good. In `./standalone` and `./keystore` as well.
|
|
35
|
+
- **`OrbitDBWebAuthnIdentityProviderFunction({ signer })` needs no `webauthnCredential`.** A signer
|
|
36
|
+
is the whole identity, and no passkey stands behind it. Without either, the provider refuses
|
|
37
|
+
instead of failing on the missing credential.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- The varsig registration — `WebAuthnVarsigProvider.createCredential`, `createWebAuthnSigner` and
|
|
42
|
+
the signers built on it, and `createWebAuthnEd25519Credential` — honours `authenticatorType` too,
|
|
43
|
+
which it dropped the same way (#83). It still requires user verification: its code asked for
|
|
44
|
+
`'preferred'`, which never reached the browser either, and a varsig signature without the UV flag
|
|
45
|
+
is refused when it is made and when it is verified.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- `createEd25519DidFromPublicKey` lives in `src/keystore/` and is re-exported from where it was, so
|
|
50
|
+
the main entry can build a `did:key` without importing the worker client.
|
|
51
|
+
|
|
3
52
|
## 0.9.1
|
|
4
53
|
|
|
5
54
|
`createCredential` honours the choice of authenticator that its options, its types and
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ This package provides:
|
|
|
19
19
|
- Authentication/assertion requests omit `allowCredentials` by default, so the browser/authenticator can resolve the credential discoverably.
|
|
20
20
|
- You can switch this centrally with `configureWebAuthn({ discoverableCredentials: true|false })`.
|
|
21
21
|
- Registration is still the point where this package extracts the credential public key from attestation.
|
|
22
|
-
- Registration offers every kind of authenticator — the device's own, a security key such as a YubiKey, a phone — unless `
|
|
22
|
+
- Registration offers every kind of authenticator — the device's own, a security key such as a YubiKey, a phone — unless `authenticatorType: 'platform' | 'cross-platform'` narrows it, in `createCredential` as in the varsig registration and the standalone signers (honoured since 0.9.1). It always requires user verification.
|
|
23
23
|
- Later `navigator.credentials.get()` assertions do not reliably return the public key again, so identity reconstruction still needs metadata from somewhere else.
|
|
24
24
|
|
|
25
25
|
Since **0.6.0** the passkey alone is enough, and stored metadata is a convenience rather than a requirement:
|
|
@@ -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:
|
|
@@ -228,6 +252,8 @@ const withSecondKey = await addSlot(vault, vaultKey, secondKeysSlot);
|
|
|
228
252
|
|
|
229
253
|
How it works, and what a vault does not do: [docs/API.md](docs/API.md#prf-keys-and-vaults).
|
|
230
254
|
|
|
255
|
+
Since **0.10.0**, a secret kept in that vault can be an identity of its own: `createSecretSigner(secret, { info })` turns it into an Ed25519 signer for the provider's `signer` option, so every key that opens the vault writes as the same identity — the root of a database's access controller, which a single passkey's identity would hold for good. See [docs/API.md](docs/API.md#an-identity-that-is-nobodys-passkey-createsecretsignersecret--info-).
|
|
256
|
+
|
|
231
257
|
### Keystore-based DID (WebAuthn + OrbitDB keystore)
|
|
232
258
|
|
|
233
259
|
```mermaid
|
package/docs/API.md
CHANGED
|
@@ -386,6 +386,50 @@ What a vault does not do:
|
|
|
386
386
|
`tests/vault-two-authenticators.test.js` runs this in Chromium with two virtual
|
|
387
387
|
authenticators, each with its own PRF secret as two YubiKeys have.
|
|
388
388
|
|
|
389
|
+
#### An identity that is nobody's passkey: `createSecretSigner(secret, { info })`
|
|
390
|
+
|
|
391
|
+
Since 0.10.0. A database whose access controller is rooted at one passkey's
|
|
392
|
+
identity belongs to that passkey for good: OrbitDB fixes who may grant write
|
|
393
|
+
access when a database is created, and a second security key is a second
|
|
394
|
+
identity. Give the database an identity of its own instead, and let every key
|
|
395
|
+
act as it: keep a random secret in the vault, and turn it into a signer.
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
// `secret`: 32 random bytes the application keeps in its vault's payload,
|
|
399
|
+
// so that any slot — any of the owner's keys — gets them back.
|
|
400
|
+
const books = await createSecretSigner(secret, {
|
|
401
|
+
info: 'invoice/books-identity/v1',
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const keystore = await createSessionKeystore({ signer: books });
|
|
405
|
+
const identities = await Identities({ ipfs, keystore });
|
|
406
|
+
const identity = await identities.createIdentity({
|
|
407
|
+
provider: OrbitDBWebAuthnIdentityProviderFunction({ signer: books }),
|
|
408
|
+
});
|
|
409
|
+
const orbitdb = await createOrbitDB({ ipfs, identities, identity });
|
|
410
|
+
const db = await orbitdb.open('books', {
|
|
411
|
+
AccessController: OrbitDBAccessController({ write: [books.did] }),
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
- The key is Ed25519, derived with HKDF-SHA-256 from at least 32 bytes of
|
|
416
|
+
secret under `info`, and the DID is its `did:key`. The same secret and
|
|
417
|
+
`info` give the same key, DID and identity document on every device and
|
|
418
|
+
through every slot; another `info` gives another identity. Version `info`.
|
|
419
|
+
- What comes back is `{ did, type: 'Ed25519', publicKey, sign(data) }`, the
|
|
420
|
+
shape the `signer` option takes. The private key stays inside `sign`.
|
|
421
|
+
- With `signer`, `OrbitDBWebAuthnIdentityProviderFunction` needs no
|
|
422
|
+
`webauthnCredential`: no passkey stands behind this identity, and nothing
|
|
423
|
+
asks one. Peers verify it as any signer identity — the DID's key is the
|
|
424
|
+
identity's public key.
|
|
425
|
+
- People who should write without holding the secret are granted access under
|
|
426
|
+
their own identities (`db.access.grant('write', did)`), which can be revoked.
|
|
427
|
+
The secret's identity, as the root, cannot; against a stolen key, make a new
|
|
428
|
+
vault with a new secret and move the data.
|
|
429
|
+
|
|
430
|
+
`tests/secret-signer.test.js` writes through two keys as the same root and has
|
|
431
|
+
an identity from another secret refused.
|
|
432
|
+
|
|
389
433
|
### largeBlob Metadata Helpers
|
|
390
434
|
|
|
391
435
|
These helpers encode and recover identity metadata used by the demo recovery
|
|
@@ -502,6 +546,17 @@ const credential = await WebAuthnVarsigProvider.createCredential({
|
|
|
502
546
|
const identity = await createWebAuthnVarsigIdentity({ credential });
|
|
503
547
|
```
|
|
504
548
|
|
|
549
|
+
`WebAuthnVarsigProvider.createCredential(options)` takes `userId`,
|
|
550
|
+
`displayName`, `domain`, `discoverableCredentials`, `forceP256` (offer only
|
|
551
|
+
P-256), and `authenticatorType` / `authenticatorAttachment` as
|
|
552
|
+
`WebAuthnDIDProvider.createCredential` does. Before 0.9.1 it dropped the choice
|
|
553
|
+
of authenticator the same way and offered every kind.
|
|
554
|
+
|
|
555
|
+
It always requires user verification, whatever the options say. A varsig
|
|
556
|
+
signature without the UV flag is refused, when it is made and when it is
|
|
557
|
+
verified, so a credential registered without user verification could never
|
|
558
|
+
sign.
|
|
559
|
+
|
|
505
560
|
## Standalone Export
|
|
506
561
|
|
|
507
562
|
Use the standalone export when you want WebAuthn signing and worker-keystore
|
|
@@ -525,6 +580,10 @@ import {
|
|
|
525
580
|
- `createWebAuthnEd25519Credential(userId, displayName, options?)`
|
|
526
581
|
- `checkEd25519Support()`
|
|
527
582
|
|
|
583
|
+
The `create…Signer` functions register as
|
|
584
|
+
`WebAuthnVarsigProvider.createCredential` does and take its options;
|
|
585
|
+
`createWebAuthnEd25519Credential` passes on `options.authenticatorType` alone.
|
|
586
|
+
|
|
528
587
|
```js
|
|
529
588
|
const signer = await createWebAuthnSigner({
|
|
530
589
|
userId: 'alice@example.com',
|
|
@@ -562,8 +621,9 @@ const ucanSigner = signer.toUcantoSigner({
|
|
|
562
621
|
|
|
563
622
|
The same functions as the root export, without OrbitDB: `readPrfOutput`,
|
|
564
623
|
`deriveSubkey`, `deriveAesKey`, `createVault`, `openVault`, `addSlot`,
|
|
565
|
-
`removeSlot`, `replacePayload`, `slotIdFor`, `
|
|
566
|
-
`VaultError`. See
|
|
624
|
+
`removeSlot`, `replacePayload`, `slotIdFor`, `createSecretSigner`,
|
|
625
|
+
`PrfUnavailableError` and `VaultError`. See
|
|
626
|
+
[PRF Keys and Vaults](#prf-keys-and-vaults).
|
|
567
627
|
|
|
568
628
|
### P-256 Wallet Primitives
|
|
569
629
|
|
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 {
|
|
@@ -329,6 +335,9 @@ export {
|
|
|
329
335
|
replacePayload,
|
|
330
336
|
slotIdFor,
|
|
331
337
|
} from './keystore/vault.js';
|
|
338
|
+
// An identity that is nobody's passkey — the books every key of their owner
|
|
339
|
+
// writes as, its secret kept in a vault.
|
|
340
|
+
export { createSecretSigner } from './keystore/secret-signer.js';
|
|
332
341
|
|
|
333
342
|
export default {
|
|
334
343
|
WebAuthnDIDProvider,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { varint } from 'multiformats';
|
|
2
|
+
import { base58btc } from 'multiformats/bases/base58';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a did:key identifier from raw Ed25519 public key bytes.
|
|
6
|
+
* @param {Uint8Array} publicKeyBytes
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function createEd25519DidFromPublicKey(publicKeyBytes) {
|
|
10
|
+
if (!(publicKeyBytes instanceof Uint8Array) || publicKeyBytes.length !== 32) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Invalid Ed25519 public key length: ${publicKeyBytes?.length || 0}`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ED25519_MULTICODEC = 0xed;
|
|
17
|
+
const codecLength = varint.encodingLength(ED25519_MULTICODEC);
|
|
18
|
+
const codecBytes = new Uint8Array(codecLength);
|
|
19
|
+
varint.encodeTo(ED25519_MULTICODEC, codecBytes, 0);
|
|
20
|
+
|
|
21
|
+
const multikey = new Uint8Array(codecBytes.length + publicKeyBytes.length);
|
|
22
|
+
multikey.set(codecBytes, 0);
|
|
23
|
+
multikey.set(publicKeyBytes, codecBytes.length);
|
|
24
|
+
|
|
25
|
+
return `did:key:${base58btc.encode(multikey)}`;
|
|
26
|
+
}
|
package/src/keystore/index.js
CHANGED
|
@@ -5,4 +5,9 @@ export * from './encryption.js';
|
|
|
5
5
|
export { default } from './encryption.js';
|
|
6
6
|
export * from './provider.js';
|
|
7
7
|
export * from './session-keystore.js';
|
|
8
|
+
export { createSecretSigner } from './secret-signer.js';
|
|
9
|
+
export {
|
|
10
|
+
withoutStaleIdentityCache,
|
|
11
|
+
createIdentities,
|
|
12
|
+
} from './stale-identity-cache.js';
|
|
8
13
|
export { PrfUnavailableError } from '../errors.js';
|
package/src/keystore/provider.js
CHANGED
|
@@ -46,7 +46,9 @@ function normalizeByteArray(value) {
|
|
|
46
46
|
export class OrbitDBWebAuthnIdentityProvider {
|
|
47
47
|
/**
|
|
48
48
|
* @param {Object} options - Provider configuration.
|
|
49
|
-
* @param {Object} options.webauthnCredential - WebAuthn credential info.
|
|
49
|
+
* @param {Object} [options.webauthnCredential] - WebAuthn credential info.
|
|
50
|
+
* Required unless `signer` is given: a signer is the whole identity, and
|
|
51
|
+
* no passkey stands behind it (`createSecretSigner`).
|
|
50
52
|
* @param {boolean} [options.useKeystoreDID=false] - Use keystore DID instead of WebAuthn DID.
|
|
51
53
|
* @param {Object|null} [options.keystore=null] - OrbitDB keystore instance.
|
|
52
54
|
* @param {string} [options.keystoreKeyType='secp256k1'] - Keystore key type.
|
|
@@ -82,6 +84,11 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
82
84
|
prfOutput = null,
|
|
83
85
|
}) {
|
|
84
86
|
if (signer) assertSigner(signer);
|
|
87
|
+
if (!signer && !webauthnCredential) {
|
|
88
|
+
throw new TypeError(
|
|
89
|
+
'the provider needs a webauthnCredential, or a signer that is the identity'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
85
92
|
if (
|
|
86
93
|
prfOutput != null &&
|
|
87
94
|
!(prfOutput instanceof Uint8Array && prfOutput.length >= 32)
|
|
@@ -104,7 +111,9 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
104
111
|
this.deriveSigningKeyFromPrf = deriveSigningKeyFromPrf;
|
|
105
112
|
// Held only until the signing key is in the keystore; see getId().
|
|
106
113
|
this.prfOutput = prfOutput;
|
|
107
|
-
this.webauthnProvider =
|
|
114
|
+
this.webauthnProvider = webauthnCredential
|
|
115
|
+
? new WebAuthnDIDProvider(webauthnCredential)
|
|
116
|
+
: null;
|
|
108
117
|
this.type = IDENTITY_TYPES.WEBAUTHN; // Set instance property
|
|
109
118
|
this.useKeystoreDID = useKeystoreDID; // Flag to use Ed25519 DID from keystore
|
|
110
119
|
this.keystore = keystore; // OrbitDB keystore instance
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module SecretSigner
|
|
3
|
+
* @description
|
|
4
|
+
* An identity that is nobody's passkey: the books of an application, which
|
|
5
|
+
* every registered security key of their owner may write to.
|
|
6
|
+
*
|
|
7
|
+
* A passkey identity belongs to one authenticator — its DID is the
|
|
8
|
+
* credential's key, its signing key comes from that authenticator's PRF
|
|
9
|
+
* output — so a second security key is a second identity. OrbitDB's access
|
|
10
|
+
* controllers fix who may grant write access when a database is created, so
|
|
11
|
+
* a database rooted at one key's identity stays that key's forever. The books
|
|
12
|
+
* get an identity of their own instead, and every key acts as it:
|
|
13
|
+
* the application keeps a random secret in a vault (`createVault`), any slot
|
|
14
|
+
* opens it, and this turns the secret into the books' signing key. Other
|
|
15
|
+
* people are not given the secret; they are granted write access under their
|
|
16
|
+
* own identities, which can be revoked.
|
|
17
|
+
*
|
|
18
|
+
* The key is Ed25519, derived with HKDF-SHA-256 under `info`; its DID is the
|
|
19
|
+
* key's `did:key`. The same secret and `info` give the same key, DID and
|
|
20
|
+
* OrbitDB identity document on every device and for every slot. Use it as
|
|
21
|
+
* `createSessionKeystore({ signer })` and
|
|
22
|
+
* `OrbitDBWebAuthnIdentityProviderFunction({ signer })`.
|
|
23
|
+
*/
|
|
24
|
+
import { generateKeyPairFromSeed } from '@libp2p/crypto/keys';
|
|
25
|
+
import { deriveSubkey } from './prf-keys.js';
|
|
26
|
+
import { createEd25519DidFromPublicKey } from './ed25519-did.js';
|
|
27
|
+
|
|
28
|
+
const MIN_SECRET_BYTES = 32;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An Ed25519 signer derived from a secret, in the shape the provider's
|
|
32
|
+
* `signer` option and `createSessionKeystore` take.
|
|
33
|
+
*
|
|
34
|
+
* The private key stays in this closure: what comes back can sign, but has no
|
|
35
|
+
* field that holds the key.
|
|
36
|
+
*
|
|
37
|
+
* @param {Uint8Array} secret - At least 32 random bytes, e.g. from a vault.
|
|
38
|
+
* @param {Object} options
|
|
39
|
+
* @param {string} options.info - Domain separation, part of the identity:
|
|
40
|
+
* another `info` is another key and another DID. Version it
|
|
41
|
+
* (`invoice/books-identity/v1`).
|
|
42
|
+
* @returns {Promise<{ did: string, type: 'Ed25519', publicKey: Uint8Array,
|
|
43
|
+
* sign: (data: Uint8Array) => Promise<Uint8Array> }>}
|
|
44
|
+
*/
|
|
45
|
+
export async function createSecretSigner(secret, { info } = {}) {
|
|
46
|
+
if (!(secret instanceof Uint8Array) || secret.length < MIN_SECRET_BYTES) {
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
`createSecretSigner needs a secret of at least ${MIN_SECRET_BYTES} bytes`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (typeof info !== 'string' || info.length === 0) {
|
|
52
|
+
throw new TypeError('createSecretSigner needs a non-empty info string');
|
|
53
|
+
}
|
|
54
|
+
const seed = await deriveSubkey(secret, info);
|
|
55
|
+
const key = await generateKeyPairFromSeed('Ed25519', seed);
|
|
56
|
+
seed.fill(0);
|
|
57
|
+
const publicKey = key.publicKey.raw;
|
|
58
|
+
return {
|
|
59
|
+
did: createEd25519DidFromPublicKey(publicKey),
|
|
60
|
+
type: 'Ed25519',
|
|
61
|
+
publicKey,
|
|
62
|
+
sign: async (data) => key.sign(data),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -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/src/standalone/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Lives in keystore/ so the main entry can build an Ed25519 did:key without
|
|
2
|
+
// importing this module, whose `new Worker(new URL(…))` would make bundlers
|
|
3
|
+
// pack the worker into every app.
|
|
4
|
+
import { createEd25519DidFromPublicKey } from '../../keystore/ed25519-did.js';
|
|
5
|
+
|
|
6
|
+
export { createEd25519DidFromPublicKey };
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Convert a Uint8Array into a detached ArrayBuffer slice.
|
|
@@ -59,30 +63,6 @@ function normalizeArchiveAfterDeserialization(archive) {
|
|
|
59
63
|
return archive;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
/**
|
|
63
|
-
* Build a did:key identifier from raw Ed25519 public key bytes.
|
|
64
|
-
* @param {Uint8Array} publicKeyBytes
|
|
65
|
-
* @returns {string}
|
|
66
|
-
*/
|
|
67
|
-
export function createEd25519DidFromPublicKey(publicKeyBytes) {
|
|
68
|
-
if (!(publicKeyBytes instanceof Uint8Array) || publicKeyBytes.length !== 32) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`Invalid Ed25519 public key length: ${publicKeyBytes?.length || 0}`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const ED25519_MULTICODEC = 0xed;
|
|
75
|
-
const codecLength = varint.encodingLength(ED25519_MULTICODEC);
|
|
76
|
-
const codecBytes = new Uint8Array(codecLength);
|
|
77
|
-
varint.encodeTo(ED25519_MULTICODEC, codecBytes, 0);
|
|
78
|
-
|
|
79
|
-
const multikey = new Uint8Array(codecBytes.length + publicKeyBytes.length);
|
|
80
|
-
multikey.set(codecBytes, 0);
|
|
81
|
-
multikey.set(publicKeyBytes, codecBytes.length);
|
|
82
|
-
|
|
83
|
-
return `did:key:${base58btc.encode(multikey)}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
66
|
/**
|
|
87
67
|
* @returns {boolean}
|
|
88
68
|
*/
|
package/src/varsig/credential.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
import { DIDKey } from 'iso-did';
|
|
5
5
|
import { parseAttestationObject } from 'iso-passkeys';
|
|
6
6
|
import { toArrayBuffer } from './utils.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildAuthenticatorSelection,
|
|
9
|
+
resolveAuthenticatorAttachment,
|
|
10
|
+
} from '../webauthn/config.js';
|
|
8
11
|
import { logWebAuthnResponse } from '../webauthn/debug-log.js';
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -62,6 +65,13 @@ function extractCredentialInfo(attestationObject) {
|
|
|
62
65
|
* @param {string} [options.displayName] - Display name.
|
|
63
66
|
* @param {string} [options.domain] - RP ID / domain.
|
|
64
67
|
* @param {boolean} [options.discoverableCredentials] - Override global discoverable credential policy.
|
|
68
|
+
* @param {'platform'|'cross-platform'|'any'} [options.authenticatorType='any'] -
|
|
69
|
+
* Which authenticators the browser offers, as for
|
|
70
|
+
* `WebAuthnDIDProvider.createCredential`. `'any'` names none in the
|
|
71
|
+
* request, which is what this has sent since 0.2.8.
|
|
72
|
+
* @param {'platform'|'cross-platform'} [options.authenticatorAttachment] -
|
|
73
|
+
* The WebAuthn name for the same choice; refused if it disagrees with
|
|
74
|
+
* `authenticatorType`.
|
|
65
75
|
* @returns {Promise<Object>} Credential info including public key and DID.
|
|
66
76
|
*/
|
|
67
77
|
async function createWebAuthnVarsigCredential(options = {}) {
|
|
@@ -69,7 +79,6 @@ async function createWebAuthnVarsigCredential(options = {}) {
|
|
|
69
79
|
userId,
|
|
70
80
|
displayName,
|
|
71
81
|
domain,
|
|
72
|
-
authenticatorType = 'any',
|
|
73
82
|
forceP256 = false,
|
|
74
83
|
} = {
|
|
75
84
|
userId: `orbitdb-user-${Date.now()}`,
|
|
@@ -78,6 +87,9 @@ async function createWebAuthnVarsigCredential(options = {}) {
|
|
|
78
87
|
...options,
|
|
79
88
|
};
|
|
80
89
|
|
|
90
|
+
// Before any prompt: a choice this cannot honour is the caller's mistake.
|
|
91
|
+
const authenticatorAttachment = resolveAuthenticatorAttachment(options);
|
|
92
|
+
|
|
81
93
|
const publicKey = {
|
|
82
94
|
rp: { name: 'OrbitDB Varsig Identity', id: domain },
|
|
83
95
|
user: {
|
|
@@ -94,13 +106,19 @@ async function createWebAuthnVarsigCredential(options = {}) {
|
|
|
94
106
|
{ type: 'public-key', alg: -7 },
|
|
95
107
|
],
|
|
96
108
|
attestation: 'none',
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
// This named the attachment and `userVerification: 'preferred'` from March
|
|
110
|
+
// to October 2026 without effect: buildAuthenticatorSelection passed on
|
|
111
|
+
// neither, so every kind of authenticator was offered and user
|
|
112
|
+
// verification was required. Every kind stays the default;
|
|
113
|
+
// `authenticatorType` narrows it. Verification stays required: a varsig
|
|
114
|
+
// signature without the UV flag is refused — `verifyWebAuthnAssertion`
|
|
115
|
+
// insists on it, when signing and when verifying — and asking for less
|
|
116
|
+
// would let an authenticator that cannot verify the user register here
|
|
117
|
+
// and then never sign.
|
|
118
|
+
authenticatorSelection: {
|
|
119
|
+
...buildAuthenticatorSelection(options),
|
|
120
|
+
...(authenticatorAttachment && { authenticatorAttachment }),
|
|
121
|
+
},
|
|
104
122
|
// Recovery writes the varsig metadata into the passkey's largeBlob after
|
|
105
123
|
// registration — which an authenticator only allows for a credential that
|
|
106
124
|
// was created asking for it. `preferred` costs nothing where the
|
package/src/webauthn/config.js
CHANGED
|
@@ -99,22 +99,20 @@ export function resolveAuthenticatorAttachment(
|
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* `authenticatorSelection` for a registration. Only the discoverable-credential
|
|
102
|
-
* policy comes from `options` and `configureWebAuthn
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* policy comes from `options` and `configureWebAuthn`, and user verification is
|
|
103
|
+
* always required; an `authenticatorAttachment` or `userVerification` in
|
|
104
|
+
* `options` is not read. This used to destructure both as if it read them,
|
|
105
|
+
* which is how two registrations came to name values that never reached the
|
|
106
|
+
* browser. A registration that wants an attachment adds it itself — see
|
|
107
|
+
* `resolveAuthenticatorAttachment`.
|
|
105
108
|
*/
|
|
106
109
|
export function buildAuthenticatorSelection(options = {}) {
|
|
107
|
-
const {
|
|
108
|
-
discoverableCredentials,
|
|
109
|
-
authenticatorAttachment,
|
|
110
|
-
userVerification = 'required',
|
|
111
|
-
} = resolveWebAuthnConfig(options);
|
|
110
|
+
const { discoverableCredentials } = resolveWebAuthnConfig(options);
|
|
112
111
|
|
|
113
112
|
return {
|
|
114
|
-
...(authenticatorAttachment ? { authenticatorAttachment } : {}),
|
|
115
113
|
requireResidentKey: discoverableCredentials,
|
|
116
114
|
residentKey: discoverableCredentials ? 'required' : 'discouraged',
|
|
117
|
-
userVerification,
|
|
115
|
+
userVerification: 'required',
|
|
118
116
|
};
|
|
119
117
|
}
|
|
120
118
|
|
package/types/index.d.ts
CHANGED
|
@@ -619,6 +619,22 @@ export function replacePayload(
|
|
|
619
619
|
/** The slot id an authenticator is filed under. */
|
|
620
620
|
export function slotIdFor(rawCredentialId: Uint8Array): Promise<string>;
|
|
621
621
|
|
|
622
|
+
/**
|
|
623
|
+
* An Ed25519 signer derived from a secret — an identity that is nobody's
|
|
624
|
+
* passkey, e.g. the books every key of their owner writes as. Same secret and
|
|
625
|
+
* `info`, same DID everywhere. Use with `createSessionKeystore({ signer })`
|
|
626
|
+
* and the provider's `signer` option.
|
|
627
|
+
*/
|
|
628
|
+
export function createSecretSigner(
|
|
629
|
+
secret: Uint8Array,
|
|
630
|
+
options: { info: string }
|
|
631
|
+
): Promise<{
|
|
632
|
+
did: string;
|
|
633
|
+
type: 'Ed25519';
|
|
634
|
+
publicKey: Uint8Array;
|
|
635
|
+
sign(data: Uint8Array): Promise<Uint8Array>;
|
|
636
|
+
}>;
|
|
637
|
+
|
|
622
638
|
export type VaultErrorCode =
|
|
623
639
|
| 'VAULT_MALFORMED'
|
|
624
640
|
| 'VAULT_NO_SLOT'
|
|
@@ -643,3 +659,26 @@ declare const defaultExport: {
|
|
|
643
659
|
};
|
|
644
660
|
|
|
645
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
|
@@ -40,6 +40,7 @@ export interface ExtensionSupport {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export interface OrbitDBWebAuthnIdentityProviderOptions {
|
|
43
|
+
/** Required unless `signer` is given. */
|
|
43
44
|
webauthnCredential?: Record<string, unknown>;
|
|
44
45
|
keystore?: unknown;
|
|
45
46
|
usePersistentKey?: boolean;
|
|
@@ -95,6 +96,11 @@ export function createSessionKeystore(options?: {
|
|
|
95
96
|
signer?: ExternalSigner;
|
|
96
97
|
}): Promise<unknown>;
|
|
97
98
|
export function isSessionKeystore(keystore: unknown): boolean;
|
|
99
|
+
/** An Ed25519 signer derived from a secret (HKDF under `info`): an identity that is nobody's passkey. */
|
|
100
|
+
export function createSecretSigner(
|
|
101
|
+
secret: Uint8Array,
|
|
102
|
+
options: { info: string }
|
|
103
|
+
): Promise<ExternalSigner>;
|
|
98
104
|
export function assertSigner(signer: unknown): void;
|
|
99
105
|
|
|
100
106
|
/** The authenticator returned no PRF output; nothing stands in for it. */
|
|
@@ -200,3 +206,26 @@ declare const defaultExport: {
|
|
|
200
206
|
};
|
|
201
207
|
|
|
202
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>;
|
package/types/standalone.d.ts
CHANGED
|
@@ -26,7 +26,15 @@ export interface WebAuthnVarsigCredential {
|
|
|
26
26
|
export interface WebAuthnSignerOptions {
|
|
27
27
|
userId?: string;
|
|
28
28
|
displayName?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Which authenticators the browser offers: the device's own, a security key
|
|
31
|
+
* or phone, or every kind. Default `'any'`: the request names none.
|
|
32
|
+
*/
|
|
29
33
|
authenticatorType?: AuthenticatorType;
|
|
34
|
+
/** The WebAuthn name for `authenticatorType`; the two must not disagree. */
|
|
35
|
+
authenticatorAttachment?: AuthenticatorAttachment;
|
|
36
|
+
/** Not read: a registration always requires user verification. */
|
|
37
|
+
userVerification?: UserVerificationRequirement;
|
|
30
38
|
forceP256?: boolean;
|
|
31
39
|
domain?: string;
|
|
32
40
|
[key: string]: unknown;
|
|
@@ -381,6 +389,22 @@ export function replacePayload(
|
|
|
381
389
|
/** The slot id an authenticator is filed under. */
|
|
382
390
|
export function slotIdFor(rawCredentialId: Uint8Array): Promise<string>;
|
|
383
391
|
|
|
392
|
+
/**
|
|
393
|
+
* An Ed25519 signer derived from a secret — an identity that is nobody's
|
|
394
|
+
* passkey, e.g. the books every key of their owner writes as. Same secret and
|
|
395
|
+
* `info`, same DID everywhere. Use with `createSessionKeystore({ signer })`
|
|
396
|
+
* and the provider's `signer` option.
|
|
397
|
+
*/
|
|
398
|
+
export function createSecretSigner(
|
|
399
|
+
secret: Uint8Array,
|
|
400
|
+
options: { info: string }
|
|
401
|
+
): Promise<{
|
|
402
|
+
did: string;
|
|
403
|
+
type: 'Ed25519';
|
|
404
|
+
publicKey: Uint8Array;
|
|
405
|
+
sign(data: Uint8Array): Promise<Uint8Array>;
|
|
406
|
+
}>;
|
|
407
|
+
|
|
384
408
|
export type VaultErrorCode =
|
|
385
409
|
| 'VAULT_MALFORMED'
|
|
386
410
|
| 'VAULT_NO_SLOT'
|