@serfab/cadre-core 1.16.0 → 1.18.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 +4 -3
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +81 -142
- package/dist/cadre-invite-protocol.js +168 -458
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +1089 -475
- package/dist/cadre-node.js +2170 -1037
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +88 -32
- package/dist/control-cohort.js +91 -33
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +856 -318
- package/dist/control-database.js +1422 -701
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +11 -4
- package/dist/control-protocol-guard.js +20 -5
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +97 -0
- package/dist/control-schema-version.js +225 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +262 -78
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +45 -10
- package/dist/control-write-retry.js +92 -22
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +27 -17
- package/dist/index.js +39 -17
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +29 -12
- package/dist/joined-strand-store.js +66 -27
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +256 -0
- package/dist/member-redemption.js +490 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +251 -0
- package/dist/owner-grant-protocol.js +344 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +89 -20
- package/dist/peer-join-backfill.js +265 -108
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +162 -133
- package/dist/seed-bootstrap.js +300 -283
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +24 -2
- package/dist/strand-database.js +29 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -88
- package/dist/strand-first-sync-gate.js +288 -121
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +27 -18
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +30 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +45 -22
- package/dist/strand-membership-writer.js +226 -131
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +36 -32
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +374 -150
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +17 -8
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +740 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +2402 -1079
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +204 -128
- package/src/control-database.ts +1749 -820
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +23 -6
- package/src/control-schema-version.ts +301 -0
- package/src/control-schema.ts +273 -78
- package/src/control-write-retry.ts +641 -567
- package/src/delegate-admission.ts +351 -314
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +899 -789
- package/src/joined-strand-store.ts +73 -28
- package/src/key-store.ts +10 -4
- package/src/link-budget.ts +9 -7
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +649 -0
- package/src/membership-view.ts +144 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +547 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-authorization.ts +558 -488
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +343 -349
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +352 -319
- package/src/strand-first-sync-gate.ts +334 -133
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +33 -25
- package/src/strand-membership-writer.ts +2093 -1974
- package/src/strand-move.ts +50 -38
- package/src/strand-peer-redial.ts +2 -3
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +391 -152
|
@@ -1,488 +1,558 @@
|
|
|
1
|
-
import debug from 'debug';
|
|
2
|
-
import { digest, verify } from '@optimystic/quereus-plugin-crypto';
|
|
3
|
-
import { controlAuthorizationFields, unboundAuthorizationFields, cadreInviteRowFields } from './control-authorization.js';
|
|
4
|
-
import type { BoundControlDomain, CadreInviteSignedFields, ControlAction, RevocableTable, UnboundControlDomain } from './control-authorization.js';
|
|
5
|
-
import { ed25519PublicKeyB64FromPeerId } from './ed25519-key.js';
|
|
6
|
-
import type { CadreInviteRow, CadreInviteUsageRow, CadrePeerVoucherFields, OwnerKeyRow, RevocationRow } from './types.js';
|
|
7
|
-
|
|
8
|
-
const log = debug('sereus:cadre:peer-authorization');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* base64url SHA-256 digest over the shared domain-tagged, party-bound field vector (see
|
|
12
|
-
* control-authorization.ts). The base64url twin of
|
|
13
|
-
* control-database.ts:buildAuthorizationMessage (which returns the same digest as raw
|
|
14
|
-
* bytes): sign either encoding with input encoding to match and the signed bytes agree.
|
|
15
|
-
* `partyId` is the party the verifier is configured for: a stored proof minted for
|
|
16
|
-
* another party fails here exactly as it fails the schema's `party_id()`-bound check.
|
|
17
|
-
*/
|
|
18
|
-
function taggedDigest(domain: BoundControlDomain, action: ControlAction, partyId: string, rowFields: string[]): string {
|
|
19
|
-
return digest(controlAuthorizationFields(domain, action, partyId, rowFields), 'sha256', 'base64url') as string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** {@link taggedDigest} for the two party-free digests (`UnboundControlDomain`). */
|
|
23
|
-
function unboundTaggedDigest(domain: UnboundControlDomain, action: ControlAction, rowFields: string[]): string {
|
|
24
|
-
return digest(unboundAuthorizationFields(domain, action, rowFields), 'sha256', 'base64url') as string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Canonical digest an owner signs to vouch a peer's ENROLLMENT — the offline
|
|
29
|
-
* credential `cadre enroll register` verifies. No table checks this digest, so it
|
|
30
|
-
* carries its own `'Cadre.Enrollment'` domain tag to stay disjoint from every
|
|
31
|
-
* CadreControl table rule (pre-tag it collided with the `DeviceToken` owner digests,
|
|
32
|
-
* so an enrollment vouch doubled as a push-token delete approval).
|
|
33
|
-
*
|
|
34
|
-
* Factored into one place so the producer (owner signing) and the verifier (the
|
|
35
|
-
* offline `cadre enroll register` check) can never drift apart — change the digest
|
|
36
|
-
* here and both move together.
|
|
37
|
-
*/
|
|
38
|
-
export function peerAuthorizationDigest(peerId: string): string {
|
|
39
|
-
return unboundTaggedDigest('Cadre.Enrollment', 'vouch', [peerId]);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The `CadreControl.DeviceToken` columns {@link deviceTokenAddDigest} binds, in the
|
|
44
|
-
* schema's order. A whole-row struct rather than six positional strings, so a caller
|
|
45
|
-
* cannot silently transpose `platform` and `token` (both opaque strings) and mint a
|
|
46
|
-
* signature over a row it did not mean to approve.
|
|
47
|
-
*
|
|
48
|
-
* Structurally satisfied by a `DeviceTokenRecord` spread with the row's `stampId`;
|
|
49
|
-
* `types.ts:DeviceTokenRecord` deliberately does NOT carry the stamp — it is the
|
|
50
|
-
* SELF-signed record shape, and the peer's own `Sig` does not cover `StampId`.
|
|
51
|
-
* `updatedAt` / `sig` are nullable because the columns are: both sign as `''` when
|
|
52
|
-
* absent, mirroring the schema's `coalesce(...)`.
|
|
53
|
-
*/
|
|
54
|
-
export interface DeviceTokenAuthorizedRow {
|
|
55
|
-
peerId: string;
|
|
56
|
-
platform: string;
|
|
57
|
-
token: string;
|
|
58
|
-
updatedAt: number | null;
|
|
59
|
-
sig: string | null;
|
|
60
|
-
stampId: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Canonical digest an owner signs to authorize a `DeviceToken` INSERT — the WHOLE row,
|
|
65
|
-
* ending in its single-use `StampId` nonce. SQL mirror:
|
|
66
|
-
* `digest('CadreControl.DeviceToken', 'add', party_id(), new.PeerId, new.Platform, new.Token,
|
|
67
|
-
* coalesce(cast(new.UpdatedAt as text), ''), coalesce(new.Sig, ''), new.StampId)` in
|
|
68
|
-
* `DeviceToken.AuthorizedInsert`.
|
|
69
|
-
*
|
|
70
|
-
* Binding every column means a captured approval can only ever reproduce the exact row
|
|
71
|
-
* it approved — never one carrying attacker-chosen `Platform`/`Token`/`UpdatedAt` — and
|
|
72
|
-
* binding the stamp makes it single-use: while the row lives the `unique` column blocks
|
|
73
|
-
* a replay, and after a clear the stamp is retired permanently into
|
|
74
|
-
* `CadreControl.Revocation` (`DeviceToken.NotRevoked`). This matters more here than for
|
|
75
|
-
* `CadrePeer`: a resurrected push token has NO freshness ceiling to retire it
|
|
76
|
-
* (`CadreNode.resolveDeviceToken` defaults `maxAgeMs` to infinity by design), so stamp
|
|
77
|
-
* retirement is the only thing that sticks.
|
|
78
|
-
*
|
|
79
|
-
* Distinct from {@link deviceTokenRemoveDigest} so a captured insert approval can never
|
|
80
|
-
* be replayed to delete the token, and vice versa.
|
|
81
|
-
*/
|
|
82
|
-
export function deviceTokenAddDigest(partyId: string, row: DeviceTokenAuthorizedRow): string {
|
|
83
|
-
return taggedDigest('CadreControl.DeviceToken', 'add', partyId, [
|
|
84
|
-
row.peerId,
|
|
85
|
-
row.platform,
|
|
86
|
-
row.token,
|
|
87
|
-
row.updatedAt === null ? '' : String(row.updatedAt),
|
|
88
|
-
row.sig ?? '',
|
|
89
|
-
row.stampId,
|
|
90
|
-
]);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Canonical digest an owner signs to authorize a `DeviceToken` DELETE, bound to the
|
|
95
|
-
* STORED row's (PeerId, StampId). SQL mirror:
|
|
96
|
-
* `digest('CadreControl.DeviceToken', 'remove', party_id(), old.PeerId, old.StampId)` in
|
|
97
|
-
* `DeviceToken.AuthorizedDelete`.
|
|
98
|
-
*
|
|
99
|
-
* A narrower vector than {@link deviceTokenAddDigest} on purpose (the same split
|
|
100
|
-
* `CadrePeer` makes): the clear approval names only which row is being retired, so it
|
|
101
|
-
* cannot be re-cut into an insert approval, and it is dead the moment the stamp it
|
|
102
|
-
* names is tombstoned.
|
|
103
|
-
*/
|
|
104
|
-
export function deviceTokenRemoveDigest(partyId: string, peerId: string, stampId: string): string {
|
|
105
|
-
return taggedDigest('CadreControl.DeviceToken', 'remove', partyId, [peerId, stampId]);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Canonical digest an owner signs to VOUCH a `CadrePeer` membership row (insert
|
|
110
|
-
* and the owner re-touch update — same semantics, deliberately the same digest).
|
|
111
|
-
* Binds the peer id to the row's single-use `StampId` nonce, so a captured signed
|
|
112
|
-
* insert cannot be replayed — while the row lives the `unique` column blocks it, and
|
|
113
|
-
* after a removal the stamp is retired permanently into `CadreControl.Revocation`
|
|
114
|
-
* (`CadrePeer.NotRevoked`) — and, because {@link cadrePeerRemoveDigest} scopes a
|
|
115
|
-
* DIFFERENT payload, the stored voucher (`VouchSig`) cannot be replayed to authorize
|
|
116
|
-
* a delete. The domain tag keeps the stored, replicated `VouchSig` useless against
|
|
117
|
-
* every OTHER table's rules.
|
|
118
|
-
*
|
|
119
|
-
* SQL mirror: `digest('CadreControl.CadrePeer', 'vouch', party_id(), new.PeerId, new.StampId)`.
|
|
120
|
-
*/
|
|
121
|
-
export function cadrePeerVoucherDigest(partyId: string, peerId: string, stampId: string): string {
|
|
122
|
-
return taggedDigest('CadreControl.CadrePeer', 'vouch', partyId, [peerId, stampId]);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Canonical digest an owner signs to REMOVE a `CadrePeer` row. Deliberately a
|
|
127
|
-
* distinct payload from {@link cadrePeerVoucherDigest} (the `'remove'` action tag)
|
|
128
|
-
* so the row's stored voucher — a signature over the voucher digest — can never
|
|
129
|
-
* satisfy this delete check. The signature is supplied in write context and never
|
|
130
|
-
* stored, so no reader can replay it; a captured remove is also dead after the
|
|
131
|
-
* delete lands, because a re-added row carries a FRESH `StampId` (the removed row's
|
|
132
|
-
* stamp is retired into `CadreControl.Revocation` and never reused).
|
|
133
|
-
*
|
|
134
|
-
* SQL mirror: `digest('CadreControl.CadrePeer', 'remove', party_id(), old.PeerId, old.StampId)`.
|
|
135
|
-
*/
|
|
136
|
-
export function cadrePeerRemoveDigest(partyId: string, peerId: string, stampId: string): string {
|
|
137
|
-
return taggedDigest('CadreControl.CadrePeer', 'remove', partyId, [peerId, stampId]);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Canonical digest an owner signs to APPEND a `CadreControl.Revocation` tombstone —
|
|
142
|
-
* the row retiring `stampId` for the named guarded table, and recording `rowKey`
|
|
143
|
-
* (the removed row's primary key: OwnerKey.Key / ValidationKey.Key /
|
|
144
|
-
* CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id) as which row was retired.
|
|
145
|
-
* SQL mirror:
|
|
146
|
-
* `digest('CadreControl.Revocation', 'remove', party_id(), new.TableName, new.RowKey, new.StampId)`
|
|
147
|
-
* in `Revocation.Authorized`.
|
|
148
|
-
*
|
|
149
|
-
* Its own `'CadreControl.Revocation'` domain tag makes it disjoint from the
|
|
150
|
-
* `'CadreControl.CadrePeer'` (or `OwnerKey` / `ValidationKey` / `Strand`)
|
|
151
|
-
* `'remove'` digest the same owner signs in the SAME transaction for the delete
|
|
152
|
-
* this tombstone accompanies — a removal signature is not a retirement
|
|
153
|
-
* signature and cannot be replayed as one.
|
|
154
|
-
*/
|
|
155
|
-
export function revocationDigest(partyId: string, tableName: RevocableTable, rowKey: string, stampId: string): string {
|
|
156
|
-
return taggedDigest('CadreControl.Revocation', 'remove', partyId, [tableName, rowKey, stampId]);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Does a `Revocation` row's stored signer pair (`SignerKey` / `SignerSig`) verify over the
|
|
161
|
-
* tombstone's own triple, for `partyId`? The read-side mirror of `Revocation.Authorized`,
|
|
162
|
-
* for a node that received the row by replication and must decide whether to honour it.
|
|
163
|
-
* Whether `row.signerKey` is an owner this node trusts is the caller's question, as for
|
|
164
|
-
* {@link verifyCadrePeerVoucher}. Never throws: a malformed row verifies as `false`.
|
|
165
|
-
*/
|
|
166
|
-
export function verifyRevocationSigner(partyId: string, row: RevocationRow): boolean {
|
|
167
|
-
try {
|
|
168
|
-
return verifyB64(revocationDigest(partyId, row.tableName, row.rowKey, row.stampId), row.signerSig, row.signerKey);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
log('verifyRevocationSigner failed: %o', error);
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Canonical digest an owner signs to seat a further `OwnerKey` row — the signed-add branch
|
|
177
|
-
* of `OwnerKey.Authorized`, which stores the pair as `VouchOwner` / `VouchSig`. SQL mirror:
|
|
178
|
-
* `digest('CadreControl.OwnerKey', 'add', party_id(), new.Key, new.StampId)`.
|
|
179
|
-
* digest needs no builder here: it is the generic guarded `'remove'` digest over
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
'
|
|
226
|
-
'base64url'
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
*
|
|
238
|
-
* `
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
'
|
|
266
|
-
'base64url'
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
*
|
|
316
|
-
* `
|
|
317
|
-
*
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
*
|
|
371
|
-
* `
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
* usage
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* `
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
*
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { digest, verify } from '@optimystic/quereus-plugin-crypto';
|
|
3
|
+
import { controlAuthorizationFields, unboundAuthorizationFields, cadreInviteRowFields, ownerGrantRowFields } from './control-authorization.js';
|
|
4
|
+
import type { BoundControlDomain, CadreInviteSignedFields, ControlAction, OwnerGrantSignedFields, RevocableTable, UnboundControlDomain } from './control-authorization.js';
|
|
5
|
+
import { ed25519PublicKeyB64FromPeerId } from './ed25519-key.js';
|
|
6
|
+
import type { CadreInviteRow, CadreInviteUsageRow, CadrePeerVoucherFields, OwnerGrantRow, OwnerGrantUsageRow, OwnerKeyRow, RevocationRow } from './types.js';
|
|
7
|
+
|
|
8
|
+
const log = debug('sereus:cadre:peer-authorization');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* base64url SHA-256 digest over the shared domain-tagged, party-bound field vector (see
|
|
12
|
+
* control-authorization.ts). The base64url twin of
|
|
13
|
+
* control-database.ts:buildAuthorizationMessage (which returns the same digest as raw
|
|
14
|
+
* bytes): sign either encoding with input encoding to match and the signed bytes agree.
|
|
15
|
+
* `partyId` is the party the verifier is configured for: a stored proof minted for
|
|
16
|
+
* another party fails here exactly as it fails the schema's `party_id()`-bound check.
|
|
17
|
+
*/
|
|
18
|
+
function taggedDigest(domain: BoundControlDomain, action: ControlAction, partyId: string, rowFields: string[]): string {
|
|
19
|
+
return digest(controlAuthorizationFields(domain, action, partyId, rowFields), 'sha256', 'base64url') as string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** {@link taggedDigest} for the two party-free digests (`UnboundControlDomain`). */
|
|
23
|
+
function unboundTaggedDigest(domain: UnboundControlDomain, action: ControlAction, rowFields: string[]): string {
|
|
24
|
+
return digest(unboundAuthorizationFields(domain, action, rowFields), 'sha256', 'base64url') as string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Canonical digest an owner signs to vouch a peer's ENROLLMENT — the offline
|
|
29
|
+
* credential `cadre enroll register` verifies. No table checks this digest, so it
|
|
30
|
+
* carries its own `'Cadre.Enrollment'` domain tag to stay disjoint from every
|
|
31
|
+
* CadreControl table rule (pre-tag it collided with the `DeviceToken` owner digests,
|
|
32
|
+
* so an enrollment vouch doubled as a push-token delete approval).
|
|
33
|
+
*
|
|
34
|
+
* Factored into one place so the producer (owner signing) and the verifier (the
|
|
35
|
+
* offline `cadre enroll register` check) can never drift apart — change the digest
|
|
36
|
+
* here and both move together.
|
|
37
|
+
*/
|
|
38
|
+
export function peerAuthorizationDigest(peerId: string): string {
|
|
39
|
+
return unboundTaggedDigest('Cadre.Enrollment', 'vouch', [peerId]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The `CadreControl.DeviceToken` columns {@link deviceTokenAddDigest} binds, in the
|
|
44
|
+
* schema's order. A whole-row struct rather than six positional strings, so a caller
|
|
45
|
+
* cannot silently transpose `platform` and `token` (both opaque strings) and mint a
|
|
46
|
+
* signature over a row it did not mean to approve.
|
|
47
|
+
*
|
|
48
|
+
* Structurally satisfied by a `DeviceTokenRecord` spread with the row's `stampId`;
|
|
49
|
+
* `types.ts:DeviceTokenRecord` deliberately does NOT carry the stamp — it is the
|
|
50
|
+
* SELF-signed record shape, and the peer's own `Sig` does not cover `StampId`.
|
|
51
|
+
* `updatedAt` / `sig` are nullable because the columns are: both sign as `''` when
|
|
52
|
+
* absent, mirroring the schema's `coalesce(...)`.
|
|
53
|
+
*/
|
|
54
|
+
export interface DeviceTokenAuthorizedRow {
|
|
55
|
+
peerId: string;
|
|
56
|
+
platform: string;
|
|
57
|
+
token: string;
|
|
58
|
+
updatedAt: number | null;
|
|
59
|
+
sig: string | null;
|
|
60
|
+
stampId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Canonical digest an owner signs to authorize a `DeviceToken` INSERT — the WHOLE row,
|
|
65
|
+
* ending in its single-use `StampId` nonce. SQL mirror:
|
|
66
|
+
* `digest('CadreControl.DeviceToken', 'add', party_id(), new.PeerId, new.Platform, new.Token,
|
|
67
|
+
* coalesce(cast(new.UpdatedAt as text), ''), coalesce(new.Sig, ''), new.StampId)` in
|
|
68
|
+
* `DeviceToken.AuthorizedInsert`.
|
|
69
|
+
*
|
|
70
|
+
* Binding every column means a captured approval can only ever reproduce the exact row
|
|
71
|
+
* it approved — never one carrying attacker-chosen `Platform`/`Token`/`UpdatedAt` — and
|
|
72
|
+
* binding the stamp makes it single-use: while the row lives the `unique` column blocks
|
|
73
|
+
* a replay, and after a clear the stamp is retired permanently into
|
|
74
|
+
* `CadreControl.Revocation` (`DeviceToken.NotRevoked`). This matters more here than for
|
|
75
|
+
* `CadrePeer`: a resurrected push token has NO freshness ceiling to retire it
|
|
76
|
+
* (`CadreNode.resolveDeviceToken` defaults `maxAgeMs` to infinity by design), so stamp
|
|
77
|
+
* retirement is the only thing that sticks.
|
|
78
|
+
*
|
|
79
|
+
* Distinct from {@link deviceTokenRemoveDigest} so a captured insert approval can never
|
|
80
|
+
* be replayed to delete the token, and vice versa.
|
|
81
|
+
*/
|
|
82
|
+
export function deviceTokenAddDigest(partyId: string, row: DeviceTokenAuthorizedRow): string {
|
|
83
|
+
return taggedDigest('CadreControl.DeviceToken', 'add', partyId, [
|
|
84
|
+
row.peerId,
|
|
85
|
+
row.platform,
|
|
86
|
+
row.token,
|
|
87
|
+
row.updatedAt === null ? '' : String(row.updatedAt),
|
|
88
|
+
row.sig ?? '',
|
|
89
|
+
row.stampId,
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Canonical digest an owner signs to authorize a `DeviceToken` DELETE, bound to the
|
|
95
|
+
* STORED row's (PeerId, StampId). SQL mirror:
|
|
96
|
+
* `digest('CadreControl.DeviceToken', 'remove', party_id(), old.PeerId, old.StampId)` in
|
|
97
|
+
* `DeviceToken.AuthorizedDelete`.
|
|
98
|
+
*
|
|
99
|
+
* A narrower vector than {@link deviceTokenAddDigest} on purpose (the same split
|
|
100
|
+
* `CadrePeer` makes): the clear approval names only which row is being retired, so it
|
|
101
|
+
* cannot be re-cut into an insert approval, and it is dead the moment the stamp it
|
|
102
|
+
* names is tombstoned.
|
|
103
|
+
*/
|
|
104
|
+
export function deviceTokenRemoveDigest(partyId: string, peerId: string, stampId: string): string {
|
|
105
|
+
return taggedDigest('CadreControl.DeviceToken', 'remove', partyId, [peerId, stampId]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Canonical digest an owner signs to VOUCH a `CadrePeer` membership row (insert
|
|
110
|
+
* and the owner re-touch update — same semantics, deliberately the same digest).
|
|
111
|
+
* Binds the peer id to the row's single-use `StampId` nonce, so a captured signed
|
|
112
|
+
* insert cannot be replayed — while the row lives the `unique` column blocks it, and
|
|
113
|
+
* after a removal the stamp is retired permanently into `CadreControl.Revocation`
|
|
114
|
+
* (`CadrePeer.NotRevoked`) — and, because {@link cadrePeerRemoveDigest} scopes a
|
|
115
|
+
* DIFFERENT payload, the stored voucher (`VouchSig`) cannot be replayed to authorize
|
|
116
|
+
* a delete. The domain tag keeps the stored, replicated `VouchSig` useless against
|
|
117
|
+
* every OTHER table's rules.
|
|
118
|
+
*
|
|
119
|
+
* SQL mirror: `digest('CadreControl.CadrePeer', 'vouch', party_id(), new.PeerId, new.StampId)`.
|
|
120
|
+
*/
|
|
121
|
+
export function cadrePeerVoucherDigest(partyId: string, peerId: string, stampId: string): string {
|
|
122
|
+
return taggedDigest('CadreControl.CadrePeer', 'vouch', partyId, [peerId, stampId]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Canonical digest an owner signs to REMOVE a `CadrePeer` row. Deliberately a
|
|
127
|
+
* distinct payload from {@link cadrePeerVoucherDigest} (the `'remove'` action tag)
|
|
128
|
+
* so the row's stored voucher — a signature over the voucher digest — can never
|
|
129
|
+
* satisfy this delete check. The signature is supplied in write context and never
|
|
130
|
+
* stored, so no reader can replay it; a captured remove is also dead after the
|
|
131
|
+
* delete lands, because a re-added row carries a FRESH `StampId` (the removed row's
|
|
132
|
+
* stamp is retired into `CadreControl.Revocation` and never reused).
|
|
133
|
+
*
|
|
134
|
+
* SQL mirror: `digest('CadreControl.CadrePeer', 'remove', party_id(), old.PeerId, old.StampId)`.
|
|
135
|
+
*/
|
|
136
|
+
export function cadrePeerRemoveDigest(partyId: string, peerId: string, stampId: string): string {
|
|
137
|
+
return taggedDigest('CadreControl.CadrePeer', 'remove', partyId, [peerId, stampId]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Canonical digest an owner signs to APPEND a `CadreControl.Revocation` tombstone —
|
|
142
|
+
* the row retiring `stampId` for the named guarded table, and recording `rowKey`
|
|
143
|
+
* (the removed row's primary key: OwnerKey.Key / ValidationKey.Key /
|
|
144
|
+
* CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id) as which row was retired.
|
|
145
|
+
* SQL mirror:
|
|
146
|
+
* `digest('CadreControl.Revocation', 'remove', party_id(), new.TableName, new.RowKey, new.StampId)`
|
|
147
|
+
* in `Revocation.Authorized`.
|
|
148
|
+
*
|
|
149
|
+
* Its own `'CadreControl.Revocation'` domain tag makes it disjoint from the
|
|
150
|
+
* `'CadreControl.CadrePeer'` (or `OwnerKey` / `ValidationKey` / `Strand`)
|
|
151
|
+
* `'remove'` digest the same owner signs in the SAME transaction for the delete
|
|
152
|
+
* this tombstone accompanies — a removal signature is not a retirement
|
|
153
|
+
* signature and cannot be replayed as one.
|
|
154
|
+
*/
|
|
155
|
+
export function revocationDigest(partyId: string, tableName: RevocableTable, rowKey: string, stampId: string): string {
|
|
156
|
+
return taggedDigest('CadreControl.Revocation', 'remove', partyId, [tableName, rowKey, stampId]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Does a `Revocation` row's stored signer pair (`SignerKey` / `SignerSig`) verify over the
|
|
161
|
+
* tombstone's own triple, for `partyId`? The read-side mirror of `Revocation.Authorized`,
|
|
162
|
+
* for a node that received the row by replication and must decide whether to honour it.
|
|
163
|
+
* Whether `row.signerKey` is an owner this node trusts is the caller's question, as for
|
|
164
|
+
* {@link verifyCadrePeerVoucher}. Never throws: a malformed row verifies as `false`.
|
|
165
|
+
*/
|
|
166
|
+
export function verifyRevocationSigner(partyId: string, row: RevocationRow): boolean {
|
|
167
|
+
try {
|
|
168
|
+
return verifyB64(revocationDigest(partyId, row.tableName, row.rowKey, row.stampId), row.signerSig, row.signerKey);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
log('verifyRevocationSigner failed: %o', error);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Canonical digest an owner signs to seat a further `OwnerKey` row — the signed-add branch
|
|
177
|
+
* of `OwnerKey.Authorized`, which stores the pair as `VouchOwner` / `VouchSig`. SQL mirror:
|
|
178
|
+
* `digest('CadreControl.OwnerKey', 'add', party_id(), new.Key, coalesce(new.Label, ''), new.StampId)`.
|
|
179
|
+
* The removal digest needs no builder here: it is the generic guarded `'remove'` digest over
|
|
180
|
+
* (Key, StampId).
|
|
181
|
+
*/
|
|
182
|
+
export function ownerKeyAddDigest(partyId: string, key: string, label: string | null, stampId: string): string {
|
|
183
|
+
return taggedDigest('CadreControl.OwnerKey', 'add', partyId, [key, label ?? '', stampId]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Does an owner-signed `OwnerKey` row's stored voucher verify — `vouchSig` by `vouchOwner`
|
|
188
|
+
* over {@link ownerKeyAddDigest} for (`partyId`, `key`, `label`, `stampId`)? Whether
|
|
189
|
+
* `vouchOwner` is anchored is the caller's question, as for {@link verifyCadrePeerVoucher}.
|
|
190
|
+
* Never throws.
|
|
191
|
+
*/
|
|
192
|
+
export function verifyOwnerKeyVoucher(partyId: string, key: string, label: string | null, stampId: string, vouchOwner: string, vouchSig: string): boolean {
|
|
193
|
+
try {
|
|
194
|
+
return verifyB64(ownerKeyAddDigest(partyId, key, label, stampId), vouchSig, vouchOwner);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
log('verifyOwnerKeyVoucher failed: %o', error);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Verify that `signature` is a valid owner ed25519 signature over `peerId`'s
|
|
203
|
+
* authorization digest, using `ownerPublicKey` (base64url).
|
|
204
|
+
*
|
|
205
|
+
* This is the mirror of the signing done in
|
|
206
|
+
* {@link SeedBootstrapService.authorizePeer}: it checks the signature against
|
|
207
|
+
* {@link peerAuthorizationDigest}. A `true` result means the holder of the
|
|
208
|
+
* owner private key vouched for this peer ID — it does NOT mean the peer is
|
|
209
|
+
* registered anywhere.
|
|
210
|
+
*
|
|
211
|
+
* Returns a boolean and never throws: malformed base64url, a bad/garbage key, or
|
|
212
|
+
* any crypto failure resolves to `false` (callers want a verdict, not an
|
|
213
|
+
* exception). The catch is logged at debug.
|
|
214
|
+
*/
|
|
215
|
+
export function verifyPeerAuthorization(
|
|
216
|
+
peerId: string,
|
|
217
|
+
ownerPublicKey: string,
|
|
218
|
+
signature: string
|
|
219
|
+
): boolean {
|
|
220
|
+
try {
|
|
221
|
+
return verify(
|
|
222
|
+
peerAuthorizationDigest(peerId),
|
|
223
|
+
signature,
|
|
224
|
+
ownerPublicKey,
|
|
225
|
+
'ed25519',
|
|
226
|
+
'base64url',
|
|
227
|
+
'base64url',
|
|
228
|
+
'base64url'
|
|
229
|
+
);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
log('verifyPeerAuthorization failed: %o', error);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Verify that `signature` is a valid owner ed25519 signature over the
|
|
238
|
+
* `CadrePeer` voucher digest for (`partyId`, `peerId`, `stampId`) — the read-side
|
|
239
|
+
* mirror of the voucher {@link ControlDatabase.insertCadrePeer} signs and persists into
|
|
240
|
+
* `VouchOwner`/`VouchSig` (see {@link cadrePeerVoucherDigest}).
|
|
241
|
+
*
|
|
242
|
+
* A `true` result means the holder of `ownerPublicKey` vouched THIS membership
|
|
243
|
+
* row (the peer id bound to the row's single-use `StampId` nonce) in THIS party: a
|
|
244
|
+
* voucher copied from another party's table that shares the owner key fails. It says
|
|
245
|
+
* nothing about whether that owner key is itself trustworthy — the caller must
|
|
246
|
+
* separately check the key against the node-local trusted-owner anchor
|
|
247
|
+
* (`TrustedOwnerStore`), never the replicated `OwnerKey` table.
|
|
248
|
+
*
|
|
249
|
+
* Returns a boolean and never throws (same contract as
|
|
250
|
+
* {@link verifyPeerAuthorization}): malformed input or any crypto failure
|
|
251
|
+
* resolves to `false`, logged at debug.
|
|
252
|
+
*/
|
|
253
|
+
export function verifyCadrePeerVoucher(
|
|
254
|
+
partyId: string,
|
|
255
|
+
peerId: string,
|
|
256
|
+
stampId: string,
|
|
257
|
+
ownerPublicKey: string,
|
|
258
|
+
signature: string
|
|
259
|
+
): boolean {
|
|
260
|
+
try {
|
|
261
|
+
return verify(
|
|
262
|
+
cadrePeerVoucherDigest(partyId, peerId, stampId),
|
|
263
|
+
signature,
|
|
264
|
+
ownerPublicKey,
|
|
265
|
+
'ed25519',
|
|
266
|
+
'base64url',
|
|
267
|
+
'base64url',
|
|
268
|
+
'base64url'
|
|
269
|
+
);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
log('verifyCadrePeerVoucher failed: %o', error);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Canonical digest a JOINING peer signs to consent to ONE `FormationUsage`
|
|
278
|
+
* redemption — the read-side mirror of `formationConsentMessage` in
|
|
279
|
+
* control-database.ts, base64url-encoded instead of raw bytes (see
|
|
280
|
+
* {@link unboundTaggedDigest}). That doc comment carries the field-vector rationale
|
|
281
|
+
* (notably why neither `strandId` nor the party id is signed); the two vectors must not
|
|
282
|
+
* drift, which peer-authorization.spec.ts pins by signing one form and verifying the other.
|
|
283
|
+
*/
|
|
284
|
+
export function formationConsentDigest(
|
|
285
|
+
token: string, usageStampId: string, peerKey: string, disclosure: string
|
|
286
|
+
): string {
|
|
287
|
+
return unboundTaggedDigest('CadreControl.FormationUsage', 'consent', [token, usageStampId, peerKey, disclosure]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Verify that `row.peerSig` is a valid ed25519 signature over the joining peer's
|
|
292
|
+
* consent digest (see {@link formationConsentDigest}) — the row's OWN `peerKey`.
|
|
293
|
+
*
|
|
294
|
+
* Unlike {@link verifyCadrePeerVoucher} there is no separate enrolled/owner row to
|
|
295
|
+
* look up: the identity IS the key carried on the row, so a forged consent would
|
|
296
|
+
* need that joiner's own private key. Returns a boolean and never throws (same
|
|
297
|
+
* contract as the siblings above): malformed input or any crypto failure resolves
|
|
298
|
+
* to `false`, logged at debug.
|
|
299
|
+
*/
|
|
300
|
+
export function verifyFormationConsent(row: {
|
|
301
|
+
token: string; usageStampId: string; peerKey: string; disclosure: string; peerSig: string;
|
|
302
|
+
}): boolean {
|
|
303
|
+
try {
|
|
304
|
+
return verify(
|
|
305
|
+
formationConsentDigest(row.token, row.usageStampId, row.peerKey, row.disclosure),
|
|
306
|
+
row.peerSig, row.peerKey, 'ed25519', 'base64url', 'base64url', 'base64url'
|
|
307
|
+
);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
log('verifyFormationConsent failed: %o', error);
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Canonical digest an owner signs to seat a `CadreInvite` row — the read-side mirror
|
|
316
|
+
* of `cadreInviteAddMessage` in control-database.ts, base64url instead of raw bytes
|
|
317
|
+
* (see {@link taggedDigest}). The whole row is bound, nullable columns as `''` and
|
|
318
|
+
* `ExpiresAt` in its stored canonical form, through the one field builder both sides
|
|
319
|
+
* share (`cadreInviteRowFields`). SQL mirror: `CadreInvite.AuthorizedInsert`.
|
|
320
|
+
*/
|
|
321
|
+
export function cadreInviteAddDigest(partyId: string, row: CadreInviteSignedFields): string {
|
|
322
|
+
return taggedDigest('CadreControl.CadreInvite', 'add', partyId, cadreInviteRowFields(row));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Canonical digest the HOLDER of a `CadreInvite`'s private key signs to redeem it once:
|
|
327
|
+
* proof of possession, verified against the invitation key itself
|
|
328
|
+
* (`CadreInviteUsage.InvitePossessed`). Binds the redemption's single-use nonce and the
|
|
329
|
+
* device being admitted, so one redemption cannot be re-presented for another device
|
|
330
|
+
* or another use. Mirror of `cadreInviteRedeemMessage` in control-database.ts.
|
|
331
|
+
*/
|
|
332
|
+
export function cadreInviteRedeemDigest(partyId: string, inviteKey: string, usageStampId: string, peerKey: string): string {
|
|
333
|
+
return taggedDigest('CadreControl.CadreInviteUsage', 'redeem', partyId, [inviteKey, usageStampId, peerKey]);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Canonical digest the DEVICE signs to consent to being admitted by one redemption
|
|
338
|
+
* (`CadreInviteUsage.PeerConsented`), over the same fields as
|
|
339
|
+
* {@link cadreInviteRedeemDigest} under a distinct action tag, so the holder's and
|
|
340
|
+
* the device's signatures are never interchangeable. Mirror of
|
|
341
|
+
* `cadreInviteConsentMessage` in control-database.ts.
|
|
342
|
+
*/
|
|
343
|
+
export function cadreInviteConsentDigest(partyId: string, inviteKey: string, usageStampId: string, peerKey: string): string {
|
|
344
|
+
return taggedDigest('CadreControl.CadreInviteUsage', 'consent', partyId, [inviteKey, usageStampId, peerKey]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** ed25519 verify over a base64url digest, with the siblings' never-throws contract left to the caller. */
|
|
348
|
+
function verifyB64(digestB64: string, signature: string, publicKey: string): boolean {
|
|
349
|
+
return verify(digestB64, signature, publicKey, 'ed25519', 'base64url', 'base64url', 'base64url');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Does a `CadreInvite` row carry a valid `'add'` signature by its own `issuerKey`, for
|
|
354
|
+
* `partyId`? The redeeming device runs this over the row a member replies with, after
|
|
355
|
+
* checking the issuer against its pinned owner keys and the reply's party against the
|
|
356
|
+
* invitation's, and a member over a bundle's copy it does not hold yet
|
|
357
|
+
* (`ControlDatabase.isCadreInviteLive`); whether that issuer IS an owner is the caller's question.
|
|
358
|
+
* Never throws: a malformed row verifies as `false`.
|
|
359
|
+
*/
|
|
360
|
+
export function verifyCadreInviteRow(partyId: string, invite: CadreInviteRow): boolean {
|
|
361
|
+
try {
|
|
362
|
+
return verifyB64(cadreInviteAddDigest(partyId, invite), invite.issuerSig, invite.issuerKey);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
log('verifyCadreInviteRow failed: %o', error);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The two signatures a redemption carries, checked before any database work: the holder's
|
|
371
|
+
* `'redeem'` signature with the invitation key, and the device's `'consent'` signature with
|
|
372
|
+
* its own key, both over (`inviteKey`, `usageStampId`, `peerKey`). The pre-check twin of
|
|
373
|
+
* `CadreInviteUsage.InvitePossessed` / `PeerConsented`, as `verifyFormationConsent` is for
|
|
374
|
+
* formation: the schema stays the authority, this turns a bad signature into a clean
|
|
375
|
+
* refusal instead of a constraint failure at commit. Never throws.
|
|
376
|
+
*/
|
|
377
|
+
export function verifyCadreInviteRedemption(
|
|
378
|
+
partyId: string,
|
|
379
|
+
fields: { inviteKey: string; usageStampId: string; peerKey: string },
|
|
380
|
+
inviteSig: string,
|
|
381
|
+
peerSig: string,
|
|
382
|
+
): boolean {
|
|
383
|
+
try {
|
|
384
|
+
return verifyB64(cadreInviteRedeemDigest(partyId, fields.inviteKey, fields.usageStampId, fields.peerKey), inviteSig, fields.inviteKey)
|
|
385
|
+
&& verifyB64(cadreInviteConsentDigest(partyId, fields.inviteKey, fields.usageStampId, fields.peerKey), peerSig, fields.peerKey);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
log('verifyCadreInviteRedemption failed: %o', error);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Is an invitation-admitted `CadrePeer` row (`vouchSig` null, `vouchUsage` set) a
|
|
394
|
+
* member this node should trust? The read-side mirror of the consent branch of
|
|
395
|
+
* `CadrePeer.AuthorizedInsert`, re-checked against THIS node's anchor because the
|
|
396
|
+
* replicated `OwnerKey` table can be polluted. The chain is
|
|
397
|
+
*
|
|
398
|
+
* row --vouchUsage--> usage --inviteKey--> invitation --issuerKey--> anchored owner
|
|
399
|
+
*
|
|
400
|
+
* and every link is verified: the issuer is anchored (`isAnchored`) and is the row's
|
|
401
|
+
* `vouchOwner`; the usage is the row's `vouchUsage`, names this peer id and this exact row
|
|
402
|
+
* incarnation (`peerStampId`), and names this invitation; a targeted invitation names the
|
|
403
|
+
* redeeming device; the stored `peerKey` really is the key behind the usage's `peerId` (the
|
|
404
|
+
* schema cannot unwrap a multihash, so a writer could assert any pair); the invitation's
|
|
405
|
+
* stored `'add'` signature verifies over the row rebuilt from its columns; the holder's
|
|
406
|
+
* `'redeem'` signature verifies with the invitation key and the device's `'consent'`
|
|
407
|
+
* signature with its own key. All three signatures are judged for `partyId`. A machine
|
|
408
|
+
* invitation admits a machine and nothing else, so no `OwnerKey` row is ever verified
|
|
409
|
+
* through this chain; a grant-seated owner row is {@link verifyOwnerGrantAdmission}'s.
|
|
410
|
+
*
|
|
411
|
+
* Expiry, use count and withdrawal are deliberately NOT re-checked: they were conditions
|
|
412
|
+
* at redemption (`CadreInviteUsage.Authorized`), and membership persists until an owner
|
|
413
|
+
* removes the row — exactly as an owner-vouched row's membership does. Keeping a device
|
|
414
|
+
* out is removing its row AND withdrawing the invitation.
|
|
415
|
+
*
|
|
416
|
+
* Returns a boolean and never throws (the siblings' contract): malformed input or any
|
|
417
|
+
* crypto failure resolves to `false`, logged at debug.
|
|
418
|
+
*/
|
|
419
|
+
export function verifyInvitationAdmission(
|
|
420
|
+
partyId: string,
|
|
421
|
+
row: CadrePeerVoucherFields,
|
|
422
|
+
usage: CadreInviteUsageRow,
|
|
423
|
+
invite: CadreInviteRow,
|
|
424
|
+
isAnchored: (ownerKey: string) => boolean,
|
|
425
|
+
): boolean {
|
|
426
|
+
try {
|
|
427
|
+
return row.stampId !== null
|
|
428
|
+
&& row.vouchOwner !== null
|
|
429
|
+
&& row.vouchUsage !== null
|
|
430
|
+
&& isAnchored(row.vouchOwner)
|
|
431
|
+
&& invite.issuerKey === row.vouchOwner
|
|
432
|
+
&& usage.usageStampId === row.vouchUsage
|
|
433
|
+
&& usage.peerStampId === row.stampId
|
|
434
|
+
&& usage.peerId === row.peerId
|
|
435
|
+
&& usage.inviteKey === invite.key
|
|
436
|
+
&& (invite.peerId === null || invite.peerId === usage.peerId)
|
|
437
|
+
&& ed25519PublicKeyB64FromPeerId(usage.peerId) === usage.peerKey
|
|
438
|
+
&& verifyB64(cadreInviteAddDigest(partyId, invite), invite.issuerSig, invite.issuerKey)
|
|
439
|
+
&& verifyB64(cadreInviteRedeemDigest(partyId, invite.key, usage.usageStampId, usage.peerKey), usage.inviteSig, invite.key)
|
|
440
|
+
&& verifyB64(cadreInviteConsentDigest(partyId, invite.key, usage.usageStampId, usage.peerKey), usage.peerSig, usage.peerKey);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
log('verifyInvitationAdmission failed: %o', error);
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Canonical digest an owner signs to seat an `OwnerGrant` row — the read-side mirror of
|
|
449
|
+
* `ownerGrantAddMessage` in control-database.ts, base64url instead of raw bytes. The whole
|
|
450
|
+
* row is bound, the label included, through the one field builder both sides share
|
|
451
|
+
* (`ownerGrantRowFields`). SQL mirror: `OwnerGrant.AuthorizedInsert`.
|
|
452
|
+
*/
|
|
453
|
+
export function ownerGrantAddDigest(partyId: string, row: OwnerGrantSignedFields): string {
|
|
454
|
+
return taggedDigest('CadreControl.OwnerGrant', 'add', partyId, ownerGrantRowFields(row));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Canonical digest the HOLDER of an `OwnerGrant`'s private key signs to redeem it once:
|
|
459
|
+
* proof of possession, verified against the grant key itself
|
|
460
|
+
* (`OwnerGrantUsage.GrantPossessed`). Binds the redemption's nonce and the key being seated,
|
|
461
|
+
* so one redemption cannot be re-presented for another key or another use. Mirror of
|
|
462
|
+
* `ownerGrantRedeemMessage` in control-database.ts.
|
|
463
|
+
*/
|
|
464
|
+
export function ownerGrantRedeemDigest(partyId: string, grantKey: string, usageStampId: string, seatedKey: string): string {
|
|
465
|
+
return taggedDigest('CadreControl.OwnerGrantUsage', 'redeem', partyId, [grantKey, usageStampId, seatedKey]);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Canonical digest the key being SEATED signs to prove it is held by the redeemer
|
|
470
|
+
* (`OwnerGrantUsage.OwnerConsented`), over the same fields as {@link ownerGrantRedeemDigest}
|
|
471
|
+
* under a distinct action tag, so the holder's and the key's signatures are never
|
|
472
|
+
* interchangeable. Mirror of `ownerGrantConsentMessage` in control-database.ts.
|
|
473
|
+
*/
|
|
474
|
+
export function ownerGrantConsentDigest(partyId: string, grantKey: string, usageStampId: string, seatedKey: string): string {
|
|
475
|
+
return taggedDigest('CadreControl.OwnerGrantUsage', 'consent', partyId, [grantKey, usageStampId, seatedKey]);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Does an `OwnerGrant` row carry a valid `'add'` signature by its own `issuerKey`, for
|
|
480
|
+
* `partyId`? The grant twin of {@link verifyCadreInviteRow}, with the same contract: whether
|
|
481
|
+
* that issuer IS an owner is the caller's question, and a malformed row verifies as `false`.
|
|
482
|
+
*/
|
|
483
|
+
export function verifyOwnerGrantRow(partyId: string, grant: OwnerGrantRow): boolean {
|
|
484
|
+
try {
|
|
485
|
+
return verifyB64(ownerGrantAddDigest(partyId, grant), grant.issuerSig, grant.issuerKey);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
log('verifyOwnerGrantRow failed: %o', error);
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The two signatures an owner-grant redemption carries, checked before any database work:
|
|
494
|
+
* the holder's `'redeem'` signature with the grant key and the seated key's `'consent'`
|
|
495
|
+
* signature with itself, both over (`grantKey`, `usageStampId`, `seatedKey`). The grant twin
|
|
496
|
+
* of {@link verifyCadreInviteRedemption}, with the same contract: the schema
|
|
497
|
+
* (`OwnerGrantUsage.GrantPossessed` / `OwnerConsented`) stays the authority, this turns a bad
|
|
498
|
+
* signature into a clean refusal instead of a constraint failure at commit. Never throws.
|
|
499
|
+
*/
|
|
500
|
+
export function verifyOwnerGrantRedemption(
|
|
501
|
+
partyId: string,
|
|
502
|
+
fields: { grantKey: string; usageStampId: string; seatedKey: string },
|
|
503
|
+
grantSig: string,
|
|
504
|
+
ownerSig: string,
|
|
505
|
+
): boolean {
|
|
506
|
+
try {
|
|
507
|
+
return verifyB64(ownerGrantRedeemDigest(partyId, fields.grantKey, fields.usageStampId, fields.seatedKey), grantSig, fields.grantKey)
|
|
508
|
+
&& verifyB64(ownerGrantConsentDigest(partyId, fields.grantKey, fields.usageStampId, fields.seatedKey), ownerSig, fields.seatedKey);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
log('verifyOwnerGrantRedemption failed: %o', error);
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Is a grant-seated `OwnerKey` row (`vouchSig` null, `vouchUsage` naming an `OwnerGrantUsage`
|
|
517
|
+
* row) an owner this node should derive into its anchor? The read-side mirror of the grant
|
|
518
|
+
* branch of `OwnerKey.Authorized` (`owner-anchor-sync.ts` → `deriveOwnerAnchor`). The chain is
|
|
519
|
+
*
|
|
520
|
+
* row --vouchUsage--> usage --grantKey--> grant --issuerKey--> anchored owner
|
|
521
|
+
*
|
|
522
|
+
* and every link is verified: the issuer is anchored and is the row's `vouchOwner`; the usage
|
|
523
|
+
* is the row's `vouchUsage` and names this key and this exact row incarnation
|
|
524
|
+
* (`seatedStampId`); the usage names this grant; the row carries the grant's label; the
|
|
525
|
+
* grant's stored `'add'` signature verifies over the row rebuilt from its columns; the
|
|
526
|
+
* holder's `'redeem'` signature verifies with the grant key and the seated key's `'consent'`
|
|
527
|
+
* signature with itself. All three signatures are judged for `partyId`.
|
|
528
|
+
*
|
|
529
|
+
* Expiry, use count and withdrawal are deliberately NOT re-checked, as
|
|
530
|
+
* {@link verifyInvitationAdmission} omits them: they were conditions at redemption
|
|
531
|
+
* (`OwnerGrantUsage.Authorized`), and ownership persists until an owner removes the row.
|
|
532
|
+
* Same never-throws contract as the siblings.
|
|
533
|
+
*/
|
|
534
|
+
export function verifyOwnerGrantAdmission(
|
|
535
|
+
partyId: string,
|
|
536
|
+
row: OwnerKeyRow,
|
|
537
|
+
usage: OwnerGrantUsageRow,
|
|
538
|
+
grant: OwnerGrantRow,
|
|
539
|
+
isAnchored: (ownerKey: string) => boolean,
|
|
540
|
+
): boolean {
|
|
541
|
+
try {
|
|
542
|
+
return row.vouchOwner !== null
|
|
543
|
+
&& row.vouchUsage !== null
|
|
544
|
+
&& isAnchored(row.vouchOwner)
|
|
545
|
+
&& grant.issuerKey === row.vouchOwner
|
|
546
|
+
&& usage.usageStampId === row.vouchUsage
|
|
547
|
+
&& usage.seatedKey === row.key
|
|
548
|
+
&& usage.seatedStampId === row.stampId
|
|
549
|
+
&& usage.grantKey === grant.key
|
|
550
|
+
&& (grant.label ?? '') === (row.label ?? '')
|
|
551
|
+
&& verifyB64(ownerGrantAddDigest(partyId, grant), grant.issuerSig, grant.issuerKey)
|
|
552
|
+
&& verifyB64(ownerGrantRedeemDigest(partyId, grant.key, usage.usageStampId, usage.seatedKey), usage.grantSig, grant.key)
|
|
553
|
+
&& verifyB64(ownerGrantConsentDigest(partyId, grant.key, usage.usageStampId, usage.seatedKey), usage.ownerSig, usage.seatedKey);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
log('verifyOwnerGrantAdmission failed: %o', error);
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
}
|