@le-space/orbitdb-identity-provider-webauthn-did 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.0
6
+
7
+ ### Breaking
8
+
9
+ - The varsig signing context moves into the signed payload, which **changes the
10
+ bytes a credential signs**. Entries and identities produced by 0.4.x and
11
+ earlier no longer verify, and peers running different major versions cannot
12
+ replicate with each other — the whole set has to move together.
13
+
14
+ The challenge used to be `SHA-256(label ‖ payload)`. That separated the three
15
+ signing contexts, but it meant the challenge was no longer the hash of the
16
+ payload, and the varsig WebAuthn draft has a verifier re-hash the payload and
17
+ compare it against the challenge (step 5). Nothing outside this package could
18
+ reproduce that value.
19
+
20
+ The label itself is not the problem: one credential signs an identity, a public
21
+ key and an oplog entry, and a signature over one must not be replayable as a
22
+ signature over another. That separation cannot live in the varsig header
23
+ either, because a WebAuthn signature only covers
24
+ `authenticatorData ‖ SHA-256(clientDataJSON)` — anything carried beside the
25
+ challenge is unauthenticated. So the context moves into the signed bytes:
26
+ `bindContext()` frames it length-prefixed ahead of the payload, and the
27
+ challenge becomes a plain SHA-256 multihash of those bytes. Both properties
28
+ now hold — the three contexts still yield different challenges, and a spec
29
+ verifier can reproduce the digest.
30
+
31
+ The length prefix is load-bearing. Without it, context `a` with payload `bc`
32
+ and context `ab` with payload `c` are the same bytes, so a chosen payload
33
+ could absorb the label and sidestep the separation.
34
+
35
+ This is a bet on a draft: ChainAgnostic/varsig#11 is still open with
36
+ `webauthn-varsig-header = TODO`. If the draft settles differently, the wire
37
+ format changes again.
38
+
39
+ - `iso-webauthn-varsig` moves to 0.3.0, which corrects the Ed25519 curve code in
40
+ the varsig header from `0xed01` to `0xed`. `0xed01` is how the multicodec
41
+ `ed25519-pub` is written in prose — but that is its _varint encoding_, and
42
+ using it as the code meant emitting 60673, which no other implementation reads
43
+ as Ed25519. Ed25519 varsigs written by earlier versions do not decode.
44
+ **P-256 bytes are unchanged by this second break**, so P-256 deployments are
45
+ affected only by the challenge change above.
46
+
47
+ ### Migration
48
+
49
+ Nothing carries over automatically. Re-register credentials and re-create any
50
+ database whose entries were signed under 0.4.x, and upgrade every peer —
51
+ including relays that verify identities — in one step rather than gradually.
52
+
5
53
  ## 0.4.2
6
54
 
7
55
  ### Fixed
package/README.md CHANGED
@@ -305,6 +305,63 @@ Practical implication:
305
305
 
306
306
  - If you create a passkey on one browser profile and later open the app in a fresh profile, the passkey may still exist in the platform passkey manager, but the app can only reconstruct the OrbitDB identity if it can recover metadata from `largeBlob` or some other persisted mapping.
307
307
 
