@le-space/orbitdb-identity-provider-webauthn-did 0.5.2 → 0.5.3
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 +25 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/keystore/derived-signing-key.js +50 -12
- package/src/keystore/provider.js +18 -0
- package/src/varsig/identity.js +5 -20
- package/types/keystore.d.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.3
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
|
|
9
|
+
- **Varsig identities could still be forged through
|
|
10
|
+
`createWebAuthnVarsigIdentities()`**
|
|
11
|
+
([GHSA-326j-4cc3-4rrg](https://github.com/Le-Space/orbitdb-identity-provider-webauthn-did/security/advisories/GHSA-326j-4cc3-4rrg),
|
|
12
|
+
left open by 0.5.2). 0.5.2 bound a varsig identity's id to the key that
|
|
13
|
+
signs for it, but only in `verifyVarsigIdentity`. The verifier returned by
|
|
14
|
+
`createWebAuthnVarsigIdentities()` — what OrbitDB's access controller
|
|
15
|
+
calls when an app builds its identities with it, as the varsig demo does —
|
|
16
|
+
still checked the two signatures against whatever key the identity
|
|
17
|
+
carried, so a second user's passkey on the same origin could sign someone
|
|
18
|
+
else's DID and be accepted as a writer under it. It now delegates to
|
|
19
|
+
`verifyVarsigIdentity`. orbitdb-relay is not affected: it registers
|
|
20
|
+
`verifyVarsigIdentity` directly.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `signingKeyType: 'Ed25519'` derives the OrbitDB signing key as Ed25519
|
|
25
|
+
instead of secp256k1 — still from the passkey's PRF output (HKDF-SHA256,
|
|
26
|
+
domain-separated by DID and key type), so one passkey still yields one
|
|
27
|
+
identity document on every device. The default stays secp256k1, and a
|
|
28
|
+
keystore that already holds a key for the DID keeps it.
|
|
29
|
+
|
|
5
30
|
## 0.5.2
|
|
6
31
|
|
|
7
32
|
### Security
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ This package provides:
|
|
|
10
10
|
- A standalone WebAuthn toolkit export (`@le-space/orbitdb-identity-provider-webauthn-did/standalone`) for reuse outside OrbitDB identity wiring.
|
|
11
11
|
- A keystore helper export (`@le-space/orbitdb-identity-provider-webauthn-did/keystore`) for encrypted keystore utilities and provider wiring.
|
|
12
12
|
- **WebAuthn-Varsig**: No insecure OrbitDB keystore at all. Each entry is signed by WebAuthn (varsig envelope), so keys never leave the authenticator, one Passkey (WebAuthn) prompt per write.
|
|
13
|
+
- **Passkey DID with a derived signing key** (the default): the DID is the credential's own P-256 key, and OrbitDB signs entries with a key derived from the passkey's PRF output (HKDF-SHA256, domain-separated by the DID) — secp256k1 by default, Ed25519 with `signingKeyType: 'Ed25519'`. The same passkey yields the same identity document on every device. That key lives in OrbitDB's keystore and is **not** covered by `encryptKeystore`; without PRF the keystore generates one, which then stays on that device.
|
|
13
14
|
- **Keystore-based DID**: Generates an Ed25519/secp256k1 keystore keypair for OrbitDB signing in browser memory. When `encryptKeystore` is enabled, the private key is encrypted with AES-GCM and only rehydrated in memory after a WebAuthn unlock (PRF, largeBlob, or hmac-secret).
|
|
14
15
|
|
|
15
16
|
## Current WebAuthn Model
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/orbitdb-identity-provider-webauthn-did",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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",
|
|
@@ -21,12 +21,19 @@
|
|
|
21
21
|
* Needs no change to `@orbitdb/core` — it only has to happen before
|
|
22
22
|
* `getKey(id)` is called, which `getId()` is.
|
|
23
23
|
*
|
|
24
|
+
* The key is secp256k1, the type `createKey` would have produced, unless the
|
|
25
|
+
* provider is created with `signingKeyType: 'Ed25519'`. OrbitDB's keystore
|
|
26
|
+
* reads the type from the raw key, so either signs and verifies unchanged.
|
|
27
|
+
*
|
|
24
28
|
* See issue #18, option 1.
|
|
25
29
|
*/
|
|
26
30
|
import { logger } from '@libp2p/logger';
|
|
27
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
generateKeyPairFromSeed,
|
|
33
|
+
privateKeyFromRaw,
|
|
34
|
+
} from '@libp2p/crypto/keys';
|
|
28
35
|
|
|
29
|
-
import { CRYPTO_ALGORITHMS } from '../constants.js';
|
|
36
|
+
import { CRYPTO_ALGORITHMS, KEY_TYPES } from '../constants.js';
|
|
30
37
|
import { buildCredentialRequestOptions } from '../webauthn/config.js';
|
|
31
38
|
|
|
32
39
|
const log = logger('orbitdb-identity-provider-webauthn-did:derived-key');
|
|
@@ -64,17 +71,35 @@ async function hkdf(seed, info) {
|
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
/**
|
|
67
|
-
* Turn a PRF seed into a
|
|
74
|
+
* Turn a PRF seed into a private key of the requested type, as the raw bytes
|
|
75
|
+
* `keystore.addKey` takes.
|
|
76
|
+
*
|
|
77
|
+
* secp256k1: values outside the curve order are invalid. That is vanishingly
|
|
78
|
+
* unlikely for a 256-bit hash, but retrying with a counter keeps the
|
|
79
|
+
* derivation total and deterministic rather than probabilistic.
|
|
68
80
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
81
|
+
* Ed25519: every 32 bytes are a valid seed, so there is nothing to retry. The
|
|
82
|
+
* key type is part of its info string, so the two derivations never share
|
|
83
|
+
* key material, even for the same passkey and DID.
|
|
72
84
|
*
|
|
73
85
|
* @param {Uint8Array} seed - PRF output.
|
|
74
86
|
* @param {string} did - Identity DID, mixed in for domain separation.
|
|
75
|
-
* @
|
|
87
|
+
* @param {string} [keyType='secp256k1'] - 'secp256k1' or 'Ed25519'.
|
|
88
|
+
* @returns {Promise<Uint8Array>} The 32-byte secp256k1 scalar, or the 64-byte
|
|
89
|
+
* Ed25519 private key (seed ‖ public key) libp2p expects.
|
|
76
90
|
*/
|
|
77
|
-
export async function deriveSigningKeyBytes(
|
|
91
|
+
export async function deriveSigningKeyBytes(
|
|
92
|
+
seed,
|
|
93
|
+
did,
|
|
94
|
+
keyType = KEY_TYPES.SECP256K1
|
|
95
|
+
) {
|
|
96
|
+
if (keyType === KEY_TYPES.ED25519) {
|
|
97
|
+
const edSeed = await hkdf(seed, `${DERIVATION_INFO}:ed25519:${did}`);
|
|
98
|
+
return (await generateKeyPairFromSeed(KEY_TYPES.ED25519, edSeed)).raw;
|
|
99
|
+
}
|
|
100
|
+
if (keyType !== KEY_TYPES.SECP256K1) {
|
|
101
|
+
throw new Error(`unsupported signing key type: ${keyType}`);
|
|
102
|
+
}
|
|
78
103
|
for (let counter = 0; counter < 256; counter++) {
|
|
79
104
|
const candidate = await hkdf(seed, `${DERIVATION_INFO}:${did}:${counter}`);
|
|
80
105
|
try {
|
|
@@ -158,6 +183,7 @@ export async function getPrfOutput(credential, { rpId } = {}) {
|
|
|
158
183
|
* @param {string} params.did - Identity DID.
|
|
159
184
|
* @param {Object} params.credential - Stored WebAuthn credential info.
|
|
160
185
|
* @param {string} [params.rpId] - Relying party id.
|
|
186
|
+
* @param {string} [params.keyType='secp256k1'] - Type of key to derive.
|
|
161
187
|
* @returns {Promise<'derived'|'existing'|'unavailable'>} What happened.
|
|
162
188
|
*/
|
|
163
189
|
export async function ensureDerivedSigningKey({
|
|
@@ -165,12 +191,24 @@ export async function ensureDerivedSigningKey({
|
|
|
165
191
|
did,
|
|
166
192
|
credential,
|
|
167
193
|
rpId,
|
|
194
|
+
keyType = KEY_TYPES.SECP256K1,
|
|
168
195
|
}) {
|
|
169
196
|
if (!keystore || !did || !credential) return 'unavailable';
|
|
170
197
|
|
|
171
198
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
const existing = await keystore.getKey(did);
|
|
200
|
+
if (existing) {
|
|
201
|
+
if (existing.type !== keyType) {
|
|
202
|
+
// Still left alone: a key of the other type has history behind it,
|
|
203
|
+
// and replacing it would mint a second document for the DID.
|
|
204
|
+
log(
|
|
205
|
+
'keystore already holds a %s key for this DID, not the requested %s; leaving it alone',
|
|
206
|
+
existing.type,
|
|
207
|
+
keyType
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
log('keystore already holds a key for this DID; leaving it alone');
|
|
211
|
+
}
|
|
174
212
|
return 'existing';
|
|
175
213
|
}
|
|
176
214
|
} catch (error) {
|
|
@@ -187,9 +225,9 @@ export async function ensureDerivedSigningKey({
|
|
|
187
225
|
}
|
|
188
226
|
|
|
189
227
|
try {
|
|
190
|
-
const privateKey = await deriveSigningKeyBytes(seed, did);
|
|
228
|
+
const privateKey = await deriveSigningKeyBytes(seed, did, keyType);
|
|
191
229
|
await keystore.addKey(did, { privateKey });
|
|
192
|
-
log('seeded the keystore with a PRF-derived signing key');
|
|
230
|
+
log('seeded the keystore with a PRF-derived %s signing key', keyType);
|
|
193
231
|
return 'derived';
|
|
194
232
|
} catch (error) {
|
|
195
233
|
log('failed to seed the derived key: %s', error.message);
|
package/src/keystore/provider.js
CHANGED
|
@@ -50,6 +50,11 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
50
50
|
* @param {string} [options.keystoreKeyType='secp256k1'] - Keystore key type.
|
|
51
51
|
* @param {boolean} [options.encryptKeystore=false] - Encrypt keystore at rest.
|
|
52
52
|
* @param {string} [options.keystoreEncryptionMethod='prf'] - Encryption method.
|
|
53
|
+
* @param {boolean} [options.deriveSigningKeyFromPrf=true] - Derive the OrbitDB
|
|
54
|
+
* signing key from the passkey's PRF output instead of a random one.
|
|
55
|
+
* @param {string} [options.signingKeyType='secp256k1'] - Type of that derived
|
|
56
|
+
* key: 'secp256k1' or 'Ed25519'. `useKeystoreDID` has its own
|
|
57
|
+
* `keystoreKeyType`.
|
|
53
58
|
*/
|
|
54
59
|
constructor({
|
|
55
60
|
webauthnCredential,
|
|
@@ -59,8 +64,15 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
59
64
|
encryptKeystore = false,
|
|
60
65
|
keystoreEncryptionMethod = KEYSTORE_ENCRYPTION_METHODS.PRF,
|
|
61
66
|
deriveSigningKeyFromPrf = true,
|
|
67
|
+
signingKeyType = KEY_TYPES.SECP256K1,
|
|
62
68
|
}) {
|
|
69
|
+
if (![KEY_TYPES.SECP256K1, KEY_TYPES.ED25519].includes(signingKeyType)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`signingKeyType must be '${KEY_TYPES.SECP256K1}' or '${KEY_TYPES.ED25519}', not '${signingKeyType}'`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
63
74
|
this.credential = webauthnCredential;
|
|
75
|
+
this.signingKeyType = signingKeyType;
|
|
64
76
|
// Costs one extra assertion the first time an identity is created on a
|
|
65
77
|
// device. Set false to keep a keystore-generated key instead.
|
|
66
78
|
this.deriveSigningKeyFromPrf = deriveSigningKeyFromPrf;
|
|
@@ -118,6 +130,7 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
118
130
|
keystore,
|
|
119
131
|
did,
|
|
120
132
|
credential: this.credential,
|
|
133
|
+
keyType: this.signingKeyType,
|
|
121
134
|
});
|
|
122
135
|
identityLog('Derived signing key: %s', outcome);
|
|
123
136
|
}
|
|
@@ -652,6 +665,8 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
652
665
|
keystoreKeyType = KEY_TYPES.SECP256K1,
|
|
653
666
|
encryptKeystore = false,
|
|
654
667
|
keystoreEncryptionMethod = KEYSTORE_ENCRYPTION_METHODS.PRF,
|
|
668
|
+
deriveSigningKeyFromPrf = true,
|
|
669
|
+
signingKeyType = KEY_TYPES.SECP256K1,
|
|
655
670
|
} = options;
|
|
656
671
|
|
|
657
672
|
identityLog(
|
|
@@ -668,6 +683,8 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
668
683
|
keystoreKeyType,
|
|
669
684
|
encryptKeystore,
|
|
670
685
|
keystoreEncryptionMethod,
|
|
686
|
+
deriveSigningKeyFromPrf,
|
|
687
|
+
signingKeyType,
|
|
671
688
|
});
|
|
672
689
|
|
|
673
690
|
// If encryption is enabled, create and unlock encrypted keystore
|
|
@@ -739,6 +756,7 @@ export class OrbitDBWebAuthnIdentityProvider {
|
|
|
739
756
|
* @param {string} options.keystoreKeyType - Key type for keystore: 'secp256k1' (default) or 'Ed25519'
|
|
740
757
|
* @param {boolean} options.encryptKeystore - If true, encrypts the keystore with WebAuthn-protected secret
|
|
741
758
|
* @param {string} options.keystoreEncryptionMethod - Encryption method: 'largeBlob' or 'hmac-secret'
|
|
759
|
+
* @param {string} [options.signingKeyType] - PRF-derived signing key type: 'secp256k1' (default) or 'Ed25519'
|
|
742
760
|
* @returns {Function} Provider factory for OrbitDB.
|
|
743
761
|
*/
|
|
744
762
|
export function OrbitDBWebAuthnIdentityProviderFunction(options = {}) {
|
package/src/varsig/identity.js
CHANGED
|
@@ -193,26 +193,11 @@ export function createWebAuthnVarsigIdentities(identity, domainLabels = {}) {
|
|
|
193
193
|
return false;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
labels.id
|
|
202
|
-
);
|
|
203
|
-
if (!idValid) return false;
|
|
204
|
-
|
|
205
|
-
const publicKeyPayload = concat([
|
|
206
|
-
identityToVerify.publicKey,
|
|
207
|
-
identityToVerify.signatures.id,
|
|
208
|
-
]);
|
|
209
|
-
|
|
210
|
-
return verifyVarsigForPayload(
|
|
211
|
-
identityToVerify.signatures.publicKey,
|
|
212
|
-
identityToVerify.publicKey,
|
|
213
|
-
publicKeyPayload,
|
|
214
|
-
labels.publicKey
|
|
215
|
-
);
|
|
196
|
+
// The same check OrbitDB's provider registry uses, including the binding
|
|
197
|
+
// of the id to the key that signs for it. Verifying only the two
|
|
198
|
+
// signatures, as this did, accepted a stranger's passkey signing someone
|
|
199
|
+
// else's DID (GHSA-326j-4cc3-4rrg, left open here in 0.5.2).
|
|
200
|
+
return verifyVarsigIdentity(identityToVerify, labels);
|
|
216
201
|
};
|
|
217
202
|
|
|
218
203
|
const getIdentity = async (hash) => {
|
package/types/keystore.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export interface OrbitDBWebAuthnIdentityProviderOptions {
|
|
|
48
48
|
keystoreKeyType?: string;
|
|
49
49
|
encryptKeystore?: boolean;
|
|
50
50
|
keystoreEncryptionMethod?: KeystoreEncryptionMethod;
|
|
51
|
+
/** Derive the OrbitDB signing key from the passkey's PRF output (default true). */
|
|
52
|
+
deriveSigningKeyFromPrf?: boolean;
|
|
53
|
+
/** Type of that derived key (default 'secp256k1'). */
|
|
54
|
+
signingKeyType?: 'secp256k1' | 'Ed25519';
|
|
51
55
|
[key: string]: unknown;
|
|
52
56
|
}
|
|
53
57
|
|