@le-space/orbitdb-identity-provider-webauthn-did 0.5.4 → 0.6.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 +72 -1
- package/README.md +58 -31
- package/package.json +1 -1
- package/src/index.js +17 -0
- package/src/keystore/derived-signing-key.js +9 -7
- package/src/webauthn/prf-input.js +46 -0
- package/src/webauthn/provider.js +22 -36
- package/src/webauthn/recovery.js +242 -0
- package/src/webauthn/restore.js +121 -0
- package/types/index.d.ts +46 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
An identity can be restored on a second device with **nothing stored**: two
|
|
6
|
+
touches of the same passkey give back the DID and the signing key, so
|
|
7
|
+
largeBlob and `localStorage` become a convenience rather than the only road.
|
|
8
|
+
Measured on two phones with one YubiKey before it was written (#61).
|
|
9
|
+
|
|
10
|
+
Nothing existing moves: a credential registered before this keeps the PRF
|
|
11
|
+
input it was made with, and the identity it derives is unchanged. Credentials
|
|
12
|
+
registered from here on are asked the input every device can compute, which is
|
|
13
|
+
what makes the restore work. Two behaviours do change — `getPrfOutput()` now
|
|
14
|
+
falls back to that derivable input instead of returning `null` when a
|
|
15
|
+
credential has none stored, and `createDID()` throws instead of inventing a
|
|
16
|
+
"base58-like" identifier when it cannot encode a key.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **An identity can be restored on a device that has stored nothing** (#61).
|
|
21
|
+
`restoreIdentityFromAuthenticator()` touches the same passkey twice: once
|
|
22
|
+
with user verification and the PRF input every device can compute, once for a
|
|
23
|
+
second signature. Two ECDSA signatures admit exactly one common public key —
|
|
24
|
+
which an assertion does not carry, and which is why the identity metadata has
|
|
25
|
+
had to live in largeBlob or `localStorage` until now. From it: the DID, and
|
|
26
|
+
the signing key derived from the PRF output with the DID mixed in. The pieces
|
|
27
|
+
are exported on their own as well: `prfInputForRelyingParty()`,
|
|
28
|
+
`recoverPublicKey()` and `recoverPublicKeyCandidates()`.
|
|
29
|
+
|
|
30
|
+
Measured on hardware before it was written — one YubiKey, a Galaxy Fold 5 and
|
|
31
|
+
a Galaxy A57, 2026-09-19: the same PRF value on both phones, the same DID
|
|
32
|
+
recovered from two signatures, the same derived signing key. What of that is
|
|
33
|
+
machine-checkable runs in `tests/webauthn-recovery.test.js`, against a
|
|
34
|
+
software authenticator with a real P-256 key.
|
|
35
|
+
|
|
36
|
+
Nothing stands in for a missing PRF: an authenticator without it gets a
|
|
37
|
+
refusal, because an identity derived from something else is a different
|
|
38
|
+
identity that looks like success.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **The PRF input is now fixed per relying party** instead of drawn at random
|
|
43
|
+
per credential (#61). It was stored with the credential metadata, so the
|
|
44
|
+
derived signing key — and with it the identity — was reachable only from the
|
|
45
|
+
device that kept that metadata; a second device asking the same authenticator
|
|
46
|
+
a different question got a different answer. The input is not a secret, the
|
|
47
|
+
output is. Credentials registered earlier keep working: their stored input is
|
|
48
|
+
still used when there is one, and `getPrfOutput()` now falls back to the
|
|
49
|
+
derivable input instead of giving up when there is none.
|
|
50
|
+
- **`createDID` refuses a key it cannot encode.** Its `catch` built a
|
|
51
|
+
"base58-like" identifier out of the same coordinates: a string that looked
|
|
52
|
+
like a DID, was not `did:key`, and could not be reproduced by anything else
|
|
53
|
+
reading that key — including this library on another device. It throws now.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- CI now runs all 22 test files. Eight never ran there: `identity-forgery`,
|
|
58
|
+
`p256-signature-shapes`, `session-keystore`, `webauthn-debug-log`,
|
|
59
|
+
`webauthn-extension-support`, `webauthn-user-handle`,
|
|
60
|
+
`webauthn-varsig-challenge` and `worker-signer`. They were listed in
|
|
61
|
+
`playwright.node.config.js`, which only the local `preversion` and
|
|
62
|
+
`prepublishOnly` hooks ran, while `release.yml` publishes with
|
|
63
|
+
`--ignore-scripts` because CI is its gate — so no automated gate ran them
|
|
64
|
+
before a release. CI now runs that config in one step, before installing
|
|
65
|
+
browsers; `standalone-toolkit`, `webauthn-attestation-parsing` and
|
|
66
|
+
`webauthn-two-peer-replication` lose their separate steps, which booted a
|
|
67
|
+
demo server they do not use.
|
|
68
|
+
- `identity-forgery` did not load in a clean install. It imported `concat`
|
|
69
|
+
from `uint8arrays`, which this package does not declare; only a hoisted
|
|
70
|
+
`node_modules` (`node-linker=hoisted`) made it resolvable. It now takes
|
|
71
|
+
`concat` from `iso-base/utils`, a declared dependency.
|
|
72
|
+
- `scripts/check-ci-runs-every-test.mjs` fails CI when a file in `tests/` is
|
|
73
|
+
selected by no step. This is the second time suites went unrun: 0.4.0 found
|
|
74
|
+
five.
|
|
4
75
|
|
|
5
76
|
## 0.5.4
|
|
6
77
|
|
package/README.md
CHANGED
|
@@ -188,48 +188,71 @@ sequenceDiagram
|
|
|
188
188
|
participant App
|
|
189
189
|
participant WebAuthn
|
|
190
190
|
participant Auth as Authenticator
|
|
191
|
-
participant
|
|
192
|
-
participant
|
|
191
|
+
participant Prov as Keystore-DID provider
|
|
192
|
+
participant LS as localStorage
|
|
193
|
+
participant KS as OrbitDB keystore
|
|
193
194
|
participant DB as OrbitDB
|
|
194
195
|
|
|
195
196
|
User->>App: Create credential
|
|
196
|
-
App->>WebAuthn: create()
|
|
197
|
+
App->>WebAuthn: create() with PRF and largeBlob requested
|
|
197
198
|
WebAuthn->>Auth: Create passkey
|
|
198
199
|
Auth-->>WebAuthn: Attestation
|
|
199
200
|
WebAuthn-->>App: Credential
|
|
200
201
|
|
|
201
|
-
App->>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
alt prf
|
|
209
|
-
App->>WebAuthn: get() with PRF
|
|
202
|
+
App->>Prov: createIdentity({ useKeystoreDID, encryptKeystore })
|
|
203
|
+
alt encryptKeystore, first session
|
|
204
|
+
Prov->>Prov: generateSecretKey() gives sk
|
|
205
|
+
Prov->>Prov: generateKeyPair() gives a random Ed25519 key
|
|
206
|
+
Prov->>Prov: AES-GCM encrypt the private key with sk
|
|
207
|
+
alt prf (the default)
|
|
208
|
+
Prov->>WebAuthn: get() with PRF
|
|
210
209
|
WebAuthn->>Auth: User verification
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
alt PRF output
|
|
211
|
+
Auth-->>Prov: PRF output
|
|
212
|
+
Prov->>Prov: wrap sk with a key derived from it
|
|
213
|
+
Prov->>LS: sealed key, wrapped sk, PRF input
|
|
214
|
+
Prov->>KS: addKey(did, the unlocked key)
|
|
215
|
+
else no PRF
|
|
216
|
+
Auth-->>Prov: no PRF result
|
|
217
|
+
Note right of Prov: Nothing is sealed. encryptionState is { enabled false, reason prf-unavailable }
|
|
218
|
+
Prov->>KS: getKey(), or add a generated key
|
|
219
|
+
end
|
|
214
220
|
else largeBlob
|
|
215
|
-
|
|
221
|
+
Prov->>WebAuthn: get() with largeBlob write
|
|
216
222
|
WebAuthn->>Auth: User verification
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
alt written
|
|
224
|
+
Auth-->>Prov: written is true
|
|
225
|
+
Prov->>LS: sealed key, sk kept on the authenticator
|
|
226
|
+
Prov->>KS: addKey(did, the unlocked key)
|
|
227
|
+
else not written
|
|
228
|
+
Auth-->>Prov: written is false
|
|
229
|
+
Note right of Prov: Throws. Nothing is stored.
|
|
230
|
+
end
|
|
219
231
|
else hmac-secret
|
|
220
|
-
|
|
232
|
+
Prov->>WebAuthn: get() with hmac-secret
|
|
221
233
|
WebAuthn->>Auth: User verification
|
|
222
|
-
Auth-->>
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
Auth-->>Prov: HMAC output
|
|
235
|
+
Prov->>Prov: wrap sk with it
|
|
236
|
+
Prov->>LS: sealed key, wrapped sk
|
|
237
|
+
Prov->>KS: addKey(did, the unlocked key)
|
|
225
238
|
end
|
|
239
|
+
else encryptKeystore with prf, later session
|
|
240
|
+
Prov->>LS: load the sealed key
|
|
241
|
+
Prov->>WebAuthn: get() with PRF, the stored input
|
|
242
|
+
Auth-->>Prov: PRF output
|
|
243
|
+
Prov->>Prov: unwrap sk, then decrypt the key
|
|
244
|
+
Prov->>KS: addKey(did, the unlocked key)
|
|
245
|
+
else not encrypted
|
|
246
|
+
Prov->>KS: getKey(), or add a generated key
|
|
226
247
|
end
|
|
248
|
+
Prov-->>App: Identity, DID = did:key of the keystore key
|
|
227
249
|
|
|
250
|
+
User->>App: Add entry
|
|
228
251
|
App->>DB: db.put()
|
|
229
|
-
DB->>KS: sign entry with keystore key
|
|
252
|
+
DB->>KS: sign entry with the keystore key
|
|
230
253
|
KS-->>DB: Entry signature
|
|
231
254
|
|
|
232
|
-
Note over App,KS:
|
|
255
|
+
Note over App,KS: The sealed copy stays at rest. The unlocked key lives in the OrbitDB keystore, so create it with createSessionKeystore() to keep it in memory.
|
|
233
256
|
```
|
|
234
257
|
|
|
235
258
|
OrbitDB signs with whatever its keystore returns, and the default keystore
|
|
@@ -274,24 +297,28 @@ sequenceDiagram
|
|
|
274
297
|
participant DB as OrbitDB
|
|
275
298
|
|
|
276
299
|
User->>App: Create credential
|
|
277
|
-
App->>WebAuthn: create()
|
|
300
|
+
App->>WebAuthn: create() with largeBlob requested
|
|
278
301
|
WebAuthn->>Auth: Create passkey
|
|
279
302
|
Auth-->>WebAuthn: Attestation
|
|
280
303
|
WebAuthn-->>App: Credential
|
|
281
304
|
|
|
282
305
|
User->>App: Create varsig identity
|
|
283
|
-
App->>Var:
|
|
284
|
-
Var->>
|
|
306
|
+
App->>Var: createWebAuthnVarsigIdentity({ credential })
|
|
307
|
+
Var->>Var: id = did:key of the passkey's public key
|
|
308
|
+
Var->>WebAuthn: get() over the id
|
|
285
309
|
WebAuthn->>Auth: User verification
|
|
286
310
|
Auth-->>WebAuthn: Assertion
|
|
287
|
-
WebAuthn-->>Var: Assertion
|
|
288
|
-
Var->>
|
|
311
|
+
WebAuthn-->>Var: Assertion, becomes signatures.id
|
|
312
|
+
Var->>WebAuthn: get() over publicKey + idSignature
|
|
313
|
+
WebAuthn->>Auth: User verification
|
|
314
|
+
Auth-->>WebAuthn: Assertion
|
|
315
|
+
WebAuthn-->>Var: Assertion, becomes signatures.publicKey
|
|
289
316
|
Var-->>App: Identity
|
|
290
317
|
|
|
291
318
|
User->>App: Add entry
|
|
292
319
|
App->>DB: db.put()
|
|
293
|
-
DB->>Var:
|
|
294
|
-
Var->>WebAuthn: get()
|
|
320
|
+
DB->>Var: sign(entry)
|
|
321
|
+
Var->>WebAuthn: get() over the entry
|
|
295
322
|
WebAuthn->>Auth: User verification
|
|
296
323
|
Auth-->>WebAuthn: Assertion
|
|
297
324
|
WebAuthn-->>Var: Assertion
|
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.6.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",
|
package/src/index.js
CHANGED
|
@@ -297,6 +297,23 @@ export {
|
|
|
297
297
|
isSessionKeystore,
|
|
298
298
|
} from './keystore/session-keystore.js';
|
|
299
299
|
export { PrfUnavailableError } from './errors.js';
|
|
300
|
+
// Restoring on a device that has stored nothing (#61): the fixed PRF input,
|
|
301
|
+
// the public key recovered from two assertions, and the two together.
|
|
302
|
+
export {
|
|
303
|
+
PRF_INPUT_INFO,
|
|
304
|
+
prfInputForRelyingParty,
|
|
305
|
+
} from './webauthn/prf-input.js';
|
|
306
|
+
export {
|
|
307
|
+
recoverPublicKey,
|
|
308
|
+
recoverPublicKeyCandidates,
|
|
309
|
+
} from './webauthn/recovery.js';
|
|
310
|
+
export { restoreIdentityFromAuthenticator } from './webauthn/restore.js';
|
|
311
|
+
// The two halves of it, for a caller that already holds one: a PRF output, or
|
|
312
|
+
// a credential it wants the output for.
|
|
313
|
+
export {
|
|
314
|
+
deriveSigningKeyBytes,
|
|
315
|
+
getPrfOutput,
|
|
316
|
+
} from './keystore/derived-signing-key.js';
|
|
300
317
|
|
|
301
318
|
export default {
|
|
302
319
|
WebAuthnDIDProvider,
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
|
|
36
36
|
import { CRYPTO_ALGORITHMS, KEY_TYPES } from '../constants.js';
|
|
37
37
|
import { buildCredentialRequestOptions } from '../webauthn/config.js';
|
|
38
|
+
import { prfInputForRelyingParty } from '../webauthn/prf-input.js';
|
|
38
39
|
|
|
39
40
|
const log = logger('orbitdb-identity-provider-webauthn-did:derived-key');
|
|
40
41
|
|
|
@@ -126,12 +127,13 @@ export async function deriveSigningKeyBytes(
|
|
|
126
127
|
* @returns {Promise<Uint8Array|null>} PRF output, or null.
|
|
127
128
|
*/
|
|
128
129
|
export async function getPrfOutput(credential, { rpId } = {}) {
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
// A credential registered before #61 carries the random input it was made
|
|
131
|
+
// with, and that input is the only one its authenticator answers the same
|
|
132
|
+
// way twice. Without one, fall back to the input every device can compute
|
|
133
|
+
// (#61): a credential registered since then was asked exactly that, so a
|
|
134
|
+
// second device needs nothing from the first.
|
|
135
|
+
const prfInput =
|
|
136
|
+
credential?.prfInput ?? (await prfInputForRelyingParty(rpId));
|
|
135
137
|
|
|
136
138
|
if (typeof navigator === 'undefined' || !navigator.credentials?.get) {
|
|
137
139
|
log('no WebAuthn available; cannot derive a key');
|
|
@@ -153,7 +155,7 @@ export async function getPrfOutput(credential, { rpId } = {}) {
|
|
|
153
155
|
credentialId: rawCredentialId,
|
|
154
156
|
rpId: rpId ?? globalThis.window?.location?.hostname,
|
|
155
157
|
userVerification: 'required',
|
|
156
|
-
extensions: { prf: { eval: { first:
|
|
158
|
+
extensions: { prf: { eval: { first: prfInput } } },
|
|
157
159
|
})
|
|
158
160
|
);
|
|
159
161
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module PrfInput
|
|
3
|
+
* @description
|
|
4
|
+
* The PRF input an authenticator is asked to evaluate.
|
|
5
|
+
*
|
|
6
|
+
* It was drawn at random per credential and stored with the credential's
|
|
7
|
+
* metadata, which makes the derived signing key — and with it the identity —
|
|
8
|
+
* reachable only from the device that kept that metadata. A second device
|
|
9
|
+
* holding the same passkey asks the same authenticator and gets a different
|
|
10
|
+
* answer, because it asks a different question.
|
|
11
|
+
*
|
|
12
|
+
* Fixing the input per relying party removes the question: both devices ask
|
|
13
|
+
* the same thing, and the authenticator answers the same thing. The input is
|
|
14
|
+
* not a secret — the *output* is, and it never leaves the authenticator's
|
|
15
|
+
* answer — so nothing is lost by making it derivable. See issue #61.
|
|
16
|
+
*/
|
|
17
|
+
import { CRYPTO_ALGORITHMS } from '../constants.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Bumping this rotates every PRF output, and with it every derived signing
|
|
21
|
+
* key, so treat it as a breaking change.
|
|
22
|
+
*/
|
|
23
|
+
export const PRF_INPUT_INFO = 'orbitdb-identity-provider-webauthn-did:prf:v2';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The fixed PRF input for a relying party.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} rpId - Relying party id, e.g. `example.org`. The current
|
|
29
|
+
* host is used when it is omitted and there is one.
|
|
30
|
+
* @returns {Promise<Uint8Array>} 32 bytes, the same on every device.
|
|
31
|
+
*/
|
|
32
|
+
export async function prfInputForRelyingParty(rpId) {
|
|
33
|
+
const party =
|
|
34
|
+
rpId ??
|
|
35
|
+
(typeof globalThis.location === 'object'
|
|
36
|
+
? globalThis.location.hostname
|
|
37
|
+
: undefined);
|
|
38
|
+
if (typeof party !== 'string' || party.length === 0) {
|
|
39
|
+
throw new TypeError('prfInputForRelyingParty needs a relying party id');
|
|
40
|
+
}
|
|
41
|
+
const digest = await crypto.subtle.digest(
|
|
42
|
+
CRYPTO_ALGORITHMS.SHA_256,
|
|
43
|
+
new TextEncoder().encode(`${PRF_INPUT_INFO}:${party}`)
|
|
44
|
+
);
|
|
45
|
+
return new Uint8Array(digest);
|
|
46
|
+
}
|
package/src/webauthn/provider.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
KEYSTORE_ENCRYPTION_METHODS,
|
|
17
17
|
} from '../constants.js';
|
|
18
18
|
import {
|
|
19
|
+
ERROR_CODES,
|
|
19
20
|
WebAuthnIdentityError,
|
|
20
21
|
WebAuthnAuthenticationError,
|
|
21
22
|
WebAuthnCredentialError,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
buildCredentialRequestOptions,
|
|
27
28
|
} from './config.js';
|
|
28
29
|
import { logWebAuthnResponse } from './debug-log.js';
|
|
30
|
+
import { prfInputForRelyingParty } from './prf-input.js';
|
|
29
31
|
import {
|
|
30
32
|
p256PublicKeyBytes,
|
|
31
33
|
verifyWebAuthnProof,
|
|
@@ -173,7 +175,8 @@ export class WebAuthnDIDProvider {
|
|
|
173
175
|
|
|
174
176
|
if (keystoreEncryptionMethod === KEYSTORE_ENCRYPTION_METHODS.PRF) {
|
|
175
177
|
const prfConfig = KeystoreEncryption.addPRFToCredentialOptions(
|
|
176
|
-
credentialOptions.publicKey
|
|
178
|
+
credentialOptions.publicKey,
|
|
179
|
+
await prfInputForRelyingParty(domain)
|
|
177
180
|
);
|
|
178
181
|
credentialOptions.publicKey = prfConfig.credentialOptions;
|
|
179
182
|
prfInput = prfConfig.prfInput;
|
|
@@ -199,14 +202,19 @@ export class WebAuthnDIDProvider {
|
|
|
199
202
|
};
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
// Request PRF even when the keystore is not encrypted
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
+
// Request PRF even when the keystore is not encrypted: it is what lets the
|
|
206
|
+
// OrbitDB signing key be derived reproducibly later, so a credential
|
|
207
|
+
// registered without it can never get a reproducible identity. The input
|
|
208
|
+
// is fixed per relying party rather than drawn at random (#61), so a
|
|
209
|
+
// second device asks the authenticator the same question and gets the same
|
|
210
|
+
// answer — without that, "the same passkey" still means a different key on
|
|
211
|
+
// every device.
|
|
205
212
|
// Requesting the extension is harmless where it is unsupported: the
|
|
206
213
|
// authenticator simply returns no PRF results.
|
|
207
214
|
if (!prfInput) {
|
|
208
215
|
const prfConfig = KeystoreEncryption.addPRFToCredentialOptions(
|
|
209
|
-
credentialOptions.publicKey
|
|
216
|
+
credentialOptions.publicKey,
|
|
217
|
+
await prfInputForRelyingParty(domain)
|
|
210
218
|
);
|
|
211
219
|
credentialOptions.publicKey = prfConfig.credentialOptions;
|
|
212
220
|
prfInput = prfConfig.prfInput;
|
|
@@ -581,38 +589,16 @@ export class WebAuthnDIDProvider {
|
|
|
581
589
|
const multikeyEncoded = base58btc.encode(multikey);
|
|
582
590
|
return `${DID_KEY_PREFIX}${multikeyEncoded}`;
|
|
583
591
|
} catch (error) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
592
|
+
// No fallback encoding. What stood here invented a "base58-like"
|
|
593
|
+
// identifier out of the same coordinates — a string that looks like a
|
|
594
|
+
// DID, is not `did:key`, and cannot be reproduced by anything else that
|
|
595
|
+
// reads this key, including this library on another device. An identity
|
|
596
|
+
// nobody else computes the same way is worse than no identity, and it
|
|
597
|
+
// fails silently, at registration, where nothing checks it again.
|
|
598
|
+
throw new WebAuthnIdentityError(
|
|
599
|
+
`could not encode the public key as a did:key: ${error.message}`,
|
|
600
|
+
{ code: ERROR_CODES.INVALID_INPUT, cause: error }
|
|
587
601
|
);
|
|
588
|
-
|
|
589
|
-
// Fallback: Simple DID creation without multiformats dependency
|
|
590
|
-
const { x, y } = credentialInfo.publicKey;
|
|
591
|
-
|
|
592
|
-
// Create a hash-based approach for consistency
|
|
593
|
-
const combined = new Uint8Array(x.length + y.length);
|
|
594
|
-
combined.set(x, 0);
|
|
595
|
-
combined.set(y, x.length);
|
|
596
|
-
|
|
597
|
-
// Simple base58-like encoding for fallback
|
|
598
|
-
const base58Chars =
|
|
599
|
-
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
600
|
-
let encoded = 'z'; // base58btc prefix
|
|
601
|
-
|
|
602
|
-
for (let i = 0; i < Math.min(combined.length, 32); i += 4) {
|
|
603
|
-
const chunk = combined.slice(i, i + 4);
|
|
604
|
-
let value = 0;
|
|
605
|
-
for (let j = 0; j < chunk.length; j++) {
|
|
606
|
-
value = value * 256 + chunk[j];
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
for (let k = 0; k < 6; k++) {
|
|
610
|
-
encoded += base58Chars[value % 58];
|
|
611
|
-
value = Math.floor(value / 58);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return `${DID_KEY_PREFIX}${encoded}`;
|
|
616
602
|
}
|
|
617
603
|
}
|
|
618
604
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Recovery
|
|
3
|
+
* @description
|
|
4
|
+
* Get the credential's public key back from assertions, so a device that has
|
|
5
|
+
* stored nothing can still know which identity it holds.
|
|
6
|
+
*
|
|
7
|
+
* A WebAuthn assertion does not carry the public key; only registration does.
|
|
8
|
+
* That is why the identity metadata has had to be kept somewhere — largeBlob,
|
|
9
|
+
* localStorage, a proof store — and why a fresh device with the same
|
|
10
|
+
* authenticator could not name the identity it was holding.
|
|
11
|
+
*
|
|
12
|
+
* It does not have to be kept. An ECDSA signature admits exactly two public
|
|
13
|
+
* keys, and both are computable from the signature itself; a second signature
|
|
14
|
+
* over a different challenge admits two more, and only the signer's key is in
|
|
15
|
+
* both sets. So two touches of the same passkey are enough to recover the key,
|
|
16
|
+
* and from it the DID. See issue #61.
|
|
17
|
+
*
|
|
18
|
+
* Every candidate this module returns has been checked against the signature
|
|
19
|
+
* it came from with `crypto.subtle.verify`. A mistake in the arithmetic below
|
|
20
|
+
* can therefore cost a candidate, never invent one: what comes out has
|
|
21
|
+
* verified, or it does not come out.
|
|
22
|
+
*/
|
|
23
|
+
import { derToRawSignature } from 'iso-webauthn-varsig';
|
|
24
|
+
|
|
25
|
+
import { ERROR_CODES, WebAuthnIdentityError } from '../errors.js';
|
|
26
|
+
import { CRYPTO_ALGORITHMS } from '../constants.js';
|
|
27
|
+
|
|
28
|
+
// NIST P-256 (secp256r1). a = p - 3.
|
|
29
|
+
const P = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn;
|
|
30
|
+
const N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
31
|
+
const B = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn;
|
|
32
|
+
const G = {
|
|
33
|
+
x: 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296n,
|
|
34
|
+
y: 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5n,
|
|
35
|
+
};
|
|
36
|
+
const P1363_SIGNATURE_LENGTH = 64;
|
|
37
|
+
const COORDINATE_BYTES = 32;
|
|
38
|
+
|
|
39
|
+
const mod = (value, modulus = P) => ((value % modulus) + modulus) % modulus;
|
|
40
|
+
|
|
41
|
+
/** Extended Euclid; `value` must not be a multiple of `modulus`. */
|
|
42
|
+
function inverse(value, modulus) {
|
|
43
|
+
let [old, current] = [mod(value, modulus), modulus];
|
|
44
|
+
let [oldCoefficient, coefficient] = [1n, 0n];
|
|
45
|
+
while (current !== 0n) {
|
|
46
|
+
const quotient = old / current;
|
|
47
|
+
[old, current] = [current, old - quotient * current];
|
|
48
|
+
[oldCoefficient, coefficient] = [
|
|
49
|
+
coefficient,
|
|
50
|
+
oldCoefficient - quotient * coefficient,
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
if (old !== 1n) throw new Error('not invertible');
|
|
54
|
+
return mod(oldCoefficient, modulus);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function power(base, exponent, modulus) {
|
|
58
|
+
let result = 1n;
|
|
59
|
+
let factor = mod(base, modulus);
|
|
60
|
+
let left = exponent;
|
|
61
|
+
while (left > 0n) {
|
|
62
|
+
if (left & 1n) result = mod(result * factor, modulus);
|
|
63
|
+
factor = mod(factor * factor, modulus);
|
|
64
|
+
left >>= 1n;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** P ≡ 3 (mod 4), so a square root is one exponentiation. Null when there is none. */
|
|
70
|
+
function squareRoot(value) {
|
|
71
|
+
const root = power(value, (P + 1n) / 4n, P);
|
|
72
|
+
return mod(root * root, P) === mod(value, P) ? root : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Points are `{ x, y }`, or null for the point at infinity.
|
|
76
|
+
function add(left, right) {
|
|
77
|
+
if (left === null) return right;
|
|
78
|
+
if (right === null) return left;
|
|
79
|
+
if (left.x === right.x && mod(left.y + right.y) === 0n) return null;
|
|
80
|
+
const slope =
|
|
81
|
+
left.x === right.x && left.y === right.y
|
|
82
|
+
? mod((3n * left.x * left.x - 3n) * inverse(2n * left.y, P))
|
|
83
|
+
: mod((right.y - left.y) * inverse(right.x - left.x, P));
|
|
84
|
+
const x = mod(slope * slope - left.x - right.x);
|
|
85
|
+
return { x, y: mod(slope * (left.x - x) - left.y) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function multiply(point, scalar) {
|
|
89
|
+
let result = null;
|
|
90
|
+
let addend = point;
|
|
91
|
+
let left = mod(scalar, N);
|
|
92
|
+
while (left > 0n) {
|
|
93
|
+
if (left & 1n) result = add(result, addend);
|
|
94
|
+
addend = add(addend, addend);
|
|
95
|
+
left >>= 1n;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const toBigInt = (bytes) =>
|
|
101
|
+
bytes.reduce((value, byte) => (value << 8n) + BigInt(byte), 0n);
|
|
102
|
+
|
|
103
|
+
function toBytes(value, length = COORDINATE_BYTES) {
|
|
104
|
+
const out = new Uint8Array(length);
|
|
105
|
+
let left = value;
|
|
106
|
+
for (let index = length - 1; index >= 0; index -= 1) {
|
|
107
|
+
out[index] = Number(left & 0xffn);
|
|
108
|
+
left >>= 8n;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const asBytes = (value) =>
|
|
114
|
+
value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
115
|
+
|
|
116
|
+
/** The bytes WebAuthn signs: authenticatorData ‖ SHA-256(clientDataJSON). */
|
|
117
|
+
async function signedBytesOf(authenticatorData, clientDataJSON) {
|
|
118
|
+
const hash = new Uint8Array(
|
|
119
|
+
await crypto.subtle.digest(CRYPTO_ALGORITHMS.SHA_256, clientDataJSON)
|
|
120
|
+
);
|
|
121
|
+
const out = new Uint8Array(authenticatorData.length + hash.length);
|
|
122
|
+
out.set(authenticatorData, 0);
|
|
123
|
+
out.set(hash, authenticatorData.length);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function verifies(point, signedBytes, rawSignature) {
|
|
128
|
+
const uncompressed = new Uint8Array(1 + COORDINATE_BYTES * 2);
|
|
129
|
+
uncompressed[0] = 0x04;
|
|
130
|
+
uncompressed.set(toBytes(point.x), 1);
|
|
131
|
+
uncompressed.set(toBytes(point.y), 1 + COORDINATE_BYTES);
|
|
132
|
+
try {
|
|
133
|
+
const key = await crypto.subtle.importKey(
|
|
134
|
+
'raw',
|
|
135
|
+
uncompressed,
|
|
136
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
137
|
+
false,
|
|
138
|
+
['verify']
|
|
139
|
+
);
|
|
140
|
+
return await crypto.subtle.verify(
|
|
141
|
+
{ name: 'ECDSA', hash: CRYPTO_ALGORITHMS.SHA_256 },
|
|
142
|
+
key,
|
|
143
|
+
rawSignature,
|
|
144
|
+
signedBytes
|
|
145
|
+
);
|
|
146
|
+
} catch {
|
|
147
|
+
return false; // not a point on the curve, or not a key at all
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** An assertion, however the caller happens to hold it. */
|
|
152
|
+
function responseOf(assertion) {
|
|
153
|
+
const response = assertion?.response ?? assertion;
|
|
154
|
+
const { authenticatorData, clientDataJSON, signature } = response ?? {};
|
|
155
|
+
if (!authenticatorData || !clientDataJSON || !signature) {
|
|
156
|
+
throw new WebAuthnIdentityError(
|
|
157
|
+
'an assertion with authenticatorData, clientDataJSON and signature is required',
|
|
158
|
+
{ code: ERROR_CODES.INVALID_INPUT }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
authenticatorData: asBytes(authenticatorData),
|
|
163
|
+
clientDataJSON: asBytes(clientDataJSON),
|
|
164
|
+
signature: asBytes(signature),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The public keys that could have produced this assertion — at most two, each
|
|
170
|
+
* checked against the signature.
|
|
171
|
+
*
|
|
172
|
+
* @param {PublicKeyCredential|{authenticatorData: BufferSource, clientDataJSON: BufferSource, signature: BufferSource}} assertion
|
|
173
|
+
* @returns {Promise<Array<{x: bigint, y: bigint}>>}
|
|
174
|
+
*/
|
|
175
|
+
export async function recoverPublicKeyCandidates(assertion) {
|
|
176
|
+
const { authenticatorData, clientDataJSON, signature } =
|
|
177
|
+
responseOf(assertion);
|
|
178
|
+
const raw =
|
|
179
|
+
signature.length === P1363_SIGNATURE_LENGTH
|
|
180
|
+
? signature
|
|
181
|
+
: derToRawSignature(signature);
|
|
182
|
+
const r = toBigInt(raw.slice(0, COORDINATE_BYTES));
|
|
183
|
+
const s = toBigInt(raw.slice(COORDINATE_BYTES));
|
|
184
|
+
if (r <= 0n || r >= N || s <= 0n || s >= N) {
|
|
185
|
+
throw new WebAuthnIdentityError(
|
|
186
|
+
'the signature is not an ES256 signature, so no key can be recovered from it',
|
|
187
|
+
{ code: ERROR_CODES.INVALID_INPUT }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const signedBytes = await signedBytesOf(authenticatorData, clientDataJSON);
|
|
192
|
+
const digest = new Uint8Array(
|
|
193
|
+
await crypto.subtle.digest(CRYPTO_ALGORITHMS.SHA_256, signedBytes)
|
|
194
|
+
);
|
|
195
|
+
const e = toBigInt(digest);
|
|
196
|
+
|
|
197
|
+
// R has x = r; both parities of y are candidates. Q = r⁻¹(sR − eG).
|
|
198
|
+
const y = squareRoot(mod(r * r * r - 3n * r + B));
|
|
199
|
+
if (y === null) return [];
|
|
200
|
+
const rInverse = inverse(r, N);
|
|
201
|
+
const candidates = [];
|
|
202
|
+
for (const candidateY of [y, mod(P - y)]) {
|
|
203
|
+
const point = multiply(
|
|
204
|
+
add(multiply({ x: r, y: candidateY }, s), multiply(G, N - mod(e, N))),
|
|
205
|
+
rInverse
|
|
206
|
+
);
|
|
207
|
+
if (point === null) continue;
|
|
208
|
+
if (await verifies(point, signedBytes, raw)) candidates.push(point);
|
|
209
|
+
}
|
|
210
|
+
return candidates;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The public key behind two assertions of the same credential.
|
|
215
|
+
*
|
|
216
|
+
* One assertion leaves two possibilities; a second one, over another
|
|
217
|
+
* challenge, leaves one. Both must come from the same passkey — pass the
|
|
218
|
+
* credential id of the first to the second `navigator.credentials.get()`.
|
|
219
|
+
*
|
|
220
|
+
* @param {*} first
|
|
221
|
+
* @param {*} second
|
|
222
|
+
* @returns {Promise<{x: Uint8Array, y: Uint8Array}>} coordinates, as
|
|
223
|
+
* `WebAuthnDIDProvider.createDID` takes them
|
|
224
|
+
*/
|
|
225
|
+
export async function recoverPublicKey(first, second) {
|
|
226
|
+
const [a, b] = await Promise.all([
|
|
227
|
+
recoverPublicKeyCandidates(first),
|
|
228
|
+
recoverPublicKeyCandidates(second),
|
|
229
|
+
]);
|
|
230
|
+
const shared = a.filter((point) =>
|
|
231
|
+
b.some((other) => other.x === point.x && other.y === point.y)
|
|
232
|
+
);
|
|
233
|
+
if (shared.length !== 1) {
|
|
234
|
+
throw new WebAuthnIdentityError(
|
|
235
|
+
shared.length === 0
|
|
236
|
+
? 'the two assertions do not share a public key — they are not the same credential'
|
|
237
|
+
: 'the two assertions did not settle on one public key',
|
|
238
|
+
{ code: ERROR_CODES.WEBAUTHN_VERIFICATION_FAILED }
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return { x: toBytes(shared[0].x), y: toBytes(shared[0].y) };
|
|
242
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Restore
|
|
3
|
+
* @description
|
|
4
|
+
* Restore an identity on a device that has stored nothing (#61).
|
|
5
|
+
*
|
|
6
|
+
* Two touches of the same passkey are all it takes. The first is asked with
|
|
7
|
+
* user verification and with the PRF input every device can compute
|
|
8
|
+
* (`prf-input.js`), so the authenticator returns the same secret it returns on
|
|
9
|
+
* the device the identity was made on. The second signs another challenge, and
|
|
10
|
+
* the two signatures together give back the credential's public key
|
|
11
|
+
* (`recovery.js`) — which an assertion does not carry, and which nothing here
|
|
12
|
+
* had a way to learn without largeBlob or a copy in storage.
|
|
13
|
+
*
|
|
14
|
+
* From those two: the DID, and the signing key derived from the PRF output
|
|
15
|
+
* with the DID mixed in — the same pair the original device holds.
|
|
16
|
+
*
|
|
17
|
+
* Nothing stands in for a missing PRF. A device whose authenticator cannot
|
|
18
|
+
* evaluate it gets a refusal, because the alternative is an identity that
|
|
19
|
+
* silently differs from the one the database knows.
|
|
20
|
+
*/
|
|
21
|
+
import { logger } from '@libp2p/logger';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
ERROR_CODES,
|
|
25
|
+
PrfUnavailableError,
|
|
26
|
+
WebAuthnIdentityError,
|
|
27
|
+
} from '../errors.js';
|
|
28
|
+
import { deriveSigningKeyBytes } from '../keystore/derived-signing-key.js';
|
|
29
|
+
import { KEY_TYPES } from '../constants.js';
|
|
30
|
+
import { buildCredentialRequestOptions } from './config.js';
|
|
31
|
+
import { prfInputForRelyingParty } from './prf-input.js';
|
|
32
|
+
import { WebAuthnDIDProvider } from './provider.js';
|
|
33
|
+
import { recoverPublicKey } from './recovery.js';
|
|
34
|
+
|
|
35
|
+
const log = logger('orbitdb-identity-provider-webauthn-did:restore');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Ask the authenticator in front of this device who it is.
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} [options]
|
|
41
|
+
* @param {string} [options.rpId] - Relying party id; the current host by default.
|
|
42
|
+
* @param {number} [options.timeout=120000] - Per touch, in ms. Generous on
|
|
43
|
+
* purpose: an NFC key has to be found, held and read.
|
|
44
|
+
* @param {string} [options.signingKeyType='secp256k1'] - As the keystore wants it.
|
|
45
|
+
* @param {(step: {touch: number, of: number}) => void} [options.onTouch] -
|
|
46
|
+
* Called before each ceremony, so an application can say which touch this is.
|
|
47
|
+
* @returns {Promise<{did: string, publicKey: {x: Uint8Array, y: Uint8Array},
|
|
48
|
+
* credentialId: Uint8Array, signingKey: Uint8Array, prfInput: Uint8Array}>}
|
|
49
|
+
*/
|
|
50
|
+
export async function restoreIdentityFromAuthenticator({
|
|
51
|
+
rpId,
|
|
52
|
+
timeout = 120_000,
|
|
53
|
+
signingKeyType = KEY_TYPES.SECP256K1,
|
|
54
|
+
onTouch,
|
|
55
|
+
} = {}) {
|
|
56
|
+
if (typeof navigator === 'undefined' || !navigator.credentials?.get) {
|
|
57
|
+
throw new WebAuthnIdentityError('WebAuthn is not available here', {
|
|
58
|
+
code: ERROR_CODES.WEBAUTHN_NOT_SUPPORTED,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const party = rpId ?? globalThis.location?.hostname;
|
|
63
|
+
const prfInput = await prfInputForRelyingParty(party);
|
|
64
|
+
|
|
65
|
+
// Touch one: discoverable, so the device does not have to know which
|
|
66
|
+
// credential to ask for — it has nothing stored, that is the premise.
|
|
67
|
+
onTouch?.({ touch: 1, of: 2 });
|
|
68
|
+
const first = await navigator.credentials.get(
|
|
69
|
+
buildCredentialRequestOptions({
|
|
70
|
+
rpId: party,
|
|
71
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
72
|
+
userVerification: 'required',
|
|
73
|
+
timeout,
|
|
74
|
+
extensions: { prf: { eval: { first: prfInput } } },
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
if (!first) {
|
|
78
|
+
throw new WebAuthnIdentityError('no credential was offered', {
|
|
79
|
+
code: ERROR_CODES.WEBAUTHN_AUTHENTICATION_FAILED,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const prfOutput = first.getClientExtensionResults?.()?.prf?.results?.first;
|
|
84
|
+
if (!prfOutput) {
|
|
85
|
+
// No fallback: an identity derived from anything else is not this one.
|
|
86
|
+
throw new PrfUnavailableError(
|
|
87
|
+
'the authenticator returned no PRF output, so the signing key cannot be derived'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Touch two: the same credential, another challenge. Only the signature
|
|
92
|
+
// matters, so this asks for no more verification than the key insists on.
|
|
93
|
+
onTouch?.({ touch: 2, of: 2 });
|
|
94
|
+
const second = await navigator.credentials.get(
|
|
95
|
+
buildCredentialRequestOptions({
|
|
96
|
+
rpId: party,
|
|
97
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
98
|
+
userVerification: 'preferred',
|
|
99
|
+
timeout,
|
|
100
|
+
credentialId: first.rawId,
|
|
101
|
+
discoverableCredentials: false,
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const publicKey = await recoverPublicKey(first, second);
|
|
106
|
+
const did = await WebAuthnDIDProvider.createDID({ publicKey });
|
|
107
|
+
const signingKey = await deriveSigningKeyBytes(
|
|
108
|
+
new Uint8Array(prfOutput),
|
|
109
|
+
did,
|
|
110
|
+
signingKeyType
|
|
111
|
+
);
|
|
112
|
+
log('restored %s from the authenticator alone', did);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
did,
|
|
116
|
+
publicKey,
|
|
117
|
+
credentialId: new Uint8Array(first.rawId),
|
|
118
|
+
signingKey,
|
|
119
|
+
prfInput,
|
|
120
|
+
};
|
|
121
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -430,6 +430,52 @@ export function createVerificationResult(
|
|
|
430
430
|
|
|
431
431
|
export { VerificationUtils };
|
|
432
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Restoring on a device that has stored nothing (#61).
|
|
435
|
+
*/
|
|
436
|
+
export interface RestoredIdentity {
|
|
437
|
+
did: string;
|
|
438
|
+
publicKey: { x: Uint8Array; y: Uint8Array };
|
|
439
|
+
credentialId: Uint8Array;
|
|
440
|
+
signingKey: Uint8Array;
|
|
441
|
+
prfInput: Uint8Array;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export interface RestoreIdentityOptions {
|
|
445
|
+
rpId?: string;
|
|
446
|
+
timeout?: number;
|
|
447
|
+
signingKeyType?: string;
|
|
448
|
+
onTouch?: (step: { touch: number; of: number }) => void;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export const PRF_INPUT_INFO: string;
|
|
452
|
+
|
|
453
|
+
export function prfInputForRelyingParty(rpId?: string): Promise<Uint8Array>;
|
|
454
|
+
|
|
455
|
+
export function recoverPublicKeyCandidates(
|
|
456
|
+
assertion: unknown
|
|
457
|
+
): Promise<Array<{ x: bigint; y: bigint }>>;
|
|
458
|
+
|
|
459
|
+
export function recoverPublicKey(
|
|
460
|
+
first: unknown,
|
|
461
|
+
second: unknown
|
|
462
|
+
): Promise<{ x: Uint8Array; y: Uint8Array }>;
|
|
463
|
+
|
|
464
|
+
export function restoreIdentityFromAuthenticator(
|
|
465
|
+
options?: RestoreIdentityOptions
|
|
466
|
+
): Promise<RestoredIdentity>;
|
|
467
|
+
|
|
468
|
+
export function deriveSigningKeyBytes(
|
|
469
|
+
seed: Uint8Array,
|
|
470
|
+
did: string,
|
|
471
|
+
keyType?: string
|
|
472
|
+
): Promise<Uint8Array>;
|
|
473
|
+
|
|
474
|
+
export function getPrfOutput(
|
|
475
|
+
credential: Record<string, unknown>,
|
|
476
|
+
options?: { rpId?: string }
|
|
477
|
+
): Promise<Uint8Array | null>;
|
|
478
|
+
|
|
433
479
|
declare const defaultExport: {
|
|
434
480
|
WebAuthnDIDProvider: typeof WebAuthnDIDProvider;
|
|
435
481
|
OrbitDBWebAuthnIdentityProvider: typeof OrbitDBWebAuthnIdentityProvider;
|