@serfab/cadre-core 1.13.0 → 1.15.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 +293 -274
- package/dist/cadre-invite-protocol.d.ts +383 -0
- package/dist/cadre-invite-protocol.js +774 -0
- package/dist/cadre-invite-protocol.js.map +1 -0
- package/dist/cadre-join-request.d.ts +42 -0
- package/dist/cadre-join-request.js +144 -0
- package/dist/cadre-join-request.js.map +1 -0
- package/dist/cadre-node.d.ts +849 -210
- package/dist/cadre-node.js +1574 -374
- package/dist/cadre-node.js.map +1 -1
- package/dist/claim-proof.d.ts +45 -0
- package/dist/claim-proof.js +132 -0
- package/dist/claim-proof.js.map +1 -0
- package/dist/control-authorization.d.ts +80 -19
- package/dist/control-authorization.js +55 -13
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.js +1 -1
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +605 -108
- package/dist/control-database.js +1299 -280
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +142 -0
- package/dist/control-protocol-guard.js +293 -0
- package/dist/control-protocol-guard.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +817 -1067
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +4 -0
- package/dist/control-stream.js +22 -7
- package/dist/control-stream.js.map +1 -1
- package/dist/delegate-admission.d.ts +1 -1
- package/dist/delegate-admission.js +1 -1
- package/dist/device-token.d.ts +11 -9
- package/dist/device-token.js +14 -12
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +22 -9
- package/dist/ed25519-key.js +47 -10
- package/dist/ed25519-key.js.map +1 -1
- package/dist/fetch-impl.d.ts +12 -0
- package/dist/fetch-impl.js +23 -0
- package/dist/fetch-impl.js.map +1 -0
- package/dist/file-durable-slot.js +2 -1
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/file-safe-name.d.ts +25 -0
- package/dist/file-safe-name.js +29 -0
- package/dist/file-safe-name.js.map +1 -0
- package/dist/formation-approval.js +5 -11
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +0 -19
- package/dist/fs-atomic.js +3 -24
- package/dist/fs-atomic.js.map +1 -1
- package/dist/index.d.ts +22 -10
- package/dist/index.js +54 -11
- package/dist/index.js.map +1 -1
- package/dist/invitation-bootstrap.d.ts +50 -12
- package/dist/invitation-bootstrap.js +76 -13
- package/dist/invitation-bootstrap.js.map +1 -1
- package/dist/joined-strand-store.d.ts +7 -0
- package/dist/joined-strand-store.js +9 -0
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +2 -1
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +25 -33
- package/dist/link-budget.js +25 -35
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +78 -0
- package/dist/machine-removal.js +166 -0
- package/dist/machine-removal.js.map +1 -0
- package/dist/membership-connection-gater.d.ts +191 -151
- package/dist/membership-connection-gater.js +236 -173
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-claim-payload.d.ts +74 -0
- package/dist/node-claim-payload.js +224 -0
- package/dist/node-claim-payload.js.map +1 -0
- package/dist/node-default-transports.d.ts +27 -0
- package/dist/node-default-transports.js +35 -0
- package/dist/node-default-transports.js.map +1 -0
- package/dist/node-transports.d.ts +30 -0
- package/dist/node-transports.js +40 -0
- package/dist/node-transports.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +22 -0
- package/dist/owner-anchor-sync.js +143 -0
- package/dist/owner-anchor-sync.js.map +1 -0
- package/dist/peer-authorization.d.ts +113 -18
- package/dist/peer-authorization.js +225 -29
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -27
- package/dist/peer-dial.js +276 -31
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +26 -2
- package/dist/peer-join-backfill.js +60 -12
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/peer-record.d.ts +17 -52
- package/dist/peer-record.js +20 -58
- package/dist/peer-record.js.map +1 -1
- package/dist/pending-join-runner.d.ts +66 -58
- package/dist/pending-join-runner.js +136 -145
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/primary-lan-address.d.ts +8 -0
- package/dist/primary-lan-address.js +46 -0
- package/dist/primary-lan-address.js.map +1 -0
- package/dist/provider/client.d.ts +131 -0
- package/dist/provider/client.js +226 -0
- package/dist/provider/client.js.map +1 -0
- package/dist/provider/descriptor.d.ts +41 -0
- package/dist/provider/descriptor.js +103 -0
- package/dist/provider/descriptor.js.map +1 -0
- package/dist/provider/index.d.ts +9 -0
- package/dist/provider/index.js +10 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/provider/oauth.d.ts +125 -0
- package/dist/provider/oauth.js +199 -0
- package/dist/provider/oauth.js.map +1 -0
- package/dist/provider/rent-node.d.ts +101 -0
- package/dist/provider/rent-node.js +220 -0
- package/dist/provider/rent-node.js.map +1 -0
- package/dist/push-fanout.d.ts +8 -2
- package/dist/push-fanout.js +8 -2
- package/dist/push-fanout.js.map +1 -1
- package/dist/relay-addrs.d.ts +9 -4
- package/dist/relay-addrs.js +9 -4
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +51 -16
- package/dist/relay-reservation.js +107 -24
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +165 -123
- package/dist/seed-bootstrap.js +313 -275
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +98 -37
- package/dist/seed-trust-policy.js +188 -2
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +24 -26
- package/dist/strand-addr-protocol.js +19 -26
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-commit-durability.d.ts +54 -0
- package/dist/strand-commit-durability.js +74 -0
- package/dist/strand-commit-durability.js.map +1 -0
- package/dist/strand-database.d.ts +23 -8
- package/dist/strand-database.js +41 -13
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +7 -1
- package/dist/strand-first-sync-gate.js +7 -1
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-protocol.js +5 -2
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +19 -1
- package/dist/strand-fret-addrs.js +27 -9
- package/dist/strand-fret-addrs.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +81 -19
- package/dist/strand-instance-manager.js +216 -105
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-move.d.ts +124 -0
- package/dist/strand-move.js +438 -0
- package/dist/strand-move.js.map +1 -0
- package/dist/strand-network-config.js +7 -6
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-peer-redial.d.ts +113 -0
- package/dist/strand-peer-redial.js +236 -0
- package/dist/strand-peer-redial.js.map +1 -0
- package/dist/strand-revocation-enforcer.d.ts +11 -11
- package/dist/strand-revocation-enforcer.js +11 -11
- package/dist/strand-wake-protocol.d.ts +13 -16
- package/dist/strand-wake-protocol.js +23 -27
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/trusted-owner-store.d.ts +35 -5
- package/dist/trusted-owner-store.js +50 -8
- package/dist/trusted-owner-store.js.map +1 -1
- package/dist/types.d.ts +512 -89
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +19 -10
- package/src/cadre-invite-protocol.ts +1052 -0
- package/src/cadre-join-request.ts +167 -0
- package/src/cadre-node.ts +1703 -386
- package/src/claim-proof.ts +147 -0
- package/src/control-authorization.ts +203 -117
- package/src/control-cohort.ts +1 -1
- package/src/control-database.ts +4663 -3453
- package/src/control-protocol-guard.ts +348 -0
- package/src/control-schema.ts +828 -1078
- package/src/control-stream.ts +26 -7
- package/src/delegate-admission.ts +1 -1
- package/src/device-token.ts +14 -10
- package/src/ed25519-key.ts +47 -10
- package/src/fetch-impl.ts +23 -0
- package/src/file-durable-slot.ts +64 -63
- package/src/file-safe-name.ts +32 -0
- package/src/formation-approval.ts +604 -610
- package/src/fs-atomic.ts +92 -117
- package/src/index.ts +789 -648
- package/src/invitation-bootstrap.ts +91 -13
- package/src/joined-strand-store.ts +508 -498
- package/src/key-store-file.ts +142 -141
- package/src/link-budget.ts +25 -36
- package/src/machine-removal.ts +216 -0
- package/src/membership-connection-gater.ts +259 -191
- package/src/node-claim-payload.ts +259 -0
- package/src/node-default-transports.ts +38 -0
- package/src/node-transports.ts +49 -0
- package/src/owner-anchor-sync.ts +171 -0
- package/src/peer-authorization.ts +258 -29
- package/src/peer-dial.ts +604 -315
- package/src/peer-join-backfill.ts +62 -11
- package/src/peer-record.ts +159 -197
- package/src/pending-join-runner.ts +729 -735
- package/src/primary-lan-address.ts +47 -0
- package/src/provider/client.ts +326 -0
- package/src/provider/descriptor.ts +129 -0
- package/src/provider/index.ts +59 -0
- package/src/provider/oauth.ts +285 -0
- package/src/provider/rent-node.ts +275 -0
- package/src/push-fanout.ts +8 -2
- package/src/relay-addrs.ts +9 -4
- package/src/relay-reservation.ts +1211 -1121
- package/src/seed-bootstrap.ts +1502 -1448
- package/src/seed-trust-policy.ts +353 -140
- package/src/strand-addr-protocol.ts +26 -35
- package/src/strand-commit-durability.ts +116 -0
- package/src/strand-database.ts +46 -14
- package/src/strand-first-sync-gate.ts +7 -1
- package/src/strand-formation-protocol.ts +1039 -1036
- package/src/strand-fret-addrs.ts +179 -145
- package/src/strand-instance-manager.ts +2026 -1883
- package/src/strand-move.ts +585 -0
- package/src/strand-network-config.ts +7 -6
- package/src/strand-peer-redial.ts +307 -0
- package/src/strand-revocation-enforcer.ts +11 -11
- package/src/strand-wake-protocol.ts +23 -30
- package/src/trusted-owner-store.ts +242 -171
- package/src/types.ts +2687 -2242
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim proof: how the first seed delivered to a brand-new node proves that its sender
|
|
3
|
+
* holds the node's one-time claim secret.
|
|
4
|
+
*
|
|
5
|
+
* A node that belongs to nobody yet (a cadre-host child before its owner scans the QR
|
|
6
|
+
* code) has an empty trusted-owner anchor, so every seed trust policy refuses its first
|
|
7
|
+
* seed. The claim secret is the out-of-band channel that breaks the deadlock: the host
|
|
8
|
+
* mints 32 random bytes, shows them beside the node's addresses, and the owner's phone
|
|
9
|
+
* proves possession by sending, next to the seed, an HMAC keyed by those bytes.
|
|
10
|
+
*
|
|
11
|
+
* The proof is `base64url(HMAC-SHA256(secret, utf8(canonicalJson(payload))))` where
|
|
12
|
+
* `payload` is `{ purpose: 'sereus-node-claim', v: 1, nodePeerId, signerKey, seedDigest }`:
|
|
13
|
+
*
|
|
14
|
+
* - `nodePeerId` binds the proof to one node, so a captured proof cannot claim another
|
|
15
|
+
* node minted with the same secret;
|
|
16
|
+
* - `signerKey` binds it to one owner, so it cannot anchor a different key;
|
|
17
|
+
* - `seedDigest` (the same digest the seed signature covers, `seedDigest()` in
|
|
18
|
+
* `seed-bootstrap.ts`) binds it to one seed, so it cannot be reattached to another.
|
|
19
|
+
*
|
|
20
|
+
* A passive observer of one delivery therefore learns a proof that is useless anywhere
|
|
21
|
+
* else, and useless on this node once it is claimed. A keyed MAC is the standard way to
|
|
22
|
+
* prove possession of a secret without revealing it; the crypto plugin's framed
|
|
23
|
+
* `digest([...])` helper is not specified as one, so the secret is never fed to it.
|
|
24
|
+
*
|
|
25
|
+
* Verified by `claimSecretTrustPolicy` (`seed-trust-policy.ts`). Imports only
|
|
26
|
+
* `@noble/hashes`, `uint8arrays` and `canonical-json.ts`: this module loads on React
|
|
27
|
+
* Native and in the browser as well as Node.
|
|
28
|
+
*/
|
|
29
|
+
import debug from 'debug';
|
|
30
|
+
import { hmac } from '@noble/hashes/hmac.js';
|
|
31
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
32
|
+
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
33
|
+
import { canonicalJson } from './canonical-json.js';
|
|
34
|
+
import type { SeedRefusalCode } from './types.js';
|
|
35
|
+
|
|
36
|
+
const log = debug('sereus:cadre:claim-proof');
|
|
37
|
+
|
|
38
|
+
/** Length of a claim secret: 256 bits, so guessing one is infeasible. */
|
|
39
|
+
export const CLAIM_SECRET_BYTES = 32;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Thrown by `CadreNode.claimNode` when the node answered the claim seed with a refusal.
|
|
43
|
+
* Distinct from `PeerUnreachableError` (no connection to the node formed) and from a
|
|
44
|
+
* failure after the node was reached (thrown by `deliverSeed` as is): here the node was
|
|
45
|
+
* reached and said no. `code` is the node's machine-readable cause
|
|
46
|
+
* when its policy supplied one (`already-claimed`, `claim-proof-invalid`, ...), so a
|
|
47
|
+
* phone can tell "someone else owns it" from "wrong secret" without parsing `reason`.
|
|
48
|
+
*/
|
|
49
|
+
export class ClaimRefusedError extends Error {
|
|
50
|
+
constructor(
|
|
51
|
+
readonly nodePeerId: string,
|
|
52
|
+
readonly reason: string,
|
|
53
|
+
readonly code?: SeedRefusalCode,
|
|
54
|
+
) {
|
|
55
|
+
super(`Node ${nodePeerId} refused the claim${code ? ` (${code})` : ''}: ${reason}`);
|
|
56
|
+
this.name = 'ClaimRefusedError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Decode a claim secret from the base64url text the host shows and the phone scans.
|
|
62
|
+
*
|
|
63
|
+
* Refuses anything that is not base64url of exactly {@link CLAIM_SECRET_BYTES} bytes,
|
|
64
|
+
* naming the problem. The rejected value is never echoed: unlike a public key, a
|
|
65
|
+
* mistyped secret may still be a secret.
|
|
66
|
+
*
|
|
67
|
+
* @returns The 32 secret bytes.
|
|
68
|
+
*/
|
|
69
|
+
export function parseClaimSecret(text: string): Uint8Array {
|
|
70
|
+
const trimmed = text.trim();
|
|
71
|
+
if (trimmed.length === 0) {
|
|
72
|
+
throw new Error('A claim secret is required (received an empty or whitespace-only value)');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let decoded: Uint8Array;
|
|
76
|
+
try {
|
|
77
|
+
decoded = uint8ArrayFromString(trimmed, 'base64url');
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error('A claim secret must be base64url text (the value could not be decoded as base64url)', { cause: error });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (decoded.length !== CLAIM_SECRET_BYTES) {
|
|
83
|
+
throw new Error(`A claim secret must decode to exactly ${CLAIM_SECRET_BYTES} bytes (the value decoded to ${decoded.length})`);
|
|
84
|
+
}
|
|
85
|
+
return decoded;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The HMAC tag bytes over the canonical claim payload (see the module doc). */
|
|
89
|
+
function claimProofBytes(secret: Uint8Array, nodePeerId: string, signerKey: string, seedDigest: string): Uint8Array {
|
|
90
|
+
const payload = canonicalJson({ purpose: 'sereus-node-claim', v: 1, nodePeerId, signerKey, seedDigest });
|
|
91
|
+
return hmac(sha256, secret, new TextEncoder().encode(payload));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compute the claim proof the owner side sends beside its seed.
|
|
96
|
+
*
|
|
97
|
+
* @param secret - The parsed 32-byte claim secret ({@link parseClaimSecret}).
|
|
98
|
+
* @param nodePeerId - The peer id of the node being claimed.
|
|
99
|
+
* @param signerKey - The owner key that signed the seed (base64url).
|
|
100
|
+
* @param seedDigest - The seed's signature digest (`seedDigest()` in `seed-bootstrap.ts`).
|
|
101
|
+
* @returns The proof, base64url.
|
|
102
|
+
*/
|
|
103
|
+
export function claimProof(secret: Uint8Array, nodePeerId: string, signerKey: string, seedDigest: string): string {
|
|
104
|
+
return uint8ArrayToString(claimProofBytes(secret, nodePeerId, signerKey, seedDigest), 'base64url');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check a presented proof against the receiver's own view of the three bound fields.
|
|
109
|
+
*
|
|
110
|
+
* Recomputes the tag and compares in constant time. A proof that does not decode as
|
|
111
|
+
* base64url is simply invalid. `nodePeerId` must be the RECEIVER's own peer id, never
|
|
112
|
+
* one the sender supplied; that is what makes a captured proof useless elsewhere.
|
|
113
|
+
*/
|
|
114
|
+
export function verifyClaimProof(
|
|
115
|
+
secret: Uint8Array,
|
|
116
|
+
nodePeerId: string,
|
|
117
|
+
signerKey: string,
|
|
118
|
+
seedDigest: string,
|
|
119
|
+
proof: string,
|
|
120
|
+
): boolean {
|
|
121
|
+
let presented: Uint8Array;
|
|
122
|
+
try {
|
|
123
|
+
presented = uint8ArrayFromString(proof, 'base64url');
|
|
124
|
+
} catch (error) {
|
|
125
|
+
log('claim proof is not base64url: %o', error);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return constantTimeEqual(presented, claimProofBytes(secret, nodePeerId, signerKey, seedDigest));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Byte equality whose running time does not depend on WHERE the arrays differ, so a
|
|
133
|
+
* remote cannot learn a valid proof prefix from response timing. The length check
|
|
134
|
+
* short-circuits, which leaks nothing: a valid proof's length is public. A plain loop
|
|
135
|
+
* rather than `node:crypto`'s `timingSafeEqual` because this module must load on React
|
|
136
|
+
* Native and in the browser.
|
|
137
|
+
*/
|
|
138
|
+
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
139
|
+
if (a.length !== b.length) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
let diff = 0;
|
|
143
|
+
for (let i = 0; i < a.length; i++) {
|
|
144
|
+
diff |= a[i]! ^ b[i]!;
|
|
145
|
+
}
|
|
146
|
+
return diff === 0;
|
|
147
|
+
}
|
|
@@ -1,117 +1,203 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The single definition of the field vector every signed control-plane message
|
|
3
|
-
* covers. Both producers of signed bytes — `control-database.ts`'s
|
|
4
|
-
* `buildAuthorizationMessage` (raw digest bytes) and `peer-authorization.ts`'s
|
|
5
|
-
* digest helpers (base64url digest strings) — build their vector here, so the
|
|
6
|
-
* byte layout cannot drift between them or away from the SQL constraints in
|
|
7
|
-
* `schemas/control.qsql`.
|
|
8
|
-
*
|
|
9
|
-
* Every vector leads with two fixed literals:
|
|
10
|
-
*
|
|
11
|
-
* digest(<domain>, <action>, <row field 1>, ..., <row field n>)
|
|
12
|
-
*
|
|
13
|
-
* so a signature verifies ONLY against the one rule
|
|
14
|
-
* the tags, several rules built byte-identical tuples (e.g.
|
|
15
|
-
* insert and `OwnerKey` insert both signed `digest(Key, StampId)`),
|
|
16
|
-
* approval for a narrow grant doubled as an approval for full ownership.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*/
|
|
69
|
-
export type
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
* `
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The single definition of the field vector every signed control-plane message
|
|
3
|
+
* covers. Both producers of signed bytes — `control-database.ts`'s
|
|
4
|
+
* `buildAuthorizationMessage` (raw digest bytes) and `peer-authorization.ts`'s
|
|
5
|
+
* digest helpers (base64url digest strings) — build their vector here, so the
|
|
6
|
+
* byte layout cannot drift between them or away from the SQL constraints in
|
|
7
|
+
* `schemas/control.qsql`.
|
|
8
|
+
*
|
|
9
|
+
* Every vector leads with two fixed literals and the party id:
|
|
10
|
+
*
|
|
11
|
+
* digest(<domain>, <action>, <party id>, <row field 1>, ..., <row field n>)
|
|
12
|
+
*
|
|
13
|
+
* so a signature verifies ONLY against the one rule, in the one party, it was
|
|
14
|
+
* minted for. Without the tags, several rules built byte-identical tuples (e.g.
|
|
15
|
+
* `ValidationKey` insert and `OwnerKey` insert both signed `digest(Key, StampId)`),
|
|
16
|
+
* so an approval for a narrow grant doubled as an approval for full ownership.
|
|
17
|
+
* Without the party, two parties sharing an owner key (one node key owning two
|
|
18
|
+
* cadres, a hardware owner key) would accept each other's approvals. The SQL side
|
|
19
|
+
* reads the party from the node-local `party_id()` function `ControlDatabase`
|
|
20
|
+
* registers (never from a context value or a replicated table), and every
|
|
21
|
+
* TypeScript signer and verifier passes the party it is configured for.
|
|
22
|
+
*
|
|
23
|
+
* Two digests carry NO party ({@link UnboundControlDomain}): the `FormationUsage`
|
|
24
|
+
* `'consent'` and `'vouch'` digests, whose signers do not know the party (see the
|
|
25
|
+
* schema's `FormationUsage.Authorized` comment), and the offline `'Cadre.Enrollment'`
|
|
26
|
+
* vouch, which no table checks.
|
|
27
|
+
*
|
|
28
|
+
* This module deliberately has no Quereus / Optimystic / libp2p imports so the
|
|
29
|
+
* lightweight verifiers (`peer-authorization.ts`, consumed by the offline
|
|
30
|
+
* `cadre enroll register` check) can use it without pulling in the runtime.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Names of the CadreControl tables, in schema order. The single list: the
|
|
35
|
+
* {@link ControlTable} union is derived from it and `ControlDatabase.countRows`
|
|
36
|
+
* guards its dynamic `from` clause against it (keeping the table name off the
|
|
37
|
+
* SQL-injection surface), so a new table cannot be added to one and missed in
|
|
38
|
+
* the other.
|
|
39
|
+
*
|
|
40
|
+
* `Revocation` is here for the same two reasons as every other entry: it
|
|
41
|
+
* derives a `'CadreControl.Revocation'` domain tag — its `Authorized` CHECK
|
|
42
|
+
* verifies an owner signature over the `'remove'`-tagged digest that
|
|
43
|
+
* `peer-authorization.ts`'s `revocationDigest` mints, and its `AuthorizedReissue`
|
|
44
|
+
* CHECK the `'reissue'`-tagged digest `ControlDatabase.reissueRevocations`
|
|
45
|
+
* signs — and `countRows` counts it.
|
|
46
|
+
*/
|
|
47
|
+
export const CONTROL_TABLES = [
|
|
48
|
+
'OwnerKey',
|
|
49
|
+
'ValidationKey',
|
|
50
|
+
'Strand',
|
|
51
|
+
'StrandPartyKey',
|
|
52
|
+
'JoinedStrand',
|
|
53
|
+
'JoinRequest',
|
|
54
|
+
'JoinSuccess',
|
|
55
|
+
'JoinFailure',
|
|
56
|
+
'CadrePeer',
|
|
57
|
+
'DeviceToken',
|
|
58
|
+
'FormationInvite',
|
|
59
|
+
'FormationUsage',
|
|
60
|
+
'CadreInvite',
|
|
61
|
+
'CadreInviteUsage',
|
|
62
|
+
'Revocation',
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Name of a CadreControl table. Doubles as the table half of a
|
|
67
|
+
* {@link ControlDomain} tag.
|
|
68
|
+
*/
|
|
69
|
+
export type ControlTable = typeof CONTROL_TABLES[number];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The control tables whose rows carry a single-use `StampId` retired into
|
|
73
|
+
* `CadreControl.Revocation` on delete — i.e. the ones the schema's
|
|
74
|
+
* `NotRevoked` / `RevocationRecorded` CHECK pair guards, and the only values
|
|
75
|
+
* `Revocation.RowIsGone` accepts in `TableName`. `Extract` from
|
|
76
|
+
* {@link ControlTable} rather than a fresh literal list, so a renamed table is a
|
|
77
|
+
* compile error here instead of a silently dead branch.
|
|
78
|
+
*
|
|
79
|
+
* `CadreInvite` is the one member whose rows are never deleted: its tombstone is a
|
|
80
|
+
* withdrawal over a row that stays (`NotRevoked` refuses re-seating the withdrawn
|
|
81
|
+
* row; there is no `RevocationRecorded`, since nothing is ever removed), so it is
|
|
82
|
+
* revocable without being removable — `control-database.ts`'s `RemovableTable`
|
|
83
|
+
* excludes it.
|
|
84
|
+
*
|
|
85
|
+
* Lives in this import-free module rather than beside its main consumer
|
|
86
|
+
* (`control-database.ts`) so the lightweight signers — `peer-authorization.ts`'s
|
|
87
|
+
* `revocationDigest` — can type against it without pulling in the runtime.
|
|
88
|
+
*/
|
|
89
|
+
export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' | 'JoinRequest' | 'DeviceToken' | 'CadreInvite'>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* What a signature authorizes, scoped to one table rule — or, for
|
|
93
|
+
* `'Cadre.Enrollment'`, the offline peer vouch that `cadre enroll register`
|
|
94
|
+
* verifies (no table checks it, so it needs its own domain to stay disjoint
|
|
95
|
+
* from every table rule).
|
|
96
|
+
*/
|
|
97
|
+
export type ControlDomain = `CadreControl.${ControlTable}` | 'Cadre.Enrollment';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The domains whose digests bind no party id — the only two
|
|
101
|
+
* {@link unboundAuthorizationFields} accepts. `FormationUsage`: neither signer knows
|
|
102
|
+
* the party, and the invitation token scopes the digest instead (the reason is at the
|
|
103
|
+
* schema's `FormationUsage.Authorized` comment). `Cadre.Enrollment`: no table checks it.
|
|
104
|
+
*/
|
|
105
|
+
export type UnboundControlDomain = 'CadreControl.FormationUsage' | 'Cadre.Enrollment';
|
|
106
|
+
|
|
107
|
+
/** Every domain whose digests bind the party id — the only ones {@link controlAuthorizationFields} accepts. */
|
|
108
|
+
export type BoundControlDomain = Exclude<ControlDomain, UnboundControlDomain>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The action half of the tag:
|
|
112
|
+
* - `'add'` / `'remove'` — insert / delete of the named row.
|
|
113
|
+
* - `'vouch'` — an owner (or validation key) vouches the row's semantics
|
|
114
|
+
* without adding or removing it: the `CadrePeer` membership vouch (shared,
|
|
115
|
+
* deliberately, by its insert and its owner-update branch), the
|
|
116
|
+
* `FormationUsage` disclosure validation, and the offline enrollment vouch.
|
|
117
|
+
* `DeviceToken` has no `'vouch'` digest — its owner re-touch branch was
|
|
118
|
+
* removed (it rewrote the row outside the monotonicity guard), so an owner
|
|
119
|
+
* correcting a token deletes and re-inserts it.
|
|
120
|
+
* - `'publish'` — a peer self-signs its OWN record (the `CadrePeer` /
|
|
121
|
+
* `DeviceToken` self-update branches), with its own key rather than an
|
|
122
|
+
* owner key.
|
|
123
|
+
* - `'consent'` — a peer self-signs its OWN `FormationUsage` or
|
|
124
|
+
* `CadreInviteUsage` redemption (the joiner proving it agreed to join), with
|
|
125
|
+
* its own key. Distinct from the approver's `'vouch'` over the same table so
|
|
126
|
+
* the two stored signatures are never interchangeable.
|
|
127
|
+
* - `'redeem'` — the holder of a `CadreInvite`'s private key proves possession
|
|
128
|
+
* on a `CadreInviteUsage` row. Distinct from the device's `'consent'` over the
|
|
129
|
+
* same fields: the two signatures are by different keys and neither may stand
|
|
130
|
+
* in for the other.
|
|
131
|
+
* - `'reissue'` — an owner re-writes an existing `Revocation` tombstone,
|
|
132
|
+
* bumping its `ReissuedAt` counter so a tombstone committed while the node
|
|
133
|
+
* was alone can be re-broadcast. Distinct from `'remove'` so a tombstone
|
|
134
|
+
* append approval can never be replayed as a re-issue and vice versa.
|
|
135
|
+
*/
|
|
136
|
+
export type ControlAction = 'add' | 'remove' | 'vouch' | 'publish' | 'consent' | 'redeem' | 'reissue';
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The `CadreInvite` columns its `'add'` signature binds, in the schema's order and
|
|
140
|
+
* with the row's stored forms: `expiresAt` is the engine-canonical `datetime` string
|
|
141
|
+
* (`canonicalDatetime`), never epoch ms, because the SQL side digests
|
|
142
|
+
* `cast(new.ExpiresAt as text)` after coercion. Structurally satisfied by a
|
|
143
|
+
* `CadreInviteRow` (`types.ts`), which adds the issuer pair the signature does not cover.
|
|
144
|
+
*/
|
|
145
|
+
export interface CadreInviteSignedFields {
|
|
146
|
+
key: string;
|
|
147
|
+
peerId: string | null;
|
|
148
|
+
grantsOwner: boolean;
|
|
149
|
+
expiresAt: string | null;
|
|
150
|
+
totalUses: number | null;
|
|
151
|
+
stampId: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The row-field half of a `CadreInvite` `'add'` digest, shared by the signer
|
|
156
|
+
* (`control-database.ts`'s `cadreInviteAddMessage`, raw bytes) and the verifier
|
|
157
|
+
* (`peer-authorization.ts`'s `cadreInviteAddDigest`, base64url) so the two cannot
|
|
158
|
+
* drift. SQL mirror, in `CadreInvite.AuthorizedInsert`:
|
|
159
|
+
* `new.Key, coalesce(new.PeerId, ''), cast(new.GrantsOwner as text),
|
|
160
|
+
* coalesce(cast(new.ExpiresAt as text), ''), coalesce(cast(new.TotalUses as text), ''), new.StampId`.
|
|
161
|
+
*/
|
|
162
|
+
export function cadreInviteRowFields(row: CadreInviteSignedFields): string[] {
|
|
163
|
+
return [
|
|
164
|
+
row.key,
|
|
165
|
+
row.peerId ?? '',
|
|
166
|
+
row.grantsOwner ? '1' : '0',
|
|
167
|
+
row.expiresAt ?? '',
|
|
168
|
+
row.totalUses === null ? '' : String(row.totalUses),
|
|
169
|
+
row.stampId,
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The full ordered field vector a party-bound control-plane signature covers. Digest
|
|
175
|
+
* this with the crypto plugin's injective multi-field encoding (every field TEXT);
|
|
176
|
+
* the SQL mirror passes the same literals as leading `digest(...)` arguments and
|
|
177
|
+
* reads the party from its node-local `party_id()`:
|
|
178
|
+
*
|
|
179
|
+
* TS: digest(controlAuthorizationFields('CadreControl.X', 'add', partyId, [a, b]), 'sha256', ...)
|
|
180
|
+
* SQL: digest('CadreControl.X', 'add', party_id(), new.A, new.B)
|
|
181
|
+
*/
|
|
182
|
+
export function controlAuthorizationFields(
|
|
183
|
+
domain: BoundControlDomain,
|
|
184
|
+
action: ControlAction,
|
|
185
|
+
partyId: string,
|
|
186
|
+
rowFields: string[],
|
|
187
|
+
): string[] {
|
|
188
|
+
return [domain, action, partyId, ...rowFields];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The field vector of the two party-free digests ({@link UnboundControlDomain}):
|
|
193
|
+
*
|
|
194
|
+
* TS: digest(unboundAuthorizationFields('CadreControl.FormationUsage', 'consent', [a, b]), 'sha256', ...)
|
|
195
|
+
* SQL: digest('CadreControl.FormationUsage', 'consent', new.A, new.B)
|
|
196
|
+
*/
|
|
197
|
+
export function unboundAuthorizationFields(
|
|
198
|
+
domain: UnboundControlDomain,
|
|
199
|
+
action: ControlAction,
|
|
200
|
+
rowFields: string[],
|
|
201
|
+
): string[] {
|
|
202
|
+
return [domain, action, ...rowFields];
|
|
203
|
+
}
|
package/src/control-cohort.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* party it bounds out-degree to `backbone + targetDegree`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { ed25519PublicKeyB64FromPeerId } from './
|
|
24
|
+
import { ed25519PublicKeyB64FromPeerId } from './ed25519-key.js';
|
|
25
25
|
import type { CohortPeerRow } from './strand-cohort.js';
|
|
26
26
|
|
|
27
27
|
/**
|