308
+ ## Upstream Packages and Temporary Forks
309
+
310
+ This package builds on the [`iso-repo`](https://github.com/hugomrdias/iso-repo)
311
+ toolkit by **[Hugo Dias](https://github.com/hugomrdias)** — `iso-base`,
312
+ `iso-did` and `iso-passkeys` do the WebAuthn parsing, DID encoding and base
313
+ conversion that this provider is built on top of. It is excellent work and this
314
+ package would be considerably larger without it. Thank you.
315
+
316
+ Four of the dependencies resolve to `@le-space` builds rather than the
317
+ published originals. Two different reasons, and they should not be confused:
318
+
319
+ ### Temporary forks
320
+
321
+ Published only to unblock this package, and meant to disappear. Each carries a
322
+ small delta that belongs upstream.
323
+
324
+ | Dependency | Upstream | Why it is forked |
325
+ | -------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
326
+ | `iso-passkeys` | [iso-passkeys](https://github.com/hugomrdias/iso-repo/tree/main/packages/iso-passkeys) | Re-exports `parseAttestationObject` and `unwrapEC2Signature`, which already exist upstream but are not part of the public API |
327
+ | `iso-base` | [iso-base](https://github.com/hugomrdias/iso-repo/tree/main/packages/iso-base) | Kept in step with the forks above |
328
+ | `iso-did` | [iso-did](https://github.com/hugomrdias/iso-repo/tree/main/packages/iso-did) | Kept in step with the forks above |
329
+
330
+ The `iso-passkeys` delta is two lines and purely additive — nothing is changed
331
+ or removed, two existing internal functions are simply exported. It is proposed
332
+ upstream as
333
+ [hugomrdias/iso-repo#543](https://github.com/hugomrdias/iso-repo/pull/543); once
334
+ that lands, these three forks and their entries in `pnpm.overrides` can go. The
335
+ remaining entries in that block — `iso-web`, `conf>ajv`, `ajv>fast-uri` — are
336
+ unrelated and stay.
337
+
338
+ ### Not a fork
339
+
340
+ `iso-webauthn-varsig` is a **new** package rather than a modified copy of an
341
+ existing one. It implements WebAuthn varsig signing for OrbitDB oplog entries
342
+ and has no upstream equivalent. It follows `iso-repo` conventions and lives in
343
+ the same lineage, which is why it is named the way it is.
344
+
345
+ It follows the non-recursive varsig layout, which shipped in 0.2.0.
346
+
347
+ One thing is still open, and it is on the spec side rather than here: the
348
+ WebAuthn varsig header. `webauthn-varsig-header` is `TODO` in
349
+ [ChainAgnostic/varsig#11](https://github.com/ChainAgnostic/varsig/pull/11), so
350
+ the `0x300001` marker this package writes is a private-use codepoint chosen
351
+ here rather than an allocated one. Expect the wire format to change once that
352
+ is settled.
353
+
354
+ ### Not forked at all
355
+
356
+ `iso-web` appears in `pnpm.overrides` but resolves to the genuine upstream
357
+ package. The entry only pins a version.
358
+
359
+ ### Licensing
360
+
361
+ All `iso-repo` packages are MIT, © Hugo Dias. The forks keep the original
362
+ `license` and `author` fields, so authorship travels with them; only the
363
+ package name changes.
364
+
308
365
  ## License
309
366
 
310
367
  MIT. See `LICENSE`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/orbitdb-identity-provider-webauthn-did",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "WebAuthn-based DID identity provider for OrbitDB for hardware-secured wallets and biometric Passkey authentication",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -90,7 +90,7 @@
90
90
  "iso-did": "npm:@le-space/iso-did@2.1.2",
91
91
  "iso-passkeys": "npm:@le-space/iso-passkeys",
92
92
  "iso-web": "^3.1.2",
93
- "iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.2.0",
93
+ "iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.3.0",
94
94
  "multiformats": "^14.0.0"
95
95
  },
96
96
  "devDependencies": {
@@ -143,7 +143,7 @@
143
143
  "iso-base": "npm:@le-space/iso-base",
144
144
  "iso-did": "npm:@le-space/iso-did@2.1.2",
145
145
  "iso-web": "^3.1.2",
146
- "iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.2.0",
146
+ "iso-webauthn-varsig": "npm:@le-space/iso-webauthn-varsig@^0.3.0",
147
147
  "conf>ajv": "^8.18.0",
148
148
  "ajv>fast-uri": "^3.1.4"
149
149
  },
@@ -12,7 +12,12 @@ import {
12
12
  verifyP256Signature,
13
13
  verifyWebAuthnAssertion,
14
14
  } from 'iso-webauthn-varsig';
15
- import { buildChallengeBytes, toArrayBuffer, toBytes } from './utils.js';
15
+ import {
16
+ bindContext,
17
+ buildChallengeBytes,
18
+ toArrayBuffer,
19
+ toBytes,
20
+ } from './utils.js';
16
21
  import { buildCredentialRequestOptions } from '../webauthn/config.js';
17
22
  import { KEY_TYPES, WEBAUTHN_CLIENT_DATA_TYPES } from '../constants.js';
18
23
  import {
@@ -44,7 +49,10 @@ async function runWebAuthnAssertionForPayload(
44
49
  ) {
45
50
  const rpId = window.location.hostname;
46
51
  const origin = window.location.origin;
47
- const challengeBytes = await buildChallengeBytes(domainLabel, payloadBytes);
52
+ // The context is framed into the signed bytes, not into the challenge
53
+ // preimage, so the challenge stays a plain hash of what was signed.
54
+ const signedBytes = bindContext(domainLabel, payloadBytes);
55
+ const challengeBytes = await buildChallengeBytes(signedBytes);
48
56
 
49
57
  if (isTestMode()) {
50
58
  return {
@@ -208,8 +216,7 @@ async function verifyVarsigForPayload(
208
216
  }
209
217
  const clientData = parseClientDataJSON(decoded.clientDataJSON);
210
218
  const expectedChallenge = await buildChallengeBytes(
211
- domainLabel,
212
- payloadBytes
219
+ bindContext(domainLabel, payloadBytes)
213
220
  );
214
221
  const expectedChallengeEncoded = bytesToBase64url(expectedChallenge);
215
222
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Byte and challenge helpers for varsig signing.
3
3
  */
4
- import { concat } from 'iso-webauthn-varsig';
4
+ import { concat, varintEncode } from 'iso-webauthn-varsig';
5
5
 
6
6
  const encoder = new TextEncoder();
7
7
 
@@ -27,18 +27,51 @@ function toBytes(data) {
27
27
  }
28
28
 
29
29
  /**
30
- * Build a WebAuthn challenge from a domain label and payload.
31
- * @param {string} domainLabel - Domain label prefix.
30
+ * Frame a payload with the context it is signed for.
31
+ *
32
+ * The same credential signs three different things — an identity, a public key
33
+ * and an oplog entry — and a signature over one must not be reusable as a
34
+ * signature over another. The separation has to live inside the bytes that get
35
+ * hashed: the WebAuthn signature covers `authenticatorData ‖ SHA-256(clientDataJSON)`
36
+ * and nothing else, so a context field carried alongside the varsig would not
37
+ * be bound to the signature at all.
38
+ *
39
+ * It used to be mixed into the challenge preimage instead
40
+ * (`SHA-256(label ‖ payload)`), which separated the domains but meant the
41
+ * challenge was no longer the hash of the payload — so verifiers following the
42
+ * varsig spec could never reproduce it.
43
+ *
44
+ * @param {string} context - Domain label, e.g. 'orbitdb-entry:'.
32
45
  * @param {Uint8Array} payloadBytes - Payload bytes.
33
- * @returns {Promise<Uint8Array>} SHA-256 digest.
46
+ * @returns {Uint8Array} The bytes that are signed over.
34
47
  */
35
- async function buildChallengeBytes(domainLabel, payloadBytes) {
36
- const domain = encoder.encode(domainLabel);
37
- const hash = await crypto.subtle.digest(
38
- 'SHA-256',
39
- concat([domain, payloadBytes])
48
+ function bindContext(context, payloadBytes) {
49
+ const contextBytes = encoder.encode(context);
50
+ // Length-prefixed so the frame is unambiguous: without it, a context of
51
+ // 'a' with payload 'bc' and a context of 'ab' with payload 'c' would
52
+ // produce identical bytes.
53
+ return concat([
54
+ varintEncode(contextBytes.length),
55
+ contextBytes,
56
+ payloadBytes,
57
+ ]);
58
+ }
59
+
60
+ /**
61
+ * Build a WebAuthn challenge as a multihash of the payload.
62
+ *
63
+ * Per the varsig WebAuthn draft the challenge carries the payload hash as a
64
+ * multihash, so a verifier can re-hash the payload and compare. `0x12 0x20` is
65
+ * the SHA-256 prefix and its 32-byte length.
66
+ *
67
+ * @param {Uint8Array} payloadBytes - The bytes being signed, context included.
68
+ * @returns {Promise<Uint8Array>} Multihash of the payload.
69
+ */
70
+ async function buildChallengeBytes(payloadBytes) {
71
+ const hash = new Uint8Array(
72
+ await crypto.subtle.digest('SHA-256', payloadBytes)
40
73
  );
41
- return new Uint8Array(hash);
74
+ return concat([new Uint8Array([0x12, 0x20]), hash]);
42
75
  }
43
76
 
44
- export { encoder, toArrayBuffer, toBytes, buildChallengeBytes };
77
+ export { encoder, toArrayBuffer, toBytes, bindContext, buildChallengeBytes };