@interop/wallet-core 0.61.0 → 0.65.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/README.md +41 -16
- package/dist/clientAnnex/credentialAnchoredGenesis.d.ts +46 -26
- package/dist/clientAnnex/credentialAnchoredGenesis.d.ts.map +1 -1
- package/dist/clientAnnex/credentialAnchoredGenesis.js +91 -39
- package/dist/clientAnnex/credentialAnchoredGenesis.js.map +1 -1
- package/dist/clientAnnex/establish.d.ts +55 -34
- package/dist/clientAnnex/establish.d.ts.map +1 -1
- package/dist/clientAnnex/establish.js +102 -56
- package/dist/clientAnnex/establish.js.map +1 -1
- package/dist/clientAnnex/forget.d.ts +16 -2
- package/dist/clientAnnex/forget.d.ts.map +1 -1
- package/dist/clientAnnex/forget.js +11 -9
- package/dist/clientAnnex/forget.js.map +1 -1
- package/dist/clientAnnex/forgetLast.d.ts +126 -35
- package/dist/clientAnnex/forgetLast.d.ts.map +1 -1
- package/dist/clientAnnex/forgetLast.js +208 -60
- package/dist/clientAnnex/forgetLast.js.map +1 -1
- package/dist/clientAnnex/gc.d.ts +3 -2
- package/dist/clientAnnex/gc.d.ts.map +1 -1
- package/dist/clientAnnex/gc.js +7 -7
- package/dist/clientAnnex/gc.js.map +1 -1
- package/dist/clientAnnex/heal.d.ts +101 -33
- package/dist/clientAnnex/heal.d.ts.map +1 -1
- package/dist/clientAnnex/heal.js +559 -212
- package/dist/clientAnnex/heal.js.map +1 -1
- package/dist/clientAnnex/index.d.ts +21 -5
- package/dist/clientAnnex/index.d.ts.map +1 -1
- package/dist/clientAnnex/index.js +24 -5
- package/dist/clientAnnex/index.js.map +1 -1
- package/dist/clientAnnex/ladder.d.ts +304 -18
- package/dist/clientAnnex/ladder.d.ts.map +1 -1
- package/dist/clientAnnex/ladder.js +942 -64
- package/dist/clientAnnex/ladder.js.map +1 -1
- package/dist/clientAnnex/ladderAnchored.d.ts +261 -28
- package/dist/clientAnnex/ladderAnchored.d.ts.map +1 -1
- package/dist/clientAnnex/ladderAnchored.js +521 -295
- package/dist/clientAnnex/ladderAnchored.js.map +1 -1
- package/dist/clientAnnex/log.d.ts +193 -38
- package/dist/clientAnnex/log.d.ts.map +1 -1
- package/dist/clientAnnex/log.js +445 -190
- package/dist/clientAnnex/log.js.map +1 -1
- package/dist/clientAnnex/mend.d.ts +12 -6
- package/dist/clientAnnex/mend.d.ts.map +1 -1
- package/dist/clientAnnex/mend.js +26 -12
- package/dist/clientAnnex/mend.js.map +1 -1
- package/dist/clientAnnex/recoveryLadderAnchored.d.ts +38 -11
- package/dist/clientAnnex/recoveryLadderAnchored.d.ts.map +1 -1
- package/dist/clientAnnex/recoveryLadderAnchored.js +177 -55
- package/dist/clientAnnex/recoveryLadderAnchored.js.map +1 -1
- package/dist/clientAnnex/spaceCapability.d.ts +123 -0
- package/dist/clientAnnex/spaceCapability.d.ts.map +1 -0
- package/dist/clientAnnex/spaceCapability.js +152 -0
- package/dist/clientAnnex/spaceCapability.js.map +1 -0
- package/dist/clientAnnex/stages.d.ts +63 -0
- package/dist/clientAnnex/stages.d.ts.map +1 -0
- package/dist/clientAnnex/stages.js +64 -0
- package/dist/clientAnnex/stages.js.map +1 -0
- package/dist/clientAnnex/zcap.d.ts +1 -1
- package/dist/clientAnnex/zcap.d.ts.map +1 -1
- package/dist/clientAnnex/zcap.js +42 -35
- package/dist/clientAnnex/zcap.js.map +1 -1
- package/dist/clients/policy.d.ts +15 -1
- package/dist/clients/policy.d.ts.map +1 -1
- package/dist/clients/policy.js +12 -6
- package/dist/clients/policy.js.map +1 -1
- package/dist/clients/revocation.d.ts +18 -10
- package/dist/clients/revocation.d.ts.map +1 -1
- package/dist/clients/revocation.js +13 -5
- package/dist/clients/revocation.js.map +1 -1
- package/dist/clients/rosterPolicy.d.ts +10 -2
- package/dist/clients/rosterPolicy.d.ts.map +1 -1
- package/dist/clients/rosterPolicy.js +41 -24
- package/dist/clients/rosterPolicy.js.map +1 -1
- package/dist/descriptors/acquire.d.ts.map +1 -1
- package/dist/descriptors/acquire.js +6 -23
- package/dist/descriptors/acquire.js.map +1 -1
- package/dist/descriptors/cipher.d.ts.map +1 -1
- package/dist/descriptors/cipher.js +5 -0
- package/dist/descriptors/cipher.js.map +1 -1
- package/dist/descriptors/errors.d.ts +43 -0
- package/dist/descriptors/errors.d.ts.map +1 -0
- package/dist/descriptors/errors.js +45 -0
- package/dist/descriptors/errors.js.map +1 -0
- package/dist/descriptors/index.d.ts +5 -0
- package/dist/descriptors/index.d.ts.map +1 -1
- package/dist/descriptors/index.js +5 -0
- package/dist/descriptors/index.js.map +1 -1
- package/dist/enrollment/enrollment.d.ts +37 -10
- package/dist/enrollment/enrollment.d.ts.map +1 -1
- package/dist/enrollment/enrollment.js +52 -15
- package/dist/enrollment/enrollment.js.map +1 -1
- package/dist/genesis/accountGenesis.d.ts +42 -11
- package/dist/genesis/accountGenesis.d.ts.map +1 -1
- package/dist/genesis/accountGenesis.js +80 -22
- package/dist/genesis/accountGenesis.js.map +1 -1
- package/dist/genesis/index.d.ts +3 -1
- package/dist/genesis/index.d.ts.map +1 -1
- package/dist/genesis/index.js +3 -1
- package/dist/genesis/index.js.map +1 -1
- package/dist/identity/agents.d.ts +17 -1
- package/dist/identity/agents.d.ts.map +1 -1
- package/dist/identity/agents.js +24 -8
- package/dist/identity/agents.js.map +1 -1
- package/dist/identity/index.d.ts +3 -1
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +3 -1
- package/dist/identity/index.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/keyring/index.d.ts +2 -1
- package/dist/keyring/index.d.ts.map +1 -1
- package/dist/keyring/index.js +2 -1
- package/dist/keyring/index.js.map +1 -1
- package/dist/keyring/record.d.ts.map +1 -1
- package/dist/keyring/record.js +3 -6
- package/dist/keyring/record.js.map +1 -1
- package/dist/keyring/unlockSpace.d.ts +14 -5
- package/dist/keyring/unlockSpace.d.ts.map +1 -1
- package/dist/keyring/unlockSpace.js +31 -36
- package/dist/keyring/unlockSpace.js.map +1 -1
- package/dist/keys/index.d.ts +10 -6
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js +9 -5
- package/dist/keys/index.js.map +1 -1
- package/dist/keys/rosterLogStore.d.ts +12 -11
- package/dist/keys/rosterLogStore.d.ts.map +1 -1
- package/dist/keys/rosterLogStore.js +15 -13
- package/dist/keys/rosterLogStore.js.map +1 -1
- package/dist/keys/rosterStore.d.ts +3 -6
- package/dist/keys/rosterStore.d.ts.map +1 -1
- package/dist/keys/rosterStore.js +10 -9
- package/dist/keys/rosterStore.js.map +1 -1
- package/dist/keys/spaceEpochs.d.ts.map +1 -1
- package/dist/keys/spaceEpochs.js +2 -3
- package/dist/keys/spaceEpochs.js.map +1 -1
- package/dist/keys/userKeyRoster.d.ts +89 -9
- package/dist/keys/userKeyRoster.d.ts.map +1 -1
- package/dist/keys/userKeyRoster.js +262 -19
- package/dist/keys/userKeyRoster.js.map +1 -1
- package/dist/keys/userKeyRosterCascade.d.ts +7 -6
- package/dist/keys/userKeyRosterCascade.d.ts.map +1 -1
- package/dist/keys/userKeyRosterCascade.js +7 -6
- package/dist/keys/userKeyRosterCascade.js.map +1 -1
- package/dist/keys/wasLabelsStore.d.ts +9 -2
- package/dist/keys/wasLabelsStore.d.ts.map +1 -1
- package/dist/keys/wasLabelsStore.js +14 -6
- package/dist/keys/wasLabelsStore.js.map +1 -1
- package/dist/log.d.ts +7 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +6 -1
- package/dist/log.js.map +1 -1
- package/dist/recovery/index.d.ts +12 -8
- package/dist/recovery/index.d.ts.map +1 -1
- package/dist/recovery/index.js +11 -7
- package/dist/recovery/index.js.map +1 -1
- package/dist/recovery/recoveryCode.d.ts +27 -7
- package/dist/recovery/recoveryCode.d.ts.map +1 -1
- package/dist/recovery/recoveryCode.js +18 -6
- package/dist/recovery/recoveryCode.js.map +1 -1
- package/dist/recovery/recoveryDelegation.d.ts +4 -1
- package/dist/recovery/recoveryDelegation.d.ts.map +1 -1
- package/dist/recovery/recoveryDelegation.js +25 -36
- package/dist/recovery/recoveryDelegation.js.map +1 -1
- package/dist/recovery/recoveryWebvh.d.ts +117 -65
- package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
- package/dist/recovery/recoveryWebvh.js +279 -126
- package/dist/recovery/recoveryWebvh.js.map +1 -1
- package/dist/request/classify.d.ts +32 -8
- package/dist/request/classify.d.ts.map +1 -1
- package/dist/request/classify.js +39 -14
- package/dist/request/classify.js.map +1 -1
- package/dist/request/ephemeralExchange.d.ts +1 -6
- package/dist/request/ephemeralExchange.d.ts.map +1 -1
- package/dist/request/ephemeralExchange.js.map +1 -1
- package/dist/request/onboarding.d.ts.map +1 -1
- package/dist/request/onboarding.js +2 -2
- package/dist/request/onboarding.js.map +1 -1
- package/dist/request/parse.d.ts.map +1 -1
- package/dist/request/parse.js +2 -3
- package/dist/request/parse.js.map +1 -1
- package/dist/resourceLog/controller.d.ts +34 -12
- package/dist/resourceLog/controller.d.ts.map +1 -1
- package/dist/resourceLog/controller.js +79 -86
- package/dist/resourceLog/controller.js.map +1 -1
- package/dist/resourceLog/document.d.ts +182 -0
- package/dist/resourceLog/document.d.ts.map +1 -0
- package/dist/resourceLog/document.js +159 -0
- package/dist/resourceLog/document.js.map +1 -0
- package/dist/resourceLog/errors.d.ts +47 -8
- package/dist/resourceLog/errors.d.ts.map +1 -1
- package/dist/resourceLog/errors.js +54 -8
- package/dist/resourceLog/errors.js.map +1 -1
- package/dist/resourceLog/index.d.ts +7 -3
- package/dist/resourceLog/index.d.ts.map +1 -1
- package/dist/resourceLog/index.js +7 -3
- package/dist/resourceLog/index.js.map +1 -1
- package/dist/resourceLog/ladderRungs.d.ts +35 -0
- package/dist/resourceLog/ladderRungs.d.ts.map +1 -0
- package/dist/resourceLog/ladderRungs.js +352 -0
- package/dist/resourceLog/ladderRungs.js.map +1 -0
- package/dist/resourceLog/license.d.ts +42 -17
- package/dist/resourceLog/license.d.ts.map +1 -1
- package/dist/resourceLog/license.js +38 -24
- package/dist/resourceLog/license.js.map +1 -1
- package/dist/space/activity.d.ts +15 -15
- package/dist/space/activity.d.ts.map +1 -1
- package/dist/space/activity.js +15 -15
- package/dist/space/activity.js.map +1 -1
- package/dist/space/collections.d.ts +11 -0
- package/dist/space/collections.d.ts.map +1 -1
- package/dist/space/collections.js +13 -0
- package/dist/space/collections.js.map +1 -1
- package/dist/space/deleteSpace.d.ts +28 -0
- package/dist/space/deleteSpace.d.ts.map +1 -0
- package/dist/space/deleteSpace.js +44 -0
- package/dist/space/deleteSpace.js.map +1 -0
- package/dist/space/errors.d.ts.map +1 -1
- package/dist/space/errors.js +0 -1
- package/dist/space/errors.js.map +1 -1
- package/dist/space/index.d.ts +6 -0
- package/dist/space/index.d.ts.map +1 -1
- package/dist/space/index.js +6 -0
- package/dist/space/index.js.map +1 -1
- package/dist/space/plaintextCollection.d.ts +43 -0
- package/dist/space/plaintextCollection.d.ts.map +1 -0
- package/dist/space/plaintextCollection.js +17 -0
- package/dist/space/plaintextCollection.js.map +1 -0
- package/dist/stages.d.ts +23 -0
- package/dist/stages.d.ts.map +1 -0
- package/dist/stages.js +23 -0
- package/dist/stages.js.map +1 -0
- package/dist/sync/index.d.ts +7 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +7 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/push.js +4 -4
- package/dist/sync/push.js.map +1 -1
- package/dist/sync/remint.js +2 -2
- package/dist/sync/remint.js.map +1 -1
- package/dist/sync/types.d.ts +41 -1
- package/dist/sync/types.d.ts.map +1 -1
- package/dist/sync/types.js +47 -1
- package/dist/sync/types.js.map +1 -1
- package/dist/unlock/index.d.ts +6 -2
- package/dist/unlock/index.d.ts.map +1 -1
- package/dist/unlock/index.js +5 -1
- package/dist/unlock/index.js.map +1 -1
- package/dist/unlock/retire.d.ts +95 -14
- package/dist/unlock/retire.d.ts.map +1 -1
- package/dist/unlock/retire.js +102 -10
- package/dist/unlock/retire.js.map +1 -1
- package/dist/unlock/standingClient.d.ts.map +1 -1
- package/dist/unlock/standingClient.js +5 -1
- package/dist/unlock/standingClient.js.map +1 -1
- package/dist/unlock/standingWebvh.d.ts +341 -49
- package/dist/unlock/standingWebvh.d.ts.map +1 -1
- package/dist/unlock/standingWebvh.js +608 -164
- package/dist/unlock/standingWebvh.js.map +1 -1
- package/dist/webvh/accountEntry.d.ts +146 -0
- package/dist/webvh/accountEntry.d.ts.map +1 -0
- package/dist/webvh/accountEntry.js +239 -0
- package/dist/webvh/accountEntry.js.map +1 -0
- package/dist/webvh/didWeb.d.ts +12 -7
- package/dist/webvh/didWeb.d.ts.map +1 -1
- package/dist/webvh/didWeb.js +2 -2
- package/dist/webvh/didWeb.js.map +1 -1
- package/dist/webvh/didWebProjection.d.ts +164 -0
- package/dist/webvh/didWebProjection.d.ts.map +1 -0
- package/dist/webvh/didWebProjection.js +230 -0
- package/dist/webvh/didWebProjection.js.map +1 -0
- package/dist/webvh/didWebvh.d.ts +278 -138
- package/dist/webvh/didWebvh.d.ts.map +1 -1
- package/dist/webvh/didWebvh.js +314 -345
- package/dist/webvh/didWebvh.js.map +1 -1
- package/dist/webvh/enrollClient.d.ts +65 -0
- package/dist/webvh/enrollClient.d.ts.map +1 -0
- package/dist/webvh/enrollClient.js +172 -0
- package/dist/webvh/enrollClient.js.map +1 -0
- package/dist/webvh/index.d.ts +37 -12
- package/dist/webvh/index.d.ts.map +1 -1
- package/dist/webvh/index.js +33 -10
- package/dist/webvh/index.js.map +1 -1
- package/dist/webvh/listClients.d.ts +1 -33
- package/dist/webvh/listClients.d.ts.map +1 -1
- package/dist/webvh/listClients.js +2 -28
- package/dist/webvh/listClients.js.map +1 -1
- package/dist/webvh/revokeClient.d.ts +89 -11
- package/dist/webvh/revokeClient.d.ts.map +1 -1
- package/dist/webvh/revokeClient.js +182 -47
- package/dist/webvh/revokeClient.js.map +1 -1
- package/dist/webvh/standingZcap.d.ts +61 -14
- package/dist/webvh/standingZcap.d.ts.map +1 -1
- package/dist/webvh/standingZcap.js +91 -0
- package/dist/webvh/standingZcap.js.map +1 -1
- package/dist/webvh/verifyLog.d.ts +59 -5
- package/dist/webvh/verifyLog.d.ts.map +1 -1
- package/dist/webvh/verifyLog.js +76 -11
- package/dist/webvh/verifyLog.js.map +1 -1
- package/dist/webvh/wasIdStore.d.ts +8 -8
- package/dist/webvh/wasIdStore.d.ts.map +1 -1
- package/dist/webvh/wasIdStore.js +39 -14
- package/dist/webvh/wasIdStore.js.map +1 -1
- package/dist/webvh/zcap.d.ts +14 -1
- package/dist/webvh/zcap.d.ts.map +1 -1
- package/dist/webvh/zcap.js +3 -27
- package/dist/webvh/zcap.js.map +1 -1
- package/package.json +5 -5
- package/dist/webvh/keyAgreement.d.ts +0 -72
- package/dist/webvh/keyAgreement.d.ts.map +0 -1
- package/dist/webvh/keyAgreement.js +0 -47
- package/dist/webvh/keyAgreement.js.map +0 -1
|
@@ -10,10 +10,19 @@
|
|
|
10
10
|
* protect it, so a secret-derived rung would be a standing offline grind
|
|
11
11
|
* oracle against the credential.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* A self-enrollment is one loud reveal-and-commit entry signing with rung `i`
|
|
14
|
+
* and committing `hash(rung i + 1)`, after which the add entry retires the
|
|
15
|
+
* spent rung. Every other ceremony reuses its rung rather than consuming it:
|
|
16
|
+
* an entry keeps its own signer, so the acting rung is unioned back into
|
|
17
|
+
* `updateKeys`, and {@link attributeLadderRung} prefers a revealed rung over
|
|
18
|
+
* a committed one. So rung 0 stands revealed in the world-readable
|
|
19
|
+
* `updateKeys` for the credential's life, and prerotation protects it across
|
|
20
|
+
* none of the single-entry ceremonies. That is accepted on custody: the
|
|
21
|
+
* rung's private half exists only in tab memory during a ceremony, derived
|
|
22
|
+
* from the ladder seed just unsealed from the unlock record, so no attacker
|
|
23
|
+
* holds a rung without holding the seed, and the seed yields every rung. The
|
|
24
|
+
* attackers who do arise -- a phished credential, an offline grind of the
|
|
25
|
+
* record -- hold the seed, and credential rotation is already their remedy.
|
|
17
26
|
*
|
|
18
27
|
* There is no stored counter: which rung is current is recovered by
|
|
19
28
|
* re-deriving and scanning the published log's standing parameters
|
|
@@ -27,9 +36,14 @@
|
|
|
27
36
|
* ladder from the same seed), so the salt and info labels are permanent.
|
|
28
37
|
*/
|
|
29
38
|
import { deriveNextKeyHash } from '@interop/did-method-webvh';
|
|
39
|
+
import { vmFragmentOf } from '@interop/vh-resource-log';
|
|
30
40
|
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
31
41
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
32
|
-
import {
|
|
42
|
+
import { currentLogParameters, effectiveParameters, updateKeyMultibase } from '../webvh/didWebvh.js';
|
|
43
|
+
import { listEnrolledWebvhClients } from '../webvh/listClients.js';
|
|
44
|
+
import { credentialKeyAgreementMethods, ladderVmIds, relationIds, resolvedKeyAgreementMethods } from '../resourceLog/document.js';
|
|
45
|
+
import { survivingClientKeyProtection } from '../webvh/revokeClient.js';
|
|
46
|
+
import { log as logger } from '../log.js';
|
|
33
47
|
import { LADDER_SEED_BYTES } from '../unlock/unlockRecord.js';
|
|
34
48
|
/**
|
|
35
49
|
* The HKDF salt for rung derivation and the per-rung info prefix (the rung
|
|
@@ -39,9 +53,9 @@ import { LADDER_SEED_BYTES } from '../unlock/unlockRecord.js';
|
|
|
39
53
|
const LADDER_SALT = 'freewallet/unlock/update-ladder/v1';
|
|
40
54
|
const LADDER_RUNG_INFO_PREFIX = 'rung/';
|
|
41
55
|
/**
|
|
42
|
-
* The info label of the ladder VM -- the stable sibling key
|
|
43
|
-
*
|
|
44
|
-
* for everything ladder-seed-derived, with the info namespace doing the
|
|
56
|
+
* The info label of the ladder VM -- the stable sibling key a standing
|
|
57
|
+
* credential publishes in the account document for as long as it stands. One
|
|
58
|
+
* salt for everything ladder-seed-derived, with the info namespace doing the
|
|
45
59
|
* separation: `vm` can never collide with a `rung/<n>` label. Permanent.
|
|
46
60
|
*/
|
|
47
61
|
const LADDER_VM_INFO = 'vm';
|
|
@@ -135,10 +149,15 @@ export async function ladderRung({ ladderSeed, index }) {
|
|
|
135
149
|
* published verbatim in the account document (the seed is random, so the
|
|
136
150
|
* hash-commitment rule permits it) and stable across rung spends, so a
|
|
137
151
|
* delegation it signed survives every ladder advance. It carries the
|
|
138
|
-
*
|
|
152
|
+
* credential's document-visible authority (`assertionMethod` and
|
|
139
153
|
* `capabilityDelegation`), while update authority stays on the rungs -- the
|
|
140
154
|
* two roles never share a key.
|
|
141
155
|
*
|
|
156
|
+
* Its life is the credential's: the VM is installed in the entry that makes
|
|
157
|
+
* the credential standing (`publishUnlockKey`) and struck in the entry that
|
|
158
|
+
* retires it (`removeUnlockKey`). Enrollment never touches it, so several
|
|
159
|
+
* VMs stand on an account with several standing credentials.
|
|
160
|
+
*
|
|
142
161
|
* Because the key is derived, removing its verification method is never the
|
|
143
162
|
* terminal remedy: a later reinstall republishes the same key under the same
|
|
144
163
|
* id, and any still-unexpired delegation it signed resumes verifying the
|
|
@@ -277,7 +296,8 @@ function ladderSigned({ entry, ladderKeys }) {
|
|
|
277
296
|
*/
|
|
278
297
|
function entrySigners({ entry }) {
|
|
279
298
|
return (entry?.proof ?? []).flatMap(proof => {
|
|
280
|
-
const
|
|
299
|
+
const id = proof.verificationMethod;
|
|
300
|
+
const keyMultibase = id === undefined ? undefined : vmFragmentOf(id);
|
|
281
301
|
return keyMultibase === undefined ? [] : [keyMultibase];
|
|
282
302
|
});
|
|
283
303
|
}
|
|
@@ -311,6 +331,676 @@ function credentialSurvives({ entry, vmId }) {
|
|
|
311
331
|
// commitment.
|
|
312
332
|
return relationIds(entry.state.keyAgreement).includes(vmId);
|
|
313
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* The credential-class `keyAgreement` verification-method ids an entry
|
|
336
|
+
* INTRODUCES: those its document publishes and the previous entry's document
|
|
337
|
+
* did not. Credential-class means account-controlled
|
|
338
|
+
* (`credentialKeyAgreementMethods`), so an enrolled client's marked twin
|
|
339
|
+
* never counts. The co-introduction arm of the ladder-VM attribution reads
|
|
340
|
+
* this and refuses to act unless the answer is exactly this credential.
|
|
341
|
+
*
|
|
342
|
+
* @param options {object}
|
|
343
|
+
* @param options.doc {KeyAgreementDocument} the entry's document
|
|
344
|
+
* @param [options.prevDoc] {KeyAgreementDocument} the previous entry's
|
|
345
|
+
* @param options.did {string} the account DID
|
|
346
|
+
* @returns {string[]} in document order
|
|
347
|
+
*/
|
|
348
|
+
function introducedCredentialKeys({ doc, prevDoc, did }) {
|
|
349
|
+
const before = new Set((prevDoc ? credentialKeyAgreementMethods({ doc: prevDoc, did }) : []).map(method => method.id));
|
|
350
|
+
return credentialKeyAgreementMethods({ doc, did })
|
|
351
|
+
.map(method => method.id)
|
|
352
|
+
.filter((id) => id !== undefined && !before.has(id));
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The log-derived indexes every attribution walk starts from, computed once
|
|
356
|
+
* per log rather than once per credential.
|
|
357
|
+
*
|
|
358
|
+
* `effectiveParameters`, `indexLadderLog` and the enrolled-client attribution
|
|
359
|
+
* are pure functions of the log, and a retirement walks the SAME log once per
|
|
360
|
+
* retiring credential ({@link attributeRetiredCredentialRungs}), so without
|
|
361
|
+
* this the recovery-spend and last-client-forget ceremonies rebuild all three
|
|
362
|
+
* N times over. Keyed on the log's own identity: a log is read fresh and
|
|
363
|
+
* never mutated in place, so an entry keyed by one can never describe
|
|
364
|
+
* another.
|
|
365
|
+
*/
|
|
366
|
+
const ladderLogIndexes = new WeakMap();
|
|
367
|
+
/**
|
|
368
|
+
* {@link effectiveParameters} and {@link indexLadderLog} over a log, memoized
|
|
369
|
+
* on the log.
|
|
370
|
+
*
|
|
371
|
+
* @param log {DIDLog}
|
|
372
|
+
* @returns {object}
|
|
373
|
+
*/
|
|
374
|
+
function indexedLadderLog(log) {
|
|
375
|
+
const cached = ladderLogIndexes.get(log);
|
|
376
|
+
if (cached) {
|
|
377
|
+
return cached;
|
|
378
|
+
}
|
|
379
|
+
const params = effectiveParameters(log);
|
|
380
|
+
const { facts, commitIndex } = indexLadderLog({ log, params });
|
|
381
|
+
const indexed = { params, facts, commitIndex };
|
|
382
|
+
ladderLogIndexes.set(log, indexed);
|
|
383
|
+
return indexed;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The account's enrolled clients as the log attributes them, memoized on the
|
|
387
|
+
* log for the same reason as {@link indexedLadderLog}.
|
|
388
|
+
*
|
|
389
|
+
* @param log {DIDLog}
|
|
390
|
+
* @returns {ReturnType<typeof listEnrolledWebvhClients>}
|
|
391
|
+
*/
|
|
392
|
+
const enrolledClientsByLog = new WeakMap();
|
|
393
|
+
function indexedEnrolledClients(log) {
|
|
394
|
+
const cached = enrolledClientsByLog.get(log);
|
|
395
|
+
if (cached) {
|
|
396
|
+
return cached;
|
|
397
|
+
}
|
|
398
|
+
const clients = listEnrolledWebvhClients({ log });
|
|
399
|
+
enrolledClientsByLog.set(log, clients);
|
|
400
|
+
return clients;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* The one pre-pass over the log's effective parameters: per-entry facts, plus
|
|
404
|
+
* the commit index both walks project their positional questions through. The
|
|
405
|
+
* forward walk asks what an entry added and who signed it; the backward walk
|
|
406
|
+
* asks where a hash came from and what stood beside it there.
|
|
407
|
+
*
|
|
408
|
+
* @param options {object}
|
|
409
|
+
* @param options.log {DIDLog}
|
|
410
|
+
* @param options.params {Array<{ updateKeys: string[], nextKeyHashes: string[] }>}
|
|
411
|
+
* the log's effective parameters, entry by entry
|
|
412
|
+
* @returns {{ facts: LadderEntryFacts[], commitIndex: Map<string, LadderCommitOrigin> }}
|
|
413
|
+
*/
|
|
414
|
+
function indexLadderLog({ log, params }) {
|
|
415
|
+
const facts = [];
|
|
416
|
+
const commitIndex = new Map();
|
|
417
|
+
let prevUpdateKeys = new Set();
|
|
418
|
+
let prevHashes = new Set();
|
|
419
|
+
for (const [entryIndex, entry] of params.entries()) {
|
|
420
|
+
const currentUpdateKeys = new Set(entry.updateKeys);
|
|
421
|
+
const addedKeys = entry.updateKeys.filter(key => !prevUpdateKeys.has(key));
|
|
422
|
+
const removedKeys = [...prevUpdateKeys].filter(key => !currentUpdateKeys.has(key));
|
|
423
|
+
const addedHashes = entry.nextKeyHashes.filter(hash => !prevHashes.has(hash));
|
|
424
|
+
const signers = entrySigners({ entry: log[entryIndex] });
|
|
425
|
+
addedHashes.forEach((hash, at) => {
|
|
426
|
+
if (!commitIndex.has(hash)) {
|
|
427
|
+
commitIndex.set(hash, { entryIndex, at });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
facts.push({ addedKeys, removedKeys, addedHashes, signers });
|
|
431
|
+
prevUpdateKeys = currentUpdateKeys;
|
|
432
|
+
prevHashes = new Set(entry.nextKeyHashes);
|
|
433
|
+
}
|
|
434
|
+
return { facts, commitIndex };
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Walks the ladder BACKWARDS from the anchor, recovering the rungs the anchor
|
|
438
|
+
* has already climbed past. Run only when no ladder seed is in hand, which is
|
|
439
|
+
* the case the anchor's staleness would otherwise decide: the one writer that
|
|
440
|
+
* advances a recorded anchor does so after a self-enrollment, and every entry
|
|
441
|
+
* the spent rungs signed would then be invisible to the forward walk.
|
|
442
|
+
*
|
|
443
|
+
* Each step reads one hash's origin and asks which of the format's two
|
|
444
|
+
* positional rules put it there
|
|
445
|
+
* (`decisions/0007-ladder-reveal-hash-order.md`). Both rules are read here in
|
|
446
|
+
* reverse, so the shapes the emitters produce forwards are the shapes this
|
|
447
|
+
* recognizes backwards.
|
|
448
|
+
*
|
|
449
|
+
* The LAST-POSITION rule is a climb. A hash appended last among its entry's
|
|
450
|
+
* additions is the committer's own next commitment, so the key that signed
|
|
451
|
+
* that entry is the rung before it. The step is taken only when the entry
|
|
452
|
+
* authorized exactly one key and that key signed the entry, which makes it a
|
|
453
|
+
* prerotation reveal rather than mere adjacency, and only when the credential
|
|
454
|
+
* itself still stands in the entry's document.
|
|
455
|
+
*
|
|
456
|
+
* The ADJACENCY rule is a handover. A hash not in last position sits beside
|
|
457
|
+
* the rung committed immediately before it, and that predecessor is revealed
|
|
458
|
+
* later by the entry that retires the committer. The step is taken only when
|
|
459
|
+
* such a revealing entry exists and the credential stands in ITS document.
|
|
460
|
+
*
|
|
461
|
+
* What each guard protects. The credential-membership test stops the walk at a
|
|
462
|
+
* plain client genesis, at the enrolled-client bind that carries no member of
|
|
463
|
+
* ours yet, and at the spent recovery code whose reveal entry commits the
|
|
464
|
+
* REPLACEMENT code's hash last -- without it the replacement's retirement
|
|
465
|
+
* would recover the spent code's key and go on to strike the fresh
|
|
466
|
+
* credential's rungs. The single-self-revealing-key test stops it at the bind
|
|
467
|
+
* entry an enrolled client signs, which authorizes no key of its own, so the
|
|
468
|
+
* binding client's update key is never recovered as a rung. The strictly
|
|
469
|
+
* decreasing entry cursor and the already-recovered test keep the walk finite
|
|
470
|
+
* and acyclic.
|
|
471
|
+
*
|
|
472
|
+
* @param options {object}
|
|
473
|
+
* @param options.log {DIDLog}
|
|
474
|
+
* @param options.facts {LadderEntryFacts[]} from {@link indexLadderLog}
|
|
475
|
+
* @param options.commitIndex {Map<string, LadderCommitOrigin>} likewise
|
|
476
|
+
* @param options.anchorHash {string} `hash(anchorKeyMultibase)`
|
|
477
|
+
* @param options.credentialVmId {string} the credential's own `keyAgreement`
|
|
478
|
+
* verification-method id
|
|
479
|
+
* @param options.maxScan {number} how many rungs to walk back
|
|
480
|
+
* @returns {Promise<Array<{ key: string, hash: string }>>} the recovered
|
|
481
|
+
* rungs, nearest the anchor first
|
|
482
|
+
*/
|
|
483
|
+
async function recoverEarlierRungs({ log, facts, commitIndex, anchorHash, credentialVmId, maxScan }) {
|
|
484
|
+
const recovered = [];
|
|
485
|
+
const seenHashes = new Set([anchorHash]);
|
|
486
|
+
let cursorHash = anchorHash;
|
|
487
|
+
let cursorEntry = Number.POSITIVE_INFINITY;
|
|
488
|
+
for (let step = 0; step < maxScan; step++) {
|
|
489
|
+
const origin = commitIndex.get(cursorHash);
|
|
490
|
+
if (origin === undefined || origin.entryIndex >= cursorEntry) {
|
|
491
|
+
return recovered;
|
|
492
|
+
}
|
|
493
|
+
const originFacts = facts[origin.entryIndex];
|
|
494
|
+
if (origin.at === originFacts.addedHashes.length - 1) {
|
|
495
|
+
// The last-position rule, read backwards: a climb.
|
|
496
|
+
const predecessor = originFacts.addedKeys[0];
|
|
497
|
+
if (originFacts.addedKeys.length !== 1 ||
|
|
498
|
+
predecessor === undefined ||
|
|
499
|
+
!originFacts.signers.includes(predecessor) ||
|
|
500
|
+
!credentialSurvives({
|
|
501
|
+
entry: log[origin.entryIndex],
|
|
502
|
+
vmId: credentialVmId
|
|
503
|
+
})) {
|
|
504
|
+
return recovered;
|
|
505
|
+
}
|
|
506
|
+
const hash = await deriveNextKeyHash(predecessor);
|
|
507
|
+
if (seenHashes.has(hash)) {
|
|
508
|
+
return recovered;
|
|
509
|
+
}
|
|
510
|
+
recovered.push({ key: predecessor, hash });
|
|
511
|
+
seenHashes.add(hash);
|
|
512
|
+
cursorHash = hash;
|
|
513
|
+
cursorEntry = origin.entryIndex;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
// The adjacency rule, read backwards: a handover.
|
|
517
|
+
const partner = origin.at > 0 ? originFacts.addedHashes[origin.at - 1] : undefined;
|
|
518
|
+
if (partner === undefined || seenHashes.has(partner)) {
|
|
519
|
+
return recovered;
|
|
520
|
+
}
|
|
521
|
+
const reveal = await findRungReveal({
|
|
522
|
+
facts,
|
|
523
|
+
after: origin.entryIndex,
|
|
524
|
+
committerSigners: originFacts.signers,
|
|
525
|
+
hash: partner
|
|
526
|
+
});
|
|
527
|
+
if (reveal === undefined ||
|
|
528
|
+
!credentialSurvives({
|
|
529
|
+
entry: log[reveal.entryIndex],
|
|
530
|
+
vmId: credentialVmId
|
|
531
|
+
})) {
|
|
532
|
+
return recovered;
|
|
533
|
+
}
|
|
534
|
+
recovered.push({ key: reveal.key, hash: partner });
|
|
535
|
+
seenHashes.add(partner);
|
|
536
|
+
cursorHash = partner;
|
|
537
|
+
cursorEntry = origin.entryIndex;
|
|
538
|
+
}
|
|
539
|
+
return recovered;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* The entry that reveals a committed hash's key while retiring one of the
|
|
543
|
+
* signers that committed it -- the handover the adjacency rule describes.
|
|
544
|
+
* Earliest such entry wins, since a key is authorized once.
|
|
545
|
+
*
|
|
546
|
+
* @param options {object}
|
|
547
|
+
* @param options.facts {LadderEntryFacts[]}
|
|
548
|
+
* @param options.after {number} search entries strictly after this index
|
|
549
|
+
* @param options.committerSigners {string[]} the committing entry's signers
|
|
550
|
+
* @param options.hash {string} the committed hash whose key is sought
|
|
551
|
+
* @returns {Promise<{ entryIndex: number, key: string } | undefined>}
|
|
552
|
+
*/
|
|
553
|
+
async function findRungReveal({ facts, after, committerSigners, hash }) {
|
|
554
|
+
for (let entryIndex = after + 1; entryIndex < facts.length; entryIndex++) {
|
|
555
|
+
const candidate = facts[entryIndex];
|
|
556
|
+
if (!candidate.removedKeys.some(key => committerSigners.includes(key))) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
for (const key of candidate.addedKeys) {
|
|
560
|
+
if ((await deriveNextKeyHash(key)) === hash) {
|
|
561
|
+
return { entryIndex, key };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Thrown when an edit's `nextKeyHashes` would come out empty. An empty list
|
|
569
|
+
* switches prerotation off in did:webvh, so an entry that struck every
|
|
570
|
+
* commitment would leave the account with no staged key at all. Every ceremony
|
|
571
|
+
* that strikes hashes commits its own successors in the same entry, so the
|
|
572
|
+
* list is non-empty by construction; this is the assertion that says so.
|
|
573
|
+
*/
|
|
574
|
+
export class NextKeyHashesEmptyError extends Error {
|
|
575
|
+
constructor(message) {
|
|
576
|
+
super(message);
|
|
577
|
+
this.name = 'NextKeyHashesEmptyError';
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Refuses to publish an entry whose `nextKeyHashes` came out empty.
|
|
582
|
+
*
|
|
583
|
+
* @param options {object}
|
|
584
|
+
* @param options.nextKeyHashes {string[]}
|
|
585
|
+
* @param options.ceremony {string} named in the refusal
|
|
586
|
+
* @returns {string[]} the list, unchanged
|
|
587
|
+
*/
|
|
588
|
+
export function assertNextKeyHashesRemain({ nextKeyHashes, ceremony }) {
|
|
589
|
+
if (nextKeyHashes.length === 0) {
|
|
590
|
+
throw new NextKeyHashesEmptyError(`did:webvh: ${ceremony} would publish an entry committing no next key ` +
|
|
591
|
+
'hash, which switches prerotation off; the entry was not published.');
|
|
592
|
+
}
|
|
593
|
+
return nextKeyHashes;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* The enrolled-client members an entry INTRODUCES: new `capabilityInvocation`
|
|
597
|
+
* ids, and new `keyAgreement` methods the account DID does not control (a
|
|
598
|
+
* client's marked twin). The bind-anchor read refuses any entry that
|
|
599
|
+
* introduces one, because an entry publishing a client also publishes that
|
|
600
|
+
* client's update key, and reading that key as a credential's rung 0 would
|
|
601
|
+
* anchor the walk on a surviving client.
|
|
602
|
+
*
|
|
603
|
+
* @param options {object}
|
|
604
|
+
* @param options.doc {KeyAgreementDocument} the entry's document
|
|
605
|
+
* @param [options.prevDoc] {KeyAgreementDocument} the previous entry's
|
|
606
|
+
* @param options.did {string} the account DID
|
|
607
|
+
* @returns {boolean}
|
|
608
|
+
*/
|
|
609
|
+
function introducesEnrolledClient({ doc, prevDoc, did }) {
|
|
610
|
+
const beforeInvocation = new Set(relationIds(prevDoc?.capabilityInvocation));
|
|
611
|
+
if (relationIds(doc.capabilityInvocation).some(id => !beforeInvocation.has(id))) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
const markedIds = (entryDoc) => {
|
|
615
|
+
if (entryDoc === undefined) {
|
|
616
|
+
return new Set();
|
|
617
|
+
}
|
|
618
|
+
const credential = new Set(credentialKeyAgreementMethods({ doc: entryDoc, did }).map(method => method.id));
|
|
619
|
+
return new Set(resolvedKeyAgreementMethods({ doc: entryDoc })
|
|
620
|
+
.map(method => method.id)
|
|
621
|
+
.filter((id) => id !== undefined && !credential.has(id)));
|
|
622
|
+
};
|
|
623
|
+
const before = markedIds(prevDoc);
|
|
624
|
+
return [...markedIds(doc)].some(id => !before.has(id));
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* The anchor a credential's ladder walk starts from when the caller holds no
|
|
628
|
+
* recorded update key -- the log-only anchoring a cold browser needs. The
|
|
629
|
+
* credential's own `keyAgreement` member id is the anchor: the entry that
|
|
630
|
+
* FIRST introduced that member is the credential's bind entry, and what that
|
|
631
|
+
* entry did to the standing parameters names rung 0.
|
|
632
|
+
*
|
|
633
|
+
* Two shapes are read, both fail-closed:
|
|
634
|
+
*
|
|
635
|
+
* - the entry authorized exactly one update key and that key signed it (a
|
|
636
|
+
* prerotation reveal, the ladder-anchored genesis shape), so rung 0 is that
|
|
637
|
+
* key outright;
|
|
638
|
+
* - the entry authorized no key of its own and newly committed exactly one
|
|
639
|
+
* hash (the `publishUnlockKey` bind an enrolled client signs, and the
|
|
640
|
+
* recovery-code issuance sharing it), so rung 0's hash is that hash.
|
|
641
|
+
*
|
|
642
|
+
* Anything else is ambiguous and returns `undefined`, which the callers report
|
|
643
|
+
* as unclaimed rather than acting on. The reachable ambiguity is a bind entry
|
|
644
|
+
* introducing more than one credential-class member: a recovery
|
|
645
|
+
* add-and-retire entry introduces the fresh credential and the replacement
|
|
646
|
+
* code together, so neither is anchorable this way.
|
|
647
|
+
*
|
|
648
|
+
* @param options {object}
|
|
649
|
+
* @param options.log {DIDLog} a resolved, caller-verified log
|
|
650
|
+
* @param options.credentialVmId {string} the credential's `keyAgreement`
|
|
651
|
+
* verification-method id
|
|
652
|
+
* @returns {Promise<{ anchorKeyMultibase?: string, anchorHash?: string } |
|
|
653
|
+
* undefined>}
|
|
654
|
+
*/
|
|
655
|
+
export async function credentialLadderAnchor({ log, credentialVmId }) {
|
|
656
|
+
const { facts } = indexedLadderLog(log);
|
|
657
|
+
return resolveBindAnchor({ log, facts, credentialVmId });
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* The ladder VMs the log introduced ALONGSIDE something of this credential's
|
|
661
|
+
* -- the question a fail-closed retirement gate asks when its seedless walk
|
|
662
|
+
* claimed nothing. A standing VM is this credential's candidate in three
|
|
663
|
+
* shapes:
|
|
664
|
+
*
|
|
665
|
+
* - the entry that introduced it also introduced this credential's own
|
|
666
|
+
* `keyAgreement` member (the merged bind, and the recovery continuations'
|
|
667
|
+
* add-and-retire entry);
|
|
668
|
+
* - it newly committed the credential's anchor hash, or newly authorized its
|
|
669
|
+
* anchor key (the split bind, whose key entry and authority entry are two
|
|
670
|
+
* versions apart);
|
|
671
|
+
* - it introduced no credential-class member at ALL. Such an entry installs
|
|
672
|
+
* authority for a credential bound earlier, and the log does not say
|
|
673
|
+
* which, so every credential retiring seedlessly must treat it as possibly
|
|
674
|
+
* its own. A recovery code's issuance authority entry is exactly this
|
|
675
|
+
* shape, and a code whose registry entry records the wrong anchor reaches
|
|
676
|
+
* the gate through it.
|
|
677
|
+
*
|
|
678
|
+
* An empty result is the positive answer the gate needs: no entry of this log
|
|
679
|
+
* ever brought a ladder VM in beside anything of this credential's, so a VM
|
|
680
|
+
* standing now is a sibling's and the retirement has nothing of its own to
|
|
681
|
+
* leave behind. That is what tells a torn issuance's orphan -- a credential
|
|
682
|
+
* with a `keyAgreement` member, no ladder VM, and no committed rung -- from a
|
|
683
|
+
* credential whose VM stands unattributable, which the gate must still
|
|
684
|
+
* refuse. Every credential bind co-introduces its own member, so a sibling's
|
|
685
|
+
* VM never reaches the result through the third shape.
|
|
686
|
+
*
|
|
687
|
+
* Deliberately entry-shaped rather than an attribution: it answers which VMs
|
|
688
|
+
* COULD be this credential's, so a walk that refused still fails closed.
|
|
689
|
+
*
|
|
690
|
+
* @param options {object}
|
|
691
|
+
* @param options.log {DIDLog} a resolved, caller-verified log
|
|
692
|
+
* @param options.credentialVmId {string} the credential's `keyAgreement`
|
|
693
|
+
* verification-method id
|
|
694
|
+
* @param [options.anchorKeyMultibase] {string} the credential's recorded
|
|
695
|
+
* update key (a registry entry's `updateKeyMultibase`)
|
|
696
|
+
* @param [options.anchorHash] {string} the same anchor as a committed hash
|
|
697
|
+
* @returns {Promise<string[]>} the candidate ladder VM ids, in log order
|
|
698
|
+
*/
|
|
699
|
+
export async function ladderVmIdsIntroducedWithCredential({ log, credentialVmId, anchorKeyMultibase, anchorHash }) {
|
|
700
|
+
const did = credentialVmId.split('#')[0];
|
|
701
|
+
if (did === undefined || did === '') {
|
|
702
|
+
return [];
|
|
703
|
+
}
|
|
704
|
+
const { facts } = indexedLadderLog(log);
|
|
705
|
+
const rungHash = anchorHash ??
|
|
706
|
+
(anchorKeyMultibase === undefined
|
|
707
|
+
? undefined
|
|
708
|
+
: await deriveNextKeyHash(anchorKeyMultibase));
|
|
709
|
+
const candidates = [];
|
|
710
|
+
let prevDoc;
|
|
711
|
+
let prevLadderVmIds = new Set();
|
|
712
|
+
for (const [index, entry] of log.entries()) {
|
|
713
|
+
const doc = entry.state;
|
|
714
|
+
if (doc === undefined) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const publishedVmIds = ladderVmIds({ doc });
|
|
718
|
+
const newVmIds = publishedVmIds.filter(vmId => !prevLadderVmIds.has(vmId));
|
|
719
|
+
const introduced = introducedCredentialKeys({ doc, prevDoc, did });
|
|
720
|
+
prevDoc = doc;
|
|
721
|
+
prevLadderVmIds = new Set(publishedVmIds);
|
|
722
|
+
if (newVmIds.length === 0) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const bind = facts[index];
|
|
726
|
+
const carriesAnchor = introduced.includes(credentialVmId) ||
|
|
727
|
+
introduced.length === 0 ||
|
|
728
|
+
(rungHash !== undefined &&
|
|
729
|
+
(bind?.addedHashes.includes(rungHash) ?? false)) ||
|
|
730
|
+
(anchorKeyMultibase !== undefined &&
|
|
731
|
+
(bind?.addedKeys.includes(anchorKeyMultibase) ?? false));
|
|
732
|
+
if (carriesAnchor) {
|
|
733
|
+
candidates.push(...newVmIds);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return [...new Set(candidates)];
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* The synchronous core of {@link credentialLadderAnchor}, over a pre-pass the
|
|
740
|
+
* caller already ran.
|
|
741
|
+
*
|
|
742
|
+
* @param options {object}
|
|
743
|
+
* @param options.log {DIDLog}
|
|
744
|
+
* @param options.facts {LadderEntryFacts[]} from {@link indexLadderLog}
|
|
745
|
+
* @param options.credentialVmId {string}
|
|
746
|
+
* @returns {{ anchorKeyMultibase?: string, anchorHash?: string } | undefined}
|
|
747
|
+
*/
|
|
748
|
+
function resolveBindAnchor({ log, facts, credentialVmId }) {
|
|
749
|
+
const did = credentialVmId.split('#')[0];
|
|
750
|
+
if (did === undefined || did === '') {
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
// Every update key the log attributes to a client the final document still
|
|
754
|
+
// lists, so the self-signed arm can refuse one outright. A client whose
|
|
755
|
+
// active key the log cannot attribute leaves that arm unable to refuse
|
|
756
|
+
// anything, so no anchor is named at all.
|
|
757
|
+
const enrolledClients = indexedEnrolledClients(log);
|
|
758
|
+
if (enrolledClients.some(client => client.updateKeyMultibase === undefined)) {
|
|
759
|
+
return undefined;
|
|
760
|
+
}
|
|
761
|
+
const enrolledClientKeys = new Set(enrolledClients
|
|
762
|
+
.map(client => client.updateKeyMultibase)
|
|
763
|
+
.filter((key) => key !== undefined));
|
|
764
|
+
let prevDoc;
|
|
765
|
+
for (const [index, entry] of log.entries()) {
|
|
766
|
+
const doc = entry.state;
|
|
767
|
+
if (doc === undefined) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const introduced = introducedCredentialKeys({ doc, prevDoc, did });
|
|
771
|
+
const prevDocBefore = prevDoc;
|
|
772
|
+
prevDoc = doc;
|
|
773
|
+
if (!introduced.includes(credentialVmId)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
// The bind entry. More than one credential-class member introduced here
|
|
777
|
+
// and nothing below can say which addition is whose.
|
|
778
|
+
if (introduced.length !== 1) {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
const bind = facts[index];
|
|
782
|
+
if (bind === undefined) {
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
// The fourth condition: an entry that also publishes an enrolled client
|
|
786
|
+
// names no credential's rung. The remembered recovery's add-and-retire
|
|
787
|
+
// entry is exactly this shape -- the new client's key-agreement method is
|
|
788
|
+
// client-marked, so the credential-class count above sees only the
|
|
789
|
+
// replacement code and the ambiguity guard does not fire, while the one
|
|
790
|
+
// key the entry authorizes is the CLIENT's update key.
|
|
791
|
+
if (introducesEnrolledClient({
|
|
792
|
+
doc: doc,
|
|
793
|
+
prevDoc: prevDocBefore,
|
|
794
|
+
did
|
|
795
|
+
})) {
|
|
796
|
+
return undefined;
|
|
797
|
+
}
|
|
798
|
+
const revealed = bind.addedKeys[0];
|
|
799
|
+
if (bind.addedKeys.length === 1 &&
|
|
800
|
+
revealed !== undefined &&
|
|
801
|
+
bind.signers.includes(revealed) &&
|
|
802
|
+
// Belt and braces beside the condition above: never anchor on a key the
|
|
803
|
+
// log attributes to an enrolled client, whichever entry published it.
|
|
804
|
+
!enrolledClientKeys.has(revealed)) {
|
|
805
|
+
return { anchorKeyMultibase: revealed };
|
|
806
|
+
}
|
|
807
|
+
if (bind.addedKeys.length === 0 && bind.addedHashes.length === 1) {
|
|
808
|
+
// No enrolled-client check of its own: this arm reads a hash rather
|
|
809
|
+
// than a key, and no ceremony fuses a credential bind with a client's
|
|
810
|
+
// hash commitment.
|
|
811
|
+
return { anchorHash: bind.addedHashes[0] };
|
|
812
|
+
}
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Whether one retiring credential's rung inventory can be claimed from the log
|
|
819
|
+
* at all: its bind entry must name an anchor, and the walk from that anchor
|
|
820
|
+
* must not refuse. This is the log-only test, so it answers the same before
|
|
821
|
+
* and after the retirement entry lands -- which is what lets a resumed run
|
|
822
|
+
* report the same unclaimed set the first run reported.
|
|
823
|
+
*
|
|
824
|
+
* @param options {object}
|
|
825
|
+
* @param options.log {DIDLog}
|
|
826
|
+
* @param options.credentialVmId {string}
|
|
827
|
+
* @param options.maxScan {number}
|
|
828
|
+
* @returns {Promise<LadderStandingInventory | undefined>} the walk's result,
|
|
829
|
+
* or `undefined` when the credential cannot be claimed
|
|
830
|
+
*/
|
|
831
|
+
async function claimLadderInventory({ log, credentialVmId, maxScan }) {
|
|
832
|
+
try {
|
|
833
|
+
return await attributeLadderInventory({ log, credentialVmId, maxScan });
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
// An ambiguous anchor or an ambiguous history. Fail closed.
|
|
837
|
+
return undefined;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* The strike a retirement entry ALREADY published, recomputed by re-running
|
|
842
|
+
* {@link attributeRetiredCredentialRungs} over the log as it stood just before
|
|
843
|
+
* that entry. A resumed ceremony reports what its first run reported this way,
|
|
844
|
+
* rather than through a second definition of "unclaimed" that could answer
|
|
845
|
+
* differently.
|
|
846
|
+
*
|
|
847
|
+
* The entry is located by the key it authorized: every ceremony that calls
|
|
848
|
+
* this detects its own completion by that key standing in `updateKeys`, and
|
|
849
|
+
* the entry that FIRST authorized it is the one to walk back to. A log that
|
|
850
|
+
* does not authorize the key, or authorizes it at the genesis entry, has no
|
|
851
|
+
* usable prefix and is refused: a caller that reached this had already seen
|
|
852
|
+
* the key authorized, so either shape is a caller defect rather than a
|
|
853
|
+
* state to answer for.
|
|
854
|
+
*
|
|
855
|
+
* @param options {object}
|
|
856
|
+
* @param options.log {DIDLog} the post-entry log
|
|
857
|
+
* @param options.authorizedKeyMultibase {string} the update key the entry
|
|
858
|
+
* authorized
|
|
859
|
+
* @param options.credentialVmIds {string[]} the credentials the entry
|
|
860
|
+
* retired, as the caller derived them from the log
|
|
861
|
+
* @param [options.protectedHashes] {string[]} the same set the first run
|
|
862
|
+
* passed
|
|
863
|
+
* @param [options.protectedKeys] {string[]} likewise
|
|
864
|
+
* @param [options.maxScan] {number}
|
|
865
|
+
* @returns {Promise<{ struckHashes: string[], struckKeys: string[],
|
|
866
|
+
* unclaimedCredentialVmIds: string[] }>}
|
|
867
|
+
*/
|
|
868
|
+
export async function retiredCredentialRungsBeforeKey({ log, authorizedKeyMultibase, credentialVmIds, protectedHashes = [], protectedKeys = [], maxScan = LADDER_MAX_SCAN }) {
|
|
869
|
+
const params = effectiveParameters(log);
|
|
870
|
+
const entryIndex = params.findIndex(entry => entry.updateKeys.includes(authorizedKeyMultibase));
|
|
871
|
+
if (entryIndex <= 0) {
|
|
872
|
+
throw new Error(`retiredCredentialRungsBeforeKey: the log ${entryIndex < 0 ? 'never authorizes' : 'authorizes at genesis'} update key ${authorizedKeyMultibase}, so no pre-entry prefix exists`);
|
|
873
|
+
}
|
|
874
|
+
return attributeRetiredCredentialRungs({
|
|
875
|
+
log: log.slice(0, entryIndex),
|
|
876
|
+
credentialVmIds,
|
|
877
|
+
protectedHashes,
|
|
878
|
+
protectedKeys,
|
|
879
|
+
maxScan
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* What a full retirement must strike from the standing parameters for a set of
|
|
884
|
+
* credentials being retired in one entry: their committed rung hashes, and any
|
|
885
|
+
* rung of theirs standing revealed in `updateKeys`. Each credential is
|
|
886
|
+
* anchored from the log alone ({@link credentialLadderAnchor}), so a cold
|
|
887
|
+
* browser holding no registry and no seed can still strike them.
|
|
888
|
+
*
|
|
889
|
+
* The bias is under-striking, deliberately. Over-striking is silent and
|
|
890
|
+
* unhealable -- a surviving credential or client keeps its verification
|
|
891
|
+
* methods and its roster wrap, and only fails when someone finally uses it --
|
|
892
|
+
* while under-striking leaves a committed rung a retired credential's holder
|
|
893
|
+
* could reveal, which the report names. Five things keep it that way:
|
|
894
|
+
*
|
|
895
|
+
* - a credential whose anchor is ambiguous or whose walk refuses is reported
|
|
896
|
+
* as unclaimed and nothing of its is struck;
|
|
897
|
+
* - only what the walk positively claims is a candidate;
|
|
898
|
+
* - a hash or key the caller names as its own (`protectedHashes` /
|
|
899
|
+
* `protectedKeys`, the successors the entry itself commits) is dropped;
|
|
900
|
+
* - every SURVIVING enrolled client's active update key, its carry-over hash
|
|
901
|
+
* and its staged hash are dropped, whatever the walk claimed
|
|
902
|
+
* ({@link survivingClientKeyProtection}). That guard is structural rather
|
|
903
|
+
* than a property of the walk, because a mis-anchored walk landing on a
|
|
904
|
+
* client's key would otherwise end that client's ability to extend the
|
|
905
|
+
* account log for good. The walks therefore run FIRST, and the hashes they
|
|
906
|
+
* claimed are passed to the protection as known-latent, so a retiring
|
|
907
|
+
* credential's own rung cannot make a client's staged attribution ambiguous
|
|
908
|
+
* and get itself protected as a candidate;
|
|
909
|
+
* - a listed enrolled client whose ACTIVE update key the log cannot attribute
|
|
910
|
+
* withholds the WHOLE strike: nothing is struck and every credential is
|
|
911
|
+
* reported, since the structural guard cannot say what that client holds.
|
|
912
|
+
*
|
|
913
|
+
* The report is a not-fully-retired report rather than a nothing-happened one.
|
|
914
|
+
* A credential appears on `unclaimedCredentialVmIds` when its walk refused,
|
|
915
|
+
* when it claimed nothing, AND when any single hash or key it claimed was
|
|
916
|
+
* withheld by one of the kept sets. The rest of that credential's claims are
|
|
917
|
+
* still struck; what the caller must not be told is that a partial retirement
|
|
918
|
+
* was a whole one.
|
|
919
|
+
*
|
|
920
|
+
* @param options {object}
|
|
921
|
+
* @param options.log {DIDLog} a resolved, caller-verified log, read BEFORE
|
|
922
|
+
* the entry is built
|
|
923
|
+
* @param options.credentialVmIds {string[]} the retiring credentials'
|
|
924
|
+
* `keyAgreement` verification-method ids
|
|
925
|
+
* @param [options.protectedHashes] {string[]} hashes the entry itself
|
|
926
|
+
* commits, never struck
|
|
927
|
+
* @param [options.protectedKeys] {string[]} update keys the entry itself
|
|
928
|
+
* authorizes, never struck
|
|
929
|
+
* @param [options.maxScan] {number} the ladder walk's bound
|
|
930
|
+
* @returns {Promise<{ struckHashes: string[], struckKeys: string[],
|
|
931
|
+
* unclaimedCredentialVmIds: string[] }>}
|
|
932
|
+
*/
|
|
933
|
+
export async function attributeRetiredCredentialRungs({ log, credentialVmIds, protectedHashes = [], protectedKeys = [], maxScan = LADDER_MAX_SCAN }) {
|
|
934
|
+
// The walks first, so what they claimed can be vouched for as latent when
|
|
935
|
+
// the surviving clients' staged hashes are attributed below.
|
|
936
|
+
const claims = new Map();
|
|
937
|
+
const claimedHashes = new Set();
|
|
938
|
+
for (const credentialVmId of credentialVmIds) {
|
|
939
|
+
const inventory = await claimLadderInventory({
|
|
940
|
+
log,
|
|
941
|
+
credentialVmId,
|
|
942
|
+
maxScan
|
|
943
|
+
});
|
|
944
|
+
claims.set(credentialVmId, inventory);
|
|
945
|
+
for (const hash of inventory?.committedHashes ?? []) {
|
|
946
|
+
claimedHashes.add(hash);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// The structural guard, resolved once from the log rather than per
|
|
950
|
+
// credential: what the account's surviving enrolled clients hold.
|
|
951
|
+
const surviving = await survivingClientKeyProtection({
|
|
952
|
+
log,
|
|
953
|
+
retiredVmIds: credentialVmIds,
|
|
954
|
+
knownLatentHashes: [...claimedHashes]
|
|
955
|
+
});
|
|
956
|
+
if (surviving.ambiguous.length > 0) {
|
|
957
|
+
logger.warn('Withholding a credential rung strike: an enrolled client whose ' +
|
|
958
|
+
'active update key the log cannot attribute would be unprotected', { clients: surviving.ambiguous });
|
|
959
|
+
return {
|
|
960
|
+
struckHashes: [],
|
|
961
|
+
struckKeys: [],
|
|
962
|
+
unclaimedCredentialVmIds: [...credentialVmIds]
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
const keptHashes = new Set([...protectedHashes, ...surviving.hashes]);
|
|
966
|
+
const keptKeys = new Set([...protectedKeys, ...surviving.keys]);
|
|
967
|
+
const struckHashes = new Set();
|
|
968
|
+
const struckKeys = new Set();
|
|
969
|
+
const unclaimedCredentialVmIds = [];
|
|
970
|
+
for (const credentialVmId of credentialVmIds) {
|
|
971
|
+
const inventory = claims.get(credentialVmId);
|
|
972
|
+
if (inventory === undefined) {
|
|
973
|
+
unclaimedCredentialVmIds.push(credentialVmId);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
let withheld = false;
|
|
977
|
+
let struckAny = false;
|
|
978
|
+
for (const hash of inventory.committedHashes) {
|
|
979
|
+
if (keptHashes.has(hash)) {
|
|
980
|
+
withheld = true;
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
struckHashes.add(hash);
|
|
984
|
+
struckAny = true;
|
|
985
|
+
}
|
|
986
|
+
for (const key of inventory.revealedKeys) {
|
|
987
|
+
if (keptKeys.has(key)) {
|
|
988
|
+
withheld = true;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
struckKeys.add(key);
|
|
992
|
+
struckAny = true;
|
|
993
|
+
}
|
|
994
|
+
if (withheld || !struckAny) {
|
|
995
|
+
unclaimedCredentialVmIds.push(credentialVmId);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
struckHashes: [...struckHashes],
|
|
1000
|
+
struckKeys: [...struckKeys],
|
|
1001
|
+
unclaimedCredentialVmIds
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
314
1004
|
/**
|
|
315
1005
|
* Attributes a ladder's FULL standing inventory from the log -- the retirement
|
|
316
1006
|
* counterpart of {@link attributeLadderRung}, which recovers only the single
|
|
@@ -351,35 +1041,126 @@ function credentialSurvives({ entry, vmId }) {
|
|
|
351
1041
|
* commitment in that position instead, and striking that would leave the
|
|
352
1042
|
* replacement unusable and unhealable;
|
|
353
1043
|
* - a claim or revealed key that later leaves the parameters without a
|
|
354
|
-
* completion was struck by some other edit and simply stops standing
|
|
1044
|
+
* completion was struck by some other edit and simply stops standing;
|
|
1045
|
+
* - a ladder VM standing in the final document belongs to this ladder on
|
|
1046
|
+
* either of two arms, asked at the entry that PUBLISHED it (the entry at
|
|
1047
|
+
* which the id appeared among the document's ladder VMs). The SIGNER arm:
|
|
1048
|
+
* that entry was signed by a key this ladder accounted for at that point,
|
|
1049
|
+
* which covers every install a ladder rung signs (the ladder-anchored
|
|
1050
|
+
* genesis, the ladder-VM install, the transient recovery's add-and-retire
|
|
1051
|
+
* entry). The CO-INTRODUCTION arm: that entry also introduced this
|
|
1052
|
+
* credential's own `keyAgreement` member (`credentialVmId`), which is what
|
|
1053
|
+
* reaches a bind entry an ENROLLED CLIENT signed -- the shape
|
|
1054
|
+
* `publishUnlockKey` writes, whose signer is the binding client's update
|
|
1055
|
+
* key rather than a rung. Three guards keep that arm from over-claiming:
|
|
1056
|
+
* it needs `credentialVmId` in hand, the entry must introduce exactly ONE
|
|
1057
|
+
* credential-class `keyAgreement` member (the account-controlled class,
|
|
1058
|
+
* `credentialKeyAgreementMethods`; the transient recovery entry introduces
|
|
1059
|
+
* two and is left to the signer arm), and the entry must introduce exactly
|
|
1060
|
+
* ONE ladder VM. The question is anchored rather than free-standing: it
|
|
1061
|
+
* answers "is this VM mine", and a VM no anchored ladder claims is
|
|
1062
|
+
* identified by subtraction and left standing, since striking a key this
|
|
1063
|
+
* ladder cannot show it owns would take out a surviving credential's.
|
|
1064
|
+
* The COMMITMENT arm: that entry committed a hash the ladder knows a priori
|
|
1065
|
+
* (the anchor's, a seed-derived rung's, or one the backward pre-pass
|
|
1066
|
+
* recovered), introduced exactly one ladder VM, and introduced no OTHER
|
|
1067
|
+
* credential's `keyAgreement` member. It reaches the reinstall an
|
|
1068
|
+
* `establishStandingUnlock` re-run writes, which mints a fresh ladder seed
|
|
1069
|
+
* for a credential whose member already stands, so neither of the other
|
|
1070
|
+
* arms can see it. Like the co-introduction arm it needs `credentialVmId`
|
|
1071
|
+
* in hand, since with no id the foreign-member guard would pass vacuously.
|
|
1072
|
+
*
|
|
1073
|
+
* One narrowing runs across both the signer arm and the hash claims. A ladder
|
|
1074
|
+
* derives exactly ONE verification method from its seed, so a ladder already
|
|
1075
|
+
* holding a standing claimed VM cannot own a second one an entry introduces.
|
|
1076
|
+
* An entry introducing such a VM, or another credential's `keyAgreement`
|
|
1077
|
+
* member, is installing a foreign inventory: the acting credential's rung
|
|
1078
|
+
* signs exactly that on a ladder-branch bind and on a recovery code's split
|
|
1079
|
+
* issuance. The signer arm does not claim the VM there, and the hashes such
|
|
1080
|
+
* an entry commits are the bound credential's rung commitments rather than
|
|
1081
|
+
* this ladder's next one, so they stay out of the claims. Without the
|
|
1082
|
+
* narrowing, retiring the acting credential would strike the credential (or
|
|
1083
|
+
* the recovery code) it had just bound.
|
|
1084
|
+
*
|
|
1085
|
+
* The attribution is anchor-invariant across the shapes where each rung's
|
|
1086
|
+
* hash was committed by an entry that also revealed the previous rung, or by
|
|
1087
|
+
* a handover. The signer arm's key set is the anchor plus every earlier rung
|
|
1088
|
+
* a backward pre-pass recovers from the log's own positional rules ({@link
|
|
1089
|
+
* recoverEarlierRungs}, over `decisions/0007-ladder-reveal-hash-order.md`),
|
|
1090
|
+
* so an entry a spent rung signed is attributed however far the anchor has
|
|
1091
|
+
* since climbed. The ladder seed (`ladderSeed`) remains a shortcut and a
|
|
1092
|
+
* cross-check rather than a requirement: it makes every rung's key and hash
|
|
1093
|
+
* known outright and skips the pre-pass. A residue no arm can attribute is
|
|
1094
|
+
* still released -- the retirement then strikes what the recorded inventory
|
|
1095
|
+
* names and nothing more. More than one ladder reveal standing or arriving at
|
|
1096
|
+
* once matches no legitimate history and fails closed ({@link
|
|
1097
|
+
* LadderAttributionError}).
|
|
355
1098
|
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
1099
|
+
* One shape is out of reach seedlessly, and it is reachable today. The
|
|
1100
|
+
* last-client transition strikes the ladder VM and reinstalls it in the same
|
|
1101
|
+
* run (`forgetLastEnrolledClient` stage 1): same seed, the credential's
|
|
1102
|
+
* member standing, the acting rung's hash still committed, and no hash added.
|
|
1103
|
+
* A later self-enrollment then spends that already-revealed rung, so its
|
|
1104
|
+
* reveal-and-commit entry authorizes no key while committing the next rung's
|
|
1105
|
+
* hash, and the registry anchor advances to that next rung. The backward walk
|
|
1106
|
+
* climbs from the anchor by asking which key the entry that committed its
|
|
1107
|
+
* hash authorized; that entry authorized none, so the walk cannot name the
|
|
1108
|
+
* rung that signed it, and the earlier rung and the reinstalled VM go
|
|
1109
|
+
* unrecovered. A seedless retirement then reports the VM as `unclaimed` and
|
|
1110
|
+
* leaves it standing. Tracked as WC-158.
|
|
1111
|
+
*
|
|
1112
|
+
* The anchor comes in three forms, and the walk is the same afterwards. A
|
|
1113
|
+
* recorded update-key multibase (`anchorKeyMultibase`) is what a caller
|
|
1114
|
+
* holding a registry entry passes. A hash (`anchorHash`) is the same anchor
|
|
1115
|
+
* with the key withheld: the rung is picked up when the log reveals it, since
|
|
1116
|
+
* the reveal test already matches on the commitment. With neither, and a
|
|
1117
|
+
* `credentialVmId` in hand, the anchor is read off the credential's bind entry
|
|
1118
|
+
* ({@link credentialLadderAnchor}) -- the cold-browser mode, where no registry
|
|
1119
|
+
* is readable before the entry is written. An anchor the bind entry cannot
|
|
1120
|
+
* name unambiguously refuses with {@link LadderAttributionError} rather than
|
|
1121
|
+
* walking from a guess.
|
|
364
1122
|
*
|
|
365
1123
|
* @param options {object}
|
|
366
1124
|
* @param options.log {DIDLog} a resolved, caller-verified log
|
|
367
|
-
* @param options.anchorKeyMultibase {string} the credential's recorded
|
|
1125
|
+
* @param [options.anchorKeyMultibase] {string} the credential's recorded
|
|
368
1126
|
* update-key multibase (bind-time rung 0, or a refreshed later rung)
|
|
1127
|
+
* @param [options.anchorHash] {string} the same anchor as a committed hash,
|
|
1128
|
+
* for a caller that resolved one without the key
|
|
369
1129
|
* @param [options.ladderSeed] {Uint8Array} the credential's ladder seed,
|
|
370
1130
|
* when the caller holds it
|
|
371
1131
|
* @param [options.credentialVmId] {string} the credential's own
|
|
372
1132
|
* `keyAgreement` verification-method id, which tells a climb (the
|
|
373
1133
|
* credential stands afterwards) from a spend (its inventory goes in the same
|
|
374
|
-
* entry)
|
|
1134
|
+
* entry), which the ladder VM's co-introduction arm is anchored on, and
|
|
1135
|
+
* which supplies the anchor itself when neither anchor form is passed
|
|
375
1136
|
* @param [options.maxScan] {number} seeded pre-derivation bound; defaults to
|
|
376
1137
|
* {@link LADDER_MAX_SCAN}
|
|
377
|
-
* @returns {Promise<LadderStandingInventory>} what currently stands;
|
|
378
|
-
*
|
|
1138
|
+
* @returns {Promise<LadderStandingInventory>} what currently stands; every
|
|
1139
|
+
* array empty when the log carries nothing of the ladder any more
|
|
379
1140
|
*/
|
|
380
|
-
export async function attributeLadderInventory({ log, anchorKeyMultibase, ladderSeed, credentialVmId, maxScan = LADDER_MAX_SCAN }) {
|
|
381
|
-
const
|
|
382
|
-
|
|
1141
|
+
export async function attributeLadderInventory({ log, anchorKeyMultibase, anchorHash: suppliedAnchorHash, ladderSeed, credentialVmId, maxScan = LADDER_MAX_SCAN }) {
|
|
1142
|
+
const { params, facts, commitIndex } = indexedLadderLog(log);
|
|
1143
|
+
// The anchor, in the caller's order of preference: the recorded update key,
|
|
1144
|
+
// a hash the caller resolved itself, or -- holding neither, the cold-browser
|
|
1145
|
+
// case -- the credential's own bind entry, read off the log.
|
|
1146
|
+
let anchorKey = anchorKeyMultibase;
|
|
1147
|
+
let anchorHash = suppliedAnchorHash;
|
|
1148
|
+
if (anchorKey === undefined && anchorHash === undefined) {
|
|
1149
|
+
const resolved = credentialVmId === undefined
|
|
1150
|
+
? undefined
|
|
1151
|
+
: resolveBindAnchor({ log, facts, credentialVmId });
|
|
1152
|
+
if (resolved === undefined) {
|
|
1153
|
+
throw new LadderAttributionError('The ladder walk was given no anchor, and the log does not name an ' +
|
|
1154
|
+
'unambiguous bind entry for this credential; refusing to ' +
|
|
1155
|
+
'attribute an ambiguous history.');
|
|
1156
|
+
}
|
|
1157
|
+
anchorKey = resolved.anchorKeyMultibase;
|
|
1158
|
+
anchorHash = resolved.anchorHash;
|
|
1159
|
+
}
|
|
1160
|
+
if (anchorHash === undefined) {
|
|
1161
|
+
anchorHash = await deriveNextKeyHash(anchorKey);
|
|
1162
|
+
}
|
|
1163
|
+
const ladderKeys = new Set(anchorKey === undefined ? [] : [anchorKey]);
|
|
383
1164
|
// What the ladder knows a priori: the recorded key's hash and, with the
|
|
384
1165
|
// seed in hand, every rung's. A claim outside this set is held on the
|
|
385
1166
|
// evidence of the entry that committed it, and released again when a
|
|
@@ -395,33 +1176,74 @@ export async function attributeLadderInventory({ log, anchorKeyMultibase, ladder
|
|
|
395
1176
|
ladderHashes.add(rungHash);
|
|
396
1177
|
}
|
|
397
1178
|
}
|
|
398
|
-
|
|
1179
|
+
// Without the seed, the anchor alone would hide every entry a spent rung
|
|
1180
|
+
// signed. Recover those rungs from the log's positional rules first, and
|
|
1181
|
+
// treat them exactly as seed-derived ones: known a priori, on both sets.
|
|
1182
|
+
if (!ladderSeed && credentialVmId !== undefined) {
|
|
1183
|
+
const earlier = await recoverEarlierRungs({
|
|
1184
|
+
log,
|
|
1185
|
+
facts,
|
|
1186
|
+
commitIndex,
|
|
1187
|
+
anchorHash,
|
|
1188
|
+
credentialVmId,
|
|
1189
|
+
maxScan
|
|
1190
|
+
});
|
|
1191
|
+
for (const rung of earlier) {
|
|
1192
|
+
ladderKeys.add(rung.key);
|
|
1193
|
+
derivedHashes.add(rung.hash);
|
|
1194
|
+
ladderHashes.add(rung.hash);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
399
1197
|
let pending;
|
|
400
|
-
let prevUpdateKeys = new Set();
|
|
401
1198
|
let prevHashes = new Set();
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
|
|
1199
|
+
// Whether the entry that published each ladder VM seen so far was signed by
|
|
1200
|
+
// a key this ladder accounted for at that point. Re-answered at every
|
|
1201
|
+
// publication, so a VM struck and later republished by another ladder is
|
|
1202
|
+
// attributed to whoever put the standing copy there.
|
|
1203
|
+
let prevLadderVmIds = new Set();
|
|
1204
|
+
let prevEntryDoc;
|
|
1205
|
+
const ladderVmClaims = new Map();
|
|
1206
|
+
// The account DID, read off the credential's own verification-method id:
|
|
1207
|
+
// the co-introduction arm needs it to tell a credential-class
|
|
1208
|
+
// `keyAgreement` member (controlled by the account) from an enrolled
|
|
1209
|
+
// client's marked twin.
|
|
1210
|
+
const credentialDid = credentialVmId?.split('#')[0];
|
|
407
1211
|
for (const [index, entry] of params.entries()) {
|
|
408
1212
|
const currentUpdateKeys = new Set(entry.updateKeys);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1213
|
+
// The pre-pass keeps these order-preserving on purpose: the completion
|
|
1214
|
+
// transfer below reads the claim committed immediately after the client's
|
|
1215
|
+
// update-key hash as its staged hash (the reveal entry's append order, a
|
|
1216
|
+
// ratified convention).
|
|
1217
|
+
const { addedKeys, removedKeys, addedHashes } = facts[index];
|
|
1218
|
+
// What this entry introduces, resolved before the reveal branch because
|
|
1219
|
+
// the hash claims below turn on it: the ladder VMs it publishes that were
|
|
1220
|
+
// not standing before, and the credential-class `keyAgreement` members it
|
|
1221
|
+
// brings in.
|
|
1222
|
+
const entryDoc = log[index]?.state;
|
|
1223
|
+
const publishedVmIds = entryDoc ? ladderVmIds({ doc: entryDoc }) : [];
|
|
1224
|
+
const newVmIds = publishedVmIds.filter(vmId => !prevLadderVmIds.has(vmId));
|
|
1225
|
+
const introduced = credentialDid !== undefined && entryDoc !== undefined
|
|
1226
|
+
? introducedCredentialKeys({
|
|
1227
|
+
doc: entryDoc,
|
|
1228
|
+
prevDoc: prevEntryDoc,
|
|
1229
|
+
did: credentialDid
|
|
1230
|
+
})
|
|
1231
|
+
: [];
|
|
1232
|
+
// A ladder derives exactly ONE verification method from its seed, so a
|
|
1233
|
+
// ladder already holding a standing VM cannot own a second one this entry
|
|
1234
|
+
// introduces. That is what tells the acting credential's rung, signing
|
|
1235
|
+
// another credential's inventory into the document, from a rung signing
|
|
1236
|
+
// its own in.
|
|
1237
|
+
const holdsStandingVm = [...prevLadderVmIds].some(vmId => ladderVmClaims.get(vmId) === true);
|
|
1238
|
+
const foreignVmIntroduced = newVmIds.length > 0 && holdsStandingVm;
|
|
1239
|
+
// An entry installing another party's inventory -- a successor
|
|
1240
|
+
// credential's `keyAgreement` member, or a ladder VM this ladder cannot
|
|
1241
|
+
// own -- commits that party's rung hash, not this ladder's next one. The
|
|
1242
|
+
// acting rung signs such an entry on every ladder-branch bind and on a
|
|
1243
|
+
// recovery code's split issuance, and claiming what it commits there
|
|
1244
|
+
// would strike the bound credential's own commitment when this one
|
|
1245
|
+
// retires.
|
|
1246
|
+
const installsForeignInventory = foreignVmIntroduced || introduced.some(id => id !== credentialVmId);
|
|
425
1247
|
// Completion first: the pending revealed rung left `updateKeys`. When the
|
|
426
1248
|
// same entry authorizes a key whose hash sits among the reveal's claims,
|
|
427
1249
|
// the enrollment completed -- that hash and its successor (the client's
|
|
@@ -499,19 +1321,26 @@ export async function attributeLadderInventory({ log, anchorKeyMultibase, ladder
|
|
|
499
1321
|
// credential survives or the seed derives it.
|
|
500
1322
|
const keyHash = await deriveNextKeyHash(key);
|
|
501
1323
|
const origin = prevHashes.has(keyHash)
|
|
502
|
-
?
|
|
1324
|
+
? commitIndex.get(keyHash)
|
|
503
1325
|
: undefined;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
origin.
|
|
507
|
-
|
|
508
|
-
|
|
1326
|
+
const originFacts = origin === undefined ? undefined : facts[origin.entryIndex];
|
|
1327
|
+
if (origin !== undefined && originFacts !== undefined) {
|
|
1328
|
+
const successor = originFacts.addedHashes[origin.at + 1];
|
|
1329
|
+
const successorLast = origin.at + 2 === originFacts.addedHashes.length;
|
|
1330
|
+
if (successor !== undefined &&
|
|
1331
|
+
!successorLast &&
|
|
1332
|
+
originFacts.signers.some(signer => removedKeys.includes(signer))) {
|
|
1333
|
+
pending.claims.unshift(successor);
|
|
1334
|
+
ladderHashes.add(successor);
|
|
1335
|
+
}
|
|
509
1336
|
}
|
|
510
1337
|
}
|
|
511
|
-
else if (pending &&
|
|
1338
|
+
else if (pending &&
|
|
1339
|
+
!installsForeignInventory &&
|
|
1340
|
+
ladderSigned({ entry: log[index], ladderKeys })) {
|
|
512
1341
|
// The rung is still revealed AND signed this entry, so the hashes it
|
|
513
|
-
// commits were committed under the rung's authority
|
|
514
|
-
//
|
|
1342
|
+
// commits were committed under the rung's authority and join its
|
|
1343
|
+
// claims.
|
|
515
1344
|
// Signature, not mere presence in `updateKeys`, is what attributes
|
|
516
1345
|
// them: the rung's reveal outlives the ceremony that revealed it, so
|
|
517
1346
|
// claiming everything committed afterwards would sweep up hashes of
|
|
@@ -522,16 +1351,65 @@ export async function attributeLadderInventory({ log, anchorKeyMultibase, ladder
|
|
|
522
1351
|
ladderHashes.add(hash);
|
|
523
1352
|
}
|
|
524
1353
|
}
|
|
525
|
-
|
|
1354
|
+
// The claims are answered after the reveal branch, so a VM installed by
|
|
1355
|
+
// the very entry that reveals the rung signing it (the ladder-anchored
|
|
1356
|
+
// genesis, the ladder-VM install) is attributed to this ladder rather
|
|
1357
|
+
// than missed.
|
|
1358
|
+
// The co-introduction arm, under its three guards (see the header): one
|
|
1359
|
+
// new ladder VM, one newly introduced credential-class `keyAgreement`
|
|
1360
|
+
// member, and that member is this credential's. It is what reaches the
|
|
1361
|
+
// bind entry an enrolled client signs, which no rung stands behind.
|
|
1362
|
+
const coIntroduced = credentialVmId !== undefined &&
|
|
1363
|
+
newVmIds.length === 1 &&
|
|
1364
|
+
introduced.length === 1 &&
|
|
1365
|
+
introduced[0] === credentialVmId;
|
|
1366
|
+
// The COMMITMENT arm (see the header): the entry committed a hash this
|
|
1367
|
+
// ladder knows a priori, published one ladder VM, and introduced no other
|
|
1368
|
+
// credential's member. `derivedHashes` rather than `ladderHashes`: a hash
|
|
1369
|
+
// held on the evidence of the entry that committed it is not proof of
|
|
1370
|
+
// ownership, and reading one here would claim a VM on a claim the
|
|
1371
|
+
// completion may yet release. It needs `credentialVmId` in hand for the
|
|
1372
|
+
// same reason the co-introduction arm does: with no id, `introduced` is
|
|
1373
|
+
// empty and the foreign-member guard would pass vacuously.
|
|
1374
|
+
const commitmentClaimed = credentialVmId !== undefined &&
|
|
1375
|
+
newVmIds.length === 1 &&
|
|
1376
|
+
addedHashes.some(hash => derivedHashes.has(hash)) &&
|
|
1377
|
+
introduced.every(id => id === credentialVmId);
|
|
1378
|
+
// The SIGNER arm, under two guards of its own: a rung of this ladder
|
|
1379
|
+
// signed the entry that published the VM, the entry published exactly
|
|
1380
|
+
// ONE, AND it either introduced no credential-class member at all or
|
|
1381
|
+
// introduced this credential's own. What the last guard excludes is the
|
|
1382
|
+
// ladder-branch BIND entry, where the ACTING credential's rung signs an
|
|
1383
|
+
// entry installing a SUCCESSOR credential's whole inventory: without it
|
|
1384
|
+
// the acting ladder would be credited with the successor's VM, and every
|
|
1385
|
+
// later claim on either ladder would refuse as ambiguous.
|
|
1386
|
+
//
|
|
1387
|
+
// The one-VM guard is what the transient recovery's add-and-retire entry
|
|
1388
|
+
// meets on: it publishes the fresh credential's ladder VM and the
|
|
1389
|
+
// replacement code's together, and nothing in the log says which is
|
|
1390
|
+
// which -- a VM key and a rung key are independent expansions of a seed,
|
|
1391
|
+
// so no pairing exists to read. Claiming both would be silent,
|
|
1392
|
+
// unhealable damage: retiring the fresh credential would strike the
|
|
1393
|
+
// replacement code's VM and rot the bridge its record carries. So
|
|
1394
|
+
// neither is claimed, and the credential's own seed is what names its VM
|
|
1395
|
+
// (`decisions/0014`'s under-striking bias, applied to the VM axis).
|
|
1396
|
+
const signerClaimed = newVmIds.length === 1 &&
|
|
1397
|
+
ladderSigned({ entry: log[index], ladderKeys }) &&
|
|
1398
|
+
!foreignVmIntroduced &&
|
|
1399
|
+
(introduced.length === 0 ||
|
|
1400
|
+
(credentialVmId !== undefined && introduced.includes(credentialVmId)));
|
|
1401
|
+
for (const vmId of newVmIds) {
|
|
1402
|
+
ladderVmClaims.set(vmId, coIntroduced || commitmentClaimed || signerClaimed);
|
|
1403
|
+
}
|
|
1404
|
+
prevLadderVmIds = new Set(publishedVmIds);
|
|
1405
|
+
prevEntryDoc = entryDoc;
|
|
526
1406
|
prevHashes = new Set(entry.nextKeyHashes);
|
|
527
1407
|
}
|
|
528
|
-
const final =
|
|
529
|
-
updateKeys: [],
|
|
530
|
-
nextKeyHashes: []
|
|
531
|
-
};
|
|
1408
|
+
const final = currentLogParameters({ log });
|
|
532
1409
|
return {
|
|
533
1410
|
revealedKeys: final.updateKeys.filter(key => ladderKeys.has(key)),
|
|
534
|
-
committedHashes: final.nextKeyHashes.filter(hash => ladderHashes.has(hash))
|
|
1411
|
+
committedHashes: final.nextKeyHashes.filter(hash => ladderHashes.has(hash)),
|
|
1412
|
+
ladderVmIds: [...prevLadderVmIds].filter(vmId => ladderVmClaims.get(vmId) === true)
|
|
535
1413
|
};
|
|
536
1414
|
}
|
|
537
1415
|
//# sourceMappingURL=ladder.js.map
|