@cello-protocol/daemon 0.0.194 → 0.0.195
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/dist/assignment-verify.d.ts +39 -11
- package/dist/assignment-verify.d.ts.map +1 -1
- package/dist/assignment-verify.js +133 -17
- package/dist/assignment-verify.js.map +1 -1
- package/dist/authorship-verification.d.ts +70 -0
- package/dist/authorship-verification.d.ts.map +1 -0
- package/dist/authorship-verification.js +441 -0
- package/dist/authorship-verification.js.map +1 -0
- package/dist/db-identity-store.d.ts +1 -0
- package/dist/db-identity-store.d.ts.map +1 -1
- package/dist/db-identity-store.js +19 -1
- package/dist/db-identity-store.js.map +1 -1
- package/dist/held-content.d.ts +141 -0
- package/dist/held-content.d.ts.map +1 -0
- package/dist/held-content.js +388 -0
- package/dist/held-content.js.map +1 -0
- package/dist/inbound-refusals.d.ts +283 -0
- package/dist/inbound-refusals.d.ts.map +1 -0
- package/dist/inbound-refusals.js +919 -0
- package/dist/inbound-refusals.js.map +1 -0
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +65 -27
- package/dist/inbound-sessions.js.map +1 -1
- package/dist/initiate-session-handler.d.ts.map +1 -1
- package/dist/initiate-session-handler.js +63 -0
- package/dist/initiate-session-handler.js.map +1 -1
- package/dist/outbound-sessions.d.ts.map +1 -1
- package/dist/outbound-sessions.js +108 -8
- package/dist/outbound-sessions.js.map +1 -1
- package/dist/park-recovery.d.ts +205 -0
- package/dist/park-recovery.d.ts.map +1 -0
- package/dist/park-recovery.js +600 -0
- package/dist/park-recovery.js.map +1 -0
- package/dist/refusal-notices.d.ts +196 -0
- package/dist/refusal-notices.d.ts.map +1 -0
- package/dist/refusal-notices.js +496 -0
- package/dist/refusal-notices.js.map +1 -0
- package/dist/refusal-reasons.d.ts +11 -0
- package/dist/refusal-reasons.d.ts.map +1 -1
- package/dist/refusal-reasons.js +18 -0
- package/dist/refusal-reasons.js.map +1 -1
- package/dist/registration-manager.d.ts.map +1 -1
- package/dist/registration-manager.js +126 -5
- package/dist/registration-manager.js.map +1 -1
- package/dist/registration-persistence.d.ts +18 -0
- package/dist/registration-persistence.d.ts.map +1 -1
- package/dist/registration-persistence.js +6 -0
- package/dist/registration-persistence.js.map +1 -1
- package/dist/session-assignment-parser.d.ts.map +1 -1
- package/dist/session-assignment-parser.js +23 -1
- package/dist/session-assignment-parser.js.map +1 -1
- package/dist/session-ceremony.d.ts +12 -7
- package/dist/session-ceremony.d.ts.map +1 -1
- package/dist/session-ceremony.js +12 -7
- package/dist/session-ceremony.js.map +1 -1
- package/dist/session-ephemerals.d.ts +271 -0
- package/dist/session-ephemerals.d.ts.map +1 -0
- package/dist/session-ephemerals.js +546 -0
- package/dist/session-ephemerals.js.map +1 -0
- package/dist/session-leaf-records.d.ts +132 -0
- package/dist/session-leaf-records.d.ts.map +1 -0
- package/dist/session-leaf-records.js +320 -0
- package/dist/session-leaf-records.js.map +1 -0
- package/dist/session-liveness.d.ts +135 -0
- package/dist/session-liveness.d.ts.map +1 -0
- package/dist/session-liveness.js +347 -0
- package/dist/session-liveness.js.map +1 -0
- package/dist/session-node-manager.d.ts +300 -1670
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +871 -9951
- package/dist/session-node-manager.js.map +1 -1
- package/dist/session-node-types.d.ts +933 -0
- package/dist/session-node-types.d.ts.map +1 -0
- package/dist/session-node-types.js +576 -0
- package/dist/session-node-types.js.map +1 -0
- package/dist/session-queries.d.ts +476 -0
- package/dist/session-queries.d.ts.map +1 -0
- package/dist/session-queries.js +1006 -0
- package/dist/session-queries.js.map +1 -0
- package/dist/session-records.d.ts +320 -0
- package/dist/session-records.d.ts.map +1 -0
- package/dist/session-records.js +792 -0
- package/dist/session-records.js.map +1 -0
- package/dist/session-salts.d.ts +390 -0
- package/dist/session-salts.d.ts.map +1 -0
- package/dist/session-salts.js +1457 -0
- package/dist/session-salts.js.map +1 -0
- package/dist/session-schema.d.ts +30 -0
- package/dist/session-schema.d.ts.map +1 -0
- package/dist/session-schema.js +786 -0
- package/dist/session-schema.js.map +1 -0
- package/dist/standing-receivers.d.ts +280 -0
- package/dist/standing-receivers.d.ts.map +1 -0
- package/dist/standing-receivers.js +1100 -0
- package/dist/standing-receivers.js.map +1 -0
- package/dist/transport-selector.d.ts +13 -0
- package/dist/transport-selector.d.ts.map +1 -1
- package/dist/transport-selector.js.map +1 -1
- package/dist/witness-alerts.d.ts +40 -0
- package/dist/witness-alerts.d.ts.map +1 -0
- package/dist/witness-alerts.js +102 -0
- package/dist/witness-alerts.js.map +1 -0
- package/package.json +5 -5
|
@@ -11,6 +11,13 @@ import type { Logger } from "./types.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function verifyAssignmentSignature(assignment: SessionAssignment, persistence: DbRegistrationPersistence, logger: Logger, agentName: string, correlationId: string): Promise<{
|
|
13
13
|
ok: true;
|
|
14
|
+
/**
|
|
15
|
+
* 038-KEYBIND. The COUNTERPARTY's FROST group public key, hex, having been proved theirs by a
|
|
16
|
+
* signature under their own K_local. Returned rather than left on the frame so the caller
|
|
17
|
+
* cannot pin a value that was never checked — the only way to hold this string is to have
|
|
18
|
+
* come through the binding check below.
|
|
19
|
+
*/
|
|
20
|
+
counterpartyPrimaryHex: string;
|
|
14
21
|
} | {
|
|
15
22
|
ok: false;
|
|
16
23
|
reason: string;
|
|
@@ -38,22 +45,43 @@ export declare function verifyAssignmentSignature(assignment: SessionAssignment,
|
|
|
38
45
|
* that is a real check: a compromised directory can produce a signature, but not one that verifies
|
|
39
46
|
* under a key it does not hold.
|
|
40
47
|
*
|
|
41
|
-
* ───
|
|
48
|
+
* ─── 038-KEYBIND CLOSED THE CIRCLE, AND THIS PARAGRAPH REPLACES THE ONE THAT ADMITTED IT ───────
|
|
42
49
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
50
|
+
* ⚠️ THE OLD TEXT HERE IS KEPT AS EVIDENCE RATHER THAN DELETED, because it is the record of a real
|
|
51
|
+
* bound that someone reasoned their way to and wrote down. It said: *"UNPINNED (first contact):
|
|
52
|
+
* there is nothing independent to verify against, so this checks INTERNAL CONSISTENCY only… That
|
|
53
|
+
* does NOT authenticate the directory, and it is not claimed to."* That was true of the code as it
|
|
54
|
+
* stood, and the `verifyAgainst` line below really did fall back to `assignment.signer_pubkey` — a
|
|
55
|
+
* field of the very document under verification. The signature therefore always verified, and the
|
|
56
|
+
* key it verified under was then PINNED as the counterparty's identity forever.
|
|
45
57
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
58
|
+
* It is no longer true, and the thing that changed is not a stronger check on the same evidence —
|
|
59
|
+
* it is NEW evidence on the wire. `participant_a_key_binding` is a signature by the INITIATOR's own
|
|
60
|
+
* K_local naming their group key. No directory holds that key, so no directory can name a group key
|
|
61
|
+
* of its choosing. The group key stops being something the frame ASSERTS and becomes something a
|
|
62
|
+
* signature PROVES.
|
|
63
|
+
*
|
|
64
|
+
* ─── The order of the two checks is the fix, not an optimisation ───────────────────────────────
|
|
65
|
+
*
|
|
66
|
+
* The binding is verified FIRST, and the threshold signature is then verified under the key the
|
|
67
|
+
* binding proved. There is no path to the signature check that does not pass through it: `signer`
|
|
68
|
+
* is not used as a verification key anywhere below, and a missing or bad binding returns before the
|
|
69
|
+
* TBS is even built.
|
|
70
|
+
*
|
|
71
|
+
* ─── Two modes, and neither is circular any more ───────────────────────────────────────────────
|
|
72
|
+
*
|
|
73
|
+
* PINNED (`expectedSignerHex` given): the signature must verify under the PINNED key — a value THIS
|
|
74
|
+
* daemon recorded in an earlier session, which no directory can retroactively change. The binding
|
|
75
|
+
* still runs; the pin is now a consistency check ACROSS TIME rather than the sole authority.
|
|
76
|
+
*
|
|
77
|
+
* FIRST CONTACT: the signature verifies under the group key `participant_a`'s identity key vouched
|
|
78
|
+
* for. What it still cannot tell you is whether `participant_a` is who you think they are — that is
|
|
79
|
+
* the operator's out-of-band problem and always was — but a directory can no longer substitute a
|
|
80
|
+
* group key, which is what this function existed to be unable to say.
|
|
53
81
|
*/
|
|
54
82
|
export declare function verifyInboundAssignment(assignment: SessionAssignment, expectedSignerHex: string | null): {
|
|
55
83
|
ok: true;
|
|
56
|
-
mode: "pinned" | "
|
|
84
|
+
mode: "pinned" | "bound";
|
|
57
85
|
} | {
|
|
58
86
|
ok: false;
|
|
59
87
|
reason: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assignment-verify.d.ts","sourceRoot":"","sources":["../src/assignment-verify.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,iBAAiB,EAC7B,WAAW,EAAE,yBAAyB,EACtC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,
|
|
1
|
+
{"version":3,"file":"assignment-verify.d.ts","sourceRoot":"","sources":["../src/assignment-verify.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,iBAAiB,EAC7B,WAAW,EAAE,yBAAyB,EACtC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CACN;IACE,EAAE,EAAE,IAAI,CAAC;IACT;;;;;OAKG;IACH,sBAAsB,EAAE,MAAM,CAAC;CAChC,GACD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAClD,CAkKA;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,iBAAiB,EAC7B,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAsGxF"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// removed with the downgrade refusal (review F1). Verifying `directory_signature` against the
|
|
15
15
|
// `directory_pubkey` riding beside it in the same unsigned frame checks a key against itself, and
|
|
16
16
|
// having that call available is how a future edit reintroduces the bypass.
|
|
17
|
-
import { verifyFrostSignature, CONTEXT_SESSION_ESTABLISHMENT } from "@cello-protocol/crypto";
|
|
17
|
+
import { verifyFrostSignature, verifyKeyBinding, CONTEXT_SESSION_ESTABLISHMENT } from "@cello-protocol/crypto";
|
|
18
18
|
import { buildSessionEstablishmentTbs, computeGenesisPrevRoot } from "@cello-protocol/protocol-types";
|
|
19
19
|
/**
|
|
20
20
|
* DOD-M15-ASSIGN-1 — verify a session assignment's signature before anything trusts it.
|
|
@@ -109,7 +109,59 @@ export async function verifyAssignmentSignature(assignment, persistence, logger,
|
|
|
109
109
|
guidance: "The directory's session assignment did not verify against the key that should have signed it. Nothing was established. Retry the session; if it repeats, the directory you reached is not producing valid assignments and cello status will show which one that is.",
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
/**
|
|
113
|
+
* 038-KEYBIND — LEARN THE COUNTERPARTY'S GROUP KEY, AND ONLY IF THEY VOUCHED FOR IT.
|
|
114
|
+
*
|
|
115
|
+
* The initiator never used to learn the responder's group key at all. The cost was concrete and
|
|
116
|
+
* one-directional: when the RESPONDER closed the conversation first, the seal certificate came
|
|
117
|
+
* back signed by their group key, this daemon held no copy, and it accepted the certificate
|
|
118
|
+
* with `verified:false` / `signer_key_not_held`. The receipt for a conversation you had was
|
|
119
|
+
* taken on faith in the channel it arrived on.
|
|
120
|
+
*
|
|
121
|
+
* `participant_b_primary_pubkey` alone would not fix that — it would move the same circularity
|
|
122
|
+
* to the other side, where a directory names a group key and we write it down. The BINDING is
|
|
123
|
+
* what makes it a fact: a signature under `participant_b`'s K_local. No directory holds that
|
|
124
|
+
* key, so no directory can put a group key of its choosing here.
|
|
125
|
+
*
|
|
126
|
+
* ⚠️ THE ANCHOR IS `participant_b.pubkey`, AND THIS FUNCTION DOES NOT CHECK IT. An earlier draft
|
|
127
|
+
* of this note said the caller "has already compared" it against the operator's `target_pubkey`;
|
|
128
|
+
* it has not — `outbound-sessions.ts` runs that comparison ~50 lines AFTER this call, and
|
|
129
|
+
* getting the order backwards in a comment is how someone later moves the pinning above it.
|
|
130
|
+
*
|
|
131
|
+
* What makes the returned key safe to pin is that the caller RETURNS on a target mismatch, so
|
|
132
|
+
* nothing reaches `recordCounterpartyPrimary` unless `participant_b` is the counterparty the
|
|
133
|
+
* operator asked for. The ordering is load-bearing and lives at the call site, not here.
|
|
134
|
+
*/
|
|
135
|
+
const counterpartyBinding = assignment.participant_b_key_binding;
|
|
136
|
+
const counterpartyPrimary = assignment.participant_b_primary_pubkey;
|
|
137
|
+
if (!counterpartyBinding || !counterpartyPrimary) {
|
|
138
|
+
// ABSENT AND MALFORMED TAKE THIS PATH TOGETHER (the parser maps a wrong-length value to
|
|
139
|
+
// undefined). An attacker who cannot forge a binding evades a mismatch check by supplying
|
|
140
|
+
// none, so "we could not tell" must cost exactly what "it was wrong" costs.
|
|
141
|
+
logger.error("session.assignment.counterparty_binding_absent", {
|
|
142
|
+
agentName, correlationId,
|
|
143
|
+
hasBinding: !!counterpartyBinding, hasPrimary: !!counterpartyPrimary,
|
|
144
|
+
impact: "the assignment carried no proof that the counterparty's threshold group key is theirs, so this agent could not learn a key it would later have to trust for their seal; the session was refused before any dial and nothing was sent",
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: "assignment_counterparty_binding_absent",
|
|
149
|
+
guidance: "The directory returned a session assignment without the counterparty's key binding — the signature that proves their threshold key belongs to the identity you asked for. Nothing was opened and nothing you wrote was sent. Retry cello_initiate_session; a different directory node will serve it. If it repeats on every node, the counterparty registered against a directory that predates this proof and they need to re-register before you can reach them.",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (!verifyKeyBinding(counterpartyBinding, assignment.participant_b.pubkey, counterpartyPrimary)) {
|
|
153
|
+
logger.error("session.assignment.counterparty_binding_invalid", {
|
|
154
|
+
agentName, correlationId,
|
|
155
|
+
counterpartyPrefix: Buffer.from(assignment.participant_b.pubkey).toString("hex").slice(0, 16),
|
|
156
|
+
impact: "the assignment named a threshold group key for the counterparty that their own identity key has not vouched for; it was refused before any dial, so no conversation was opened and nothing was sent",
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
reason: "assignment_counterparty_binding_invalid",
|
|
161
|
+
guidance: "The directory named a threshold key for your counterparty that their own identity key has not signed for. This is refused rather than reported because the observation cannot tell a directory fault from an attempt to put a key it controls in their place; cause undetermined. Run cello_status to see which node answered and retry — if it repeats across nodes, confirm the counterparty's pubkey with their operator out of band before trying again.",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { ok: true, counterpartyPrimaryHex: Buffer.from(counterpartyPrimary).toString("hex") };
|
|
113
165
|
}
|
|
114
166
|
}
|
|
115
167
|
/**
|
|
@@ -134,18 +186,39 @@ export async function verifyAssignmentSignature(assignment, persistence, logger,
|
|
|
134
186
|
* that is a real check: a compromised directory can produce a signature, but not one that verifies
|
|
135
187
|
* under a key it does not hold.
|
|
136
188
|
*
|
|
137
|
-
* ───
|
|
189
|
+
* ─── 038-KEYBIND CLOSED THE CIRCLE, AND THIS PARAGRAPH REPLACES THE ONE THAT ADMITTED IT ───────
|
|
190
|
+
*
|
|
191
|
+
* ⚠️ THE OLD TEXT HERE IS KEPT AS EVIDENCE RATHER THAN DELETED, because it is the record of a real
|
|
192
|
+
* bound that someone reasoned their way to and wrote down. It said: *"UNPINNED (first contact):
|
|
193
|
+
* there is nothing independent to verify against, so this checks INTERNAL CONSISTENCY only… That
|
|
194
|
+
* does NOT authenticate the directory, and it is not claimed to."* That was true of the code as it
|
|
195
|
+
* stood, and the `verifyAgainst` line below really did fall back to `assignment.signer_pubkey` — a
|
|
196
|
+
* field of the very document under verification. The signature therefore always verified, and the
|
|
197
|
+
* key it verified under was then PINNED as the counterparty's identity forever.
|
|
198
|
+
*
|
|
199
|
+
* It is no longer true, and the thing that changed is not a stronger check on the same evidence —
|
|
200
|
+
* it is NEW evidence on the wire. `participant_a_key_binding` is a signature by the INITIATOR's own
|
|
201
|
+
* K_local naming their group key. No directory holds that key, so no directory can name a group key
|
|
202
|
+
* of its choosing. The group key stops being something the frame ASSERTS and becomes something a
|
|
203
|
+
* signature PROVES.
|
|
204
|
+
*
|
|
205
|
+
* ─── The order of the two checks is the fix, not an optimisation ───────────────────────────────
|
|
138
206
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
207
|
+
* The binding is verified FIRST, and the threshold signature is then verified under the key the
|
|
208
|
+
* binding proved. There is no path to the signature check that does not pass through it: `signer`
|
|
209
|
+
* is not used as a verification key anywhere below, and a missing or bad binding returns before the
|
|
210
|
+
* TBS is even built.
|
|
141
211
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
212
|
+
* ─── Two modes, and neither is circular any more ───────────────────────────────────────────────
|
|
213
|
+
*
|
|
214
|
+
* PINNED (`expectedSignerHex` given): the signature must verify under the PINNED key — a value THIS
|
|
215
|
+
* daemon recorded in an earlier session, which no directory can retroactively change. The binding
|
|
216
|
+
* still runs; the pin is now a consistency check ACROSS TIME rather than the sole authority.
|
|
217
|
+
*
|
|
218
|
+
* FIRST CONTACT: the signature verifies under the group key `participant_a`'s identity key vouched
|
|
219
|
+
* for. What it still cannot tell you is whether `participant_a` is who you think they are — that is
|
|
220
|
+
* the operator's out-of-band problem and always was — but a directory can no longer substitute a
|
|
221
|
+
* group key, which is what this function existed to be unable to say.
|
|
149
222
|
*/
|
|
150
223
|
export function verifyInboundAssignment(assignment, expectedSignerHex) {
|
|
151
224
|
if (assignment.signature_type !== "frost") {
|
|
@@ -159,9 +232,47 @@ export function verifyInboundAssignment(assignment, expectedSignerHex) {
|
|
|
159
232
|
if (!signer || signer.length !== 32) {
|
|
160
233
|
return { ok: false, reason: "inbound_assignment_no_signer", detail: "signer_pubkey missing or not 32 bytes" };
|
|
161
234
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
235
|
+
/**
|
|
236
|
+
* 038-KEYBIND — PLACE THE GROUP KEY BEFORE VERIFYING ANYTHING UNDER IT.
|
|
237
|
+
*
|
|
238
|
+
* `participant_a.pubkey` is the counterparty's K_local: the 64-hex identity their operator was
|
|
239
|
+
* given out of band, and the value this daemon keys its contacts and its pin on. The binding is a
|
|
240
|
+
* signature under THAT key naming `signer_pubkey` as its group key.
|
|
241
|
+
*
|
|
242
|
+
* ABSENT IS A REFUSAL. Tolerating a missing binding would reproduce exactly the state this exists
|
|
243
|
+
* to end — and worse, it would hand the choice to the party the check exists to catch: a
|
|
244
|
+
* directory that cannot forge a binding simply omits one, and every session goes back to
|
|
245
|
+
* verifying a key against itself. The parser maps a malformed value to `undefined`, so missing
|
|
246
|
+
* and malformed take this same path.
|
|
247
|
+
*/
|
|
248
|
+
const binding = assignment.participant_a_key_binding;
|
|
249
|
+
if (!binding) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
reason: "inbound_assignment_no_key_binding",
|
|
253
|
+
detail: "the assignment carried no key binding for the initiator, so the threshold key it names could not be placed against their identity",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (!verifyKeyBinding(binding, assignment.participant_a.pubkey, signer)) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
reason: "inbound_assignment_key_binding_invalid",
|
|
260
|
+
detail: "the initiator's identity key has not signed for the threshold key this assignment names as theirs",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The key the BINDING proved — not the key the frame supplied.
|
|
265
|
+
*
|
|
266
|
+
* Textually these are the same bytes, and that is worth being precise about rather than glossing:
|
|
267
|
+
* what changed is that this value is now unreachable without a signature by `participant_a`'s
|
|
268
|
+
* K_local having verified over it three lines above. A directory that names a group key it holds
|
|
269
|
+
* cannot get here at all.
|
|
270
|
+
*/
|
|
271
|
+
const boundGroupKey = signer;
|
|
272
|
+
// PINNED beats bound. When we hold a value from an earlier session, that is the stronger anchor —
|
|
273
|
+
// it survives the counterparty's own key material changing, which a fresh binding would not
|
|
274
|
+
// reveal on its own.
|
|
275
|
+
const verifyAgainst = expectedSignerHex !== null ? Buffer.from(expectedSignerHex, "hex") : boundGroupKey;
|
|
165
276
|
if (expectedSignerHex !== null && Buffer.from(signer).toString("hex").toLowerCase() !== expectedSignerHex.toLowerCase()) {
|
|
166
277
|
return {
|
|
167
278
|
ok: false,
|
|
@@ -177,9 +288,14 @@ export function verifyInboundAssignment(assignment, expectedSignerHex) {
|
|
|
177
288
|
reason: "inbound_assignment_signature_invalid",
|
|
178
289
|
detail: expectedSignerHex !== null
|
|
179
290
|
? "the signature did not verify under the key this agent recorded for this counterparty"
|
|
180
|
-
: "the signature did not verify
|
|
291
|
+
: "the signature did not verify under the threshold key the initiator's identity key vouched for — the frame is tampered or malformed",
|
|
181
292
|
};
|
|
182
293
|
}
|
|
183
|
-
|
|
294
|
+
// `bound` was `internal`, and the rename is the claim changing rather than the label.
|
|
295
|
+
// `internal` meant "internally consistent only — this proves nothing about the signer".
|
|
296
|
+
// A first contact now verifies under a group key the counterparty's own identity key signed
|
|
297
|
+
// for, so the old word would assert a weaker property than the code holds and send the next
|
|
298
|
+
// reader looking for a gap that has been closed.
|
|
299
|
+
return { ok: true, mode: expectedSignerHex !== null ? "pinned" : "bound" };
|
|
184
300
|
}
|
|
185
301
|
//# sourceMappingURL=assignment-verify.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assignment-verify.js","sourceRoot":"","sources":["../src/assignment-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,gGAAgG;AAChG,8FAA8F;AAC9F,kGAAkG;AAClG,2EAA2E;AAC3E,OAAO,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"assignment-verify.js","sourceRoot":"","sources":["../src/assignment-verify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELLO Daemon — WHO SENT THIS, AND DID THEY SEE WHAT THEY CLAIM
|
|
3
|
+
*
|
|
4
|
+
* Split out of `session-node-manager.ts` by 036-GODFILE, Part 1. Two questions about one signed
|
|
5
|
+
* blob: did this really come from this conversation's counterparty, and is what they say they saw
|
|
6
|
+
* actually what we sent. Everything is moved verbatim — the comments are the asset and several of
|
|
7
|
+
* them record a defect that came back once already.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ THE ORDER OF THE CHECKS IS ITSELF A SECURITY PROPERTY — decode → signature → signer → what the
|
|
10
|
+
* proof is about. Everything after the signer answers `unusable`, which refuses the message and
|
|
11
|
+
* leaves the session alive; signature and signer answer `refuted`, which FREEZES it. A check that
|
|
12
|
+
* can answer before the signature is verified hands a peer a switch for choosing the softer
|
|
13
|
+
* outcome. Do not reorder these, and do not add a cheap early check above the signature.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ THIS CLASS OWNS ITS OWN STATE, which is what made it a seam rather than a split.
|
|
16
|
+
* `#receivedFromCounterparty` and `#lastFromCounterparty` are read and written nowhere else in the
|
|
17
|
+
* daemon, so they moved with the code that maintains them. Everything else it needs arrives through
|
|
18
|
+
* `AuthorshipContext` — a narrow, explicit list. No field of the manager was widened to make this
|
|
19
|
+
* possible, and the manager is not passed in as a whole.
|
|
20
|
+
*/
|
|
21
|
+
import type { Logger, SessionRecord } from "./types.js";
|
|
22
|
+
import type { SessionTree } from "./session-tree.js";
|
|
23
|
+
import { type AuthorshipVerdict } from "./session-node-types.js";
|
|
24
|
+
/**
|
|
25
|
+
* Everything the verifier needs from the manager, stated explicitly rather than handed the manager
|
|
26
|
+
* itself. If this list ever grows long, that is the signal the seam has moved — not a reason to
|
|
27
|
+
* widen it.
|
|
28
|
+
*/
|
|
29
|
+
export interface AuthorshipContext {
|
|
30
|
+
readonly logger: Logger;
|
|
31
|
+
sessionKey(agentName: string, sessionId: string): string;
|
|
32
|
+
getSessionRecord(agentName: string, sessionId: string): SessionRecord | null;
|
|
33
|
+
getSessionTree(agentName: string, sessionId: string): SessionTree;
|
|
34
|
+
isSessionDiverged(agentName: string, sessionId: string): boolean;
|
|
35
|
+
sessionGenesisPrevRoot(agentName: string, sessionId: string): Uint8Array | undefined;
|
|
36
|
+
/** The relay's session id for this session, when a live node holds one. */
|
|
37
|
+
relaySessionIdBytes(agentName: string, sessionId: string): Uint8Array | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Content received and verified but not yet appended, because it sits behind an ordering gap.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ RETURNS THE MAP, not a flattened list, and that is deliberate: the call site's expression
|
|
42
|
+
* (`[...heldHere.values()].some(...)`) moves across unchanged. Handing back a pre-computed array
|
|
43
|
+
* of hashes would read as a tidy-up and would be a behaviour change wearing a refactor's clothes.
|
|
44
|
+
*/
|
|
45
|
+
heldContentFor(agentName: string, sessionId: string): ReadonlyMap<number, {
|
|
46
|
+
contentHashHex: string;
|
|
47
|
+
}> | undefined;
|
|
48
|
+
}
|
|
49
|
+
export declare class AuthorshipVerifier {
|
|
50
|
+
#private;
|
|
51
|
+
constructor(ctx: AuthorshipContext);
|
|
52
|
+
/**
|
|
53
|
+
* `DOD-M15-AUTHORSHIP-ABSENT-1` — DID THIS SENDER PROVE THEY WROTE THIS MESSAGE?
|
|
54
|
+
*
|
|
55
|
+
* The one place that answers it, for both callers, so "checked" cannot mean two different things
|
|
56
|
+
* in two places. It takes the signature as an ARGUMENT rather than digging it out of a structure,
|
|
57
|
+
* which is the whole of the fix: the signature used to be read only from `structure2_cbor` — the
|
|
58
|
+
* RELAY's record — so a receiver could not check authorship without a relay record, and refusing
|
|
59
|
+
* on its absence would have made the relay a precondition for reading mail. The content frame now
|
|
60
|
+
* carries the signature beside the bytes it signs, exactly as `hash_submit` always has, and this
|
|
61
|
+
* method does not care which of the two handed it over.
|
|
62
|
+
*
|
|
63
|
+
* It VERIFIES and it does not LOG. The severity of each verdict differs by caller — the content
|
|
64
|
+
* frame refuses an `unusable`, the park path shrugs at one — and a method that logged its own
|
|
65
|
+
* conclusion would either report a refusal that did not happen or stay silent on one that did.
|
|
66
|
+
*/
|
|
67
|
+
verifyAuthorshipClaim(agentName: string, sessionId: string, structure1Cbor: Uint8Array, senderSignature: Uint8Array, contentHash: Uint8Array): AuthorshipVerdict;
|
|
68
|
+
noteReceivedFromCounterparty(agentName: string, sessionId: string, contentHash: Uint8Array): void;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=authorship-verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorship-verification.d.ts","sourceRoot":"","sources":["../src/authorship-verification.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD,OAAO,EACL,KAAK,iBAAiB,EAQvB,MAAM,yBAAyB,CAAC;AAEjC;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IACzD,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAAC;IAC7E,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAAC;IAClE,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IACjE,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IACrF,2EAA2E;IAC3E,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAClF;;;;;;OAMG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE;QAAE,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,SAAS,CAAC;CACnH;AAED,qBAAa,kBAAkB;;gBAGjB,GAAG,EAAE,iBAAiB;IAkBpC;;;;;;;;;;;;;;SAcK;IACH,qBAAqB,CACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,UAAU,EAC1B,eAAe,EAAE,UAAU,EAC3B,WAAW,EAAE,UAAU,GACtB,iBAAiB;IAgQtB,4BAA4B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,GAAG,IAAI;CA0JhG"}
|