@drakon-systems/ekho-sdk 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +4 -0
- package/dist/esm/identity/index.d.ts +5 -0
- package/dist/esm/identity/index.d.ts.map +1 -0
- package/dist/esm/identity/index.js +11 -0
- package/dist/esm/identity/index.js.map +1 -0
- package/dist/esm/identity/primitives.d.ts +53 -0
- package/dist/esm/identity/primitives.d.ts.map +1 -0
- package/dist/esm/identity/primitives.js +116 -0
- package/dist/esm/identity/primitives.js.map +1 -0
- package/dist/esm/identity/store.d.ts +67 -0
- package/dist/esm/identity/store.d.ts.map +1 -0
- package/dist/esm/identity/store.js +116 -0
- package/dist/esm/identity/store.js.map +1 -0
- package/dist/esm/identity/verification.d.ts +152 -0
- package/dist/esm/identity/verification.d.ts.map +1 -0
- package/dist/esm/identity/verification.js +492 -0
- package/dist/esm/identity/verification.js.map +1 -0
- package/dist/esm/identity/verify.d.ts +41 -0
- package/dist/esm/identity/verify.d.ts.map +1 -0
- package/dist/esm/identity/verify.js +92 -0
- package/dist/esm/identity/verify.js.map +1 -0
- package/dist/identity/index.d.ts +5 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +27 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/primitives.d.ts +53 -0
- package/dist/identity/primitives.d.ts.map +1 -0
- package/dist/identity/primitives.js +133 -0
- package/dist/identity/primitives.js.map +1 -0
- package/dist/identity/store.d.ts +67 -0
- package/dist/identity/store.d.ts.map +1 -0
- package/dist/identity/store.js +127 -0
- package/dist/identity/store.js.map +1 -0
- package/dist/identity/verification.d.ts +152 -0
- package/dist/identity/verification.d.ts.map +1 -0
- package/dist/identity/verification.js +503 -0
- package/dist/identity/verification.js.map +1 -0
- package/dist/identity/verify.d.ts +41 -0
- package/dist/identity/verify.d.ts.map +1 -0
- package/dist/identity/verify.js +96 -0
- package/dist/identity/verify.js.map +1 -0
- package/package.json +11 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// Agent-side verification wiring (mirrors the Python plugin's verification.py):
|
|
2
|
+
// pin sync (endorsement-chained, no relay TOFU), per-message verdicts, the
|
|
3
|
+
// graceful execution-authority gate, and outbound signing.
|
|
4
|
+
import { endorsementPayload, fromB64url, keyId as deriveKeyId, publicKeyB64urlFromSeed, revocationPayload, sha256Hex, signCanonical, unrevokePayload, verifyCanonical } from "./primitives.js";
|
|
5
|
+
import { verifyInbound } from "./verify.js";
|
|
6
|
+
import { identityPublicKey } from "./store.js";
|
|
7
|
+
/** How many identical advisory polls to swallow before a reminder. ~5s poll
|
|
8
|
+
* → a reminder about every 10 minutes, not 150k lines in a day and a half. */
|
|
9
|
+
export const ADVISORY_REVOCATION_WARNING_SUMMARY_EVERY = 120;
|
|
10
|
+
/** Process-local cap on identity+fleet throttle scopes. Eviction re-emits that
|
|
11
|
+
* scope's next first-seen warning; it cannot mute a different live scope. */
|
|
12
|
+
export const ADVISORY_REVOCATION_WARNING_MAX_SCOPES = 64;
|
|
13
|
+
const MAX_ADVISORY_KEYS_IN_LOG = 8;
|
|
14
|
+
const MAX_ADVISORY_KID_CHARS = 32;
|
|
15
|
+
/** Process-local only. Keyed by identity public key + fleet so one box cannot
|
|
16
|
+
* mute another identity or fleet. Cleared when the advisory set goes empty. */
|
|
17
|
+
const advisoryWarnByScope = new Map();
|
|
18
|
+
/** Test-only isolation for the process-local Map. Not plugin runtime API. */
|
|
19
|
+
export function resetAdvisoryRevocationWarningStateForTests() {
|
|
20
|
+
advisoryWarnByScope.clear();
|
|
21
|
+
}
|
|
22
|
+
function advisoryScopeKey(identity, fleetId) {
|
|
23
|
+
return `${identityPublicKey(identity)}\0${fleetId ?? ""}`;
|
|
24
|
+
}
|
|
25
|
+
function touchAdvisoryScope(scopeKey, entry) {
|
|
26
|
+
advisoryWarnByScope.delete(scopeKey);
|
|
27
|
+
advisoryWarnByScope.set(scopeKey, entry);
|
|
28
|
+
while (advisoryWarnByScope.size > ADVISORY_REVOCATION_WARNING_MAX_SCOPES) {
|
|
29
|
+
const oldest = advisoryWarnByScope.keys().next().value;
|
|
30
|
+
if (oldest === undefined || oldest === scopeKey)
|
|
31
|
+
break;
|
|
32
|
+
advisoryWarnByScope.delete(oldest);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Relay-supplied key IDs are untrusted. Escape C0/C1 controls (U+0000–U+001F,
|
|
36
|
+
* U+007F–U+009F, including CSI/OSC) and Unicode line separators so each warning
|
|
37
|
+
* stays one bounded line; truncate the escaped form, never the raw id. */
|
|
38
|
+
function formatKidForLog(kid) {
|
|
39
|
+
let escaped = "";
|
|
40
|
+
for (const ch of kid) {
|
|
41
|
+
const code = ch.charCodeAt(0);
|
|
42
|
+
if (ch === "\\")
|
|
43
|
+
escaped += "\\\\";
|
|
44
|
+
else if (ch === "\n")
|
|
45
|
+
escaped += "\\n";
|
|
46
|
+
else if (ch === "\r")
|
|
47
|
+
escaped += "\\r";
|
|
48
|
+
else if (ch === "\t")
|
|
49
|
+
escaped += "\\t";
|
|
50
|
+
else if (ch === "\0")
|
|
51
|
+
escaped += "\\0";
|
|
52
|
+
else if (code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029) {
|
|
53
|
+
escaped += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
escaped += ch;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (escaped.length <= MAX_ADVISORY_KID_CHARS)
|
|
60
|
+
return escaped;
|
|
61
|
+
return `${escaped.slice(0, MAX_ADVISORY_KID_CHARS)}...`;
|
|
62
|
+
}
|
|
63
|
+
/** Bounded sample for the log line. Fingerprinting uses the complete sorted set. */
|
|
64
|
+
function formatAdvisoryKeyList(kids) {
|
|
65
|
+
const shown = kids.slice(0, MAX_ADVISORY_KEYS_IN_LOG).map(formatKidForLog);
|
|
66
|
+
const omitted = kids.length - shown.length;
|
|
67
|
+
const list = shown.join(", ");
|
|
68
|
+
if (omitted <= 0)
|
|
69
|
+
return list;
|
|
70
|
+
return `${list} (+${omitted} omitted)`;
|
|
71
|
+
}
|
|
72
|
+
function formatAdvisoryWarning(kids) {
|
|
73
|
+
if (kids.length === 1) {
|
|
74
|
+
return (`[ekho] relay reports operator key ${formatKidForLog(kids[0])} as REVOKED without a valid revocation signature. ` +
|
|
75
|
+
`Treating the claim as ADVISORY: the key is NOT unpinned and NOT tombstoned, but it will not ` +
|
|
76
|
+
`be newly adopted until a signed revocation arrives.`);
|
|
77
|
+
}
|
|
78
|
+
return (`[ekho] relay reports ${kids.length} operator keys as REVOKED without a valid revocation signature: ` +
|
|
79
|
+
`${formatAdvisoryKeyList(kids)}. Treating the claim as ADVISORY: the keys are NOT unpinned and NOT ` +
|
|
80
|
+
`tombstoned, but they will not be newly adopted until a signed revocation arrives.`);
|
|
81
|
+
}
|
|
82
|
+
function formatAdvisorySummary(kids, suppressed) {
|
|
83
|
+
return (`[ekho] still treating ${kids.length} unsigned/invalid operator-key revocation claim(s) as ADVISORY ` +
|
|
84
|
+
`(suppressed ${suppressed} identical warning(s) this process). Keys: ${formatAdvisoryKeyList(kids)}. ` +
|
|
85
|
+
`Not unpinned, not tombstoned; still blocking new adoption.`);
|
|
86
|
+
}
|
|
87
|
+
/** One aggregate warning when the advisory set appears or changes; identical
|
|
88
|
+
* repeats are silent; a bounded reminder keeps the condition visible; an empty
|
|
89
|
+
* set forgets the scope so a later recurrence is logged again. Trust is
|
|
90
|
+
* untouched — this is log volume only. */
|
|
91
|
+
function warnAdvisoryRevocations(ctx, advisory) {
|
|
92
|
+
if (advisory.size === 0) {
|
|
93
|
+
advisoryWarnByScope.delete(ctx.scopeKey);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const kids = [...advisory].sort();
|
|
97
|
+
// Canonical JSON is unambiguous; NUL-join collides `["a\0b"]` with `["a","b"]`.
|
|
98
|
+
const fingerprint = sha256Hex(JSON.stringify(kids));
|
|
99
|
+
const prev = advisoryWarnByScope.get(ctx.scopeKey);
|
|
100
|
+
if (!prev || prev.fingerprint !== fingerprint) {
|
|
101
|
+
ctx.log?.warn?.(formatAdvisoryWarning(kids));
|
|
102
|
+
touchAdvisoryScope(ctx.scopeKey, { fingerprint, suppressed: 0 });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
prev.suppressed += 1;
|
|
106
|
+
if (prev.suppressed >= ADVISORY_REVOCATION_WARNING_SUMMARY_EVERY) {
|
|
107
|
+
ctx.log?.warn?.(formatAdvisorySummary(kids, prev.suppressed));
|
|
108
|
+
prev.suppressed = 0;
|
|
109
|
+
touchAdvisoryScope(ctx.scopeKey, prev);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Apply the relay's revocation / un-revocation claims to the working trust root
|
|
113
|
+
* (#27). Mutates `pinned`, `revokedLedger` and `admissions` in place; a claim
|
|
114
|
+
* only lands if a currently pinned operator key signed it. */
|
|
115
|
+
function applyRelayKeyClaims(ctx) {
|
|
116
|
+
const out = {
|
|
117
|
+
advisory: new Set(),
|
|
118
|
+
pinRemoved: false,
|
|
119
|
+
ledgerChanged: false,
|
|
120
|
+
admissionsChanged: false
|
|
121
|
+
};
|
|
122
|
+
clearTombstonesOnSignedUnrevoke(ctx, out);
|
|
123
|
+
applySignedRevocations(ctx, out);
|
|
124
|
+
warnAdvisoryRevocations(ctx, out.advisory);
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** A SIGNED un-revoke clears a tombstone so the key can be re-admitted through
|
|
128
|
+
* the endorsement chain. It never re-pins by itself — re-admission still costs
|
|
129
|
+
* a valid endorsement. Runs before the revocation pass so a valid revocation in
|
|
130
|
+
* the same batch still wins (fail closed).
|
|
131
|
+
*
|
|
132
|
+
* The unsigned ABSENCE of `revoked` must never clear a tombstone: that was the
|
|
133
|
+
* #14 hole, where a relay could resurrect a dead key just by not mentioning it.
|
|
134
|
+
*
|
|
135
|
+
* #48: the payload now binds revoked_at_being_cleared + issued_at + nonce.
|
|
136
|
+
* Missing bind fields refuse the un-revoke (fail closed). The relay still
|
|
137
|
+
* does not emit unrevoke_sig. */
|
|
138
|
+
function clearTombstonesOnSignedUnrevoke(ctx, out) {
|
|
139
|
+
for (const k of ctx.operatorKeys) {
|
|
140
|
+
const kid = k.key_id;
|
|
141
|
+
const sig = k.unrevoke_sig;
|
|
142
|
+
const revokedAt = k.unrevoke_revoked_at;
|
|
143
|
+
const issuedAt = k.unrevoke_issued_at;
|
|
144
|
+
const nonce = k.unrevoke_nonce;
|
|
145
|
+
if (!kid || !sig || !ctx.revokedLedger[kid])
|
|
146
|
+
continue;
|
|
147
|
+
if (!revokedAt || !issuedAt || !nonce) {
|
|
148
|
+
ctx.log?.warn?.(`[ekho] refusing un-revoke of operator key ${kid}: payload bind fields missing. ` +
|
|
149
|
+
`The tombstone stands.`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!ctx.fleetId || !ctx.signedByAPinnedKey(unrevokePayload(ctx.fleetId, kid, revokedAt, issuedAt, nonce), sig)) {
|
|
153
|
+
ctx.log?.warn?.(`[ekho] refusing un-revoke of operator key ${kid}: no currently pinned operator key signed it. ` +
|
|
154
|
+
`The tombstone stands.`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// #52: the compare half of the compare-and-swap. Signing over
|
|
158
|
+
// revoked_at_being_cleared is only worth anything if apply-time checks it
|
|
159
|
+
// against the tombstone actually standing right now — otherwise a valid
|
|
160
|
+
// un-revoke captured for an OLD revocation clears whatever NEWER one has
|
|
161
|
+
// replaced it.
|
|
162
|
+
if (revokedAt !== ctx.revokedLedger[kid]) {
|
|
163
|
+
ctx.log?.warn?.(`[ekho] refusing un-revoke of operator key ${kid}: it is bound to a revocation at ${revokedAt}, ` +
|
|
164
|
+
`but the live tombstone is at ${ctx.revokedLedger[kid]} — a newer revocation this un-revoke ` +
|
|
165
|
+
`never authorized. The tombstone stands.`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
delete ctx.revokedLedger[kid];
|
|
169
|
+
out.ledgerChanged = true;
|
|
170
|
+
ctx.log?.info?.(`[ekho] operator key ${kid} un-revoked by a signed operator instruction; tombstone cleared. ` +
|
|
171
|
+
`The key is NOT re-pinned — it has to be re-endorsed by a pinned key.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** A SIGNED revocation is the only thing that can remove trust. Anything less is
|
|
175
|
+
* recorded as advisory: the key is skipped for new adoption and nothing else. */
|
|
176
|
+
function applySignedRevocations(ctx, out) {
|
|
177
|
+
for (const k of ctx.operatorKeys) {
|
|
178
|
+
const kid = k.key_id;
|
|
179
|
+
if (!kid || !k.revoked)
|
|
180
|
+
continue;
|
|
181
|
+
const at = k.revoked_at;
|
|
182
|
+
const sig = k.revocation_sig;
|
|
183
|
+
const proven = Boolean(sig && at && ctx.fleetId && ctx.signedByAPinnedKey(revocationPayload(ctx.fleetId, kid, at), sig));
|
|
184
|
+
if (!proven) {
|
|
185
|
+
noteAdvisoryClaim(out, kid);
|
|
186
|
+
}
|
|
187
|
+
else if (isLastPinnedKey(ctx, kid)) {
|
|
188
|
+
noteLastRootRefusal(ctx, kid);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
dropRevokedKey(ctx, out, kid, at);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** The relay says a key is dead but cannot prove it. Skip the key for NEW
|
|
196
|
+
* adoption, write nothing. Visibility is the process-local aggregate throttle
|
|
197
|
+
* in warnAdvisoryRevocations — not a per-key line on every poll.
|
|
198
|
+
*
|
|
199
|
+
* Deleting the pin here is the tempting one-liner and it is wrong twice over:
|
|
200
|
+
* it lets whoever controls the relay drop trust unilaterally, AND a deleted pin
|
|
201
|
+
* is re-admitted on the very next poll by the same still-valid endorsement.
|
|
202
|
+
* Rejecting a key takes a revokedOperatorKeys tombstone, not a deletion — and a
|
|
203
|
+
* tombstone takes a signature. */
|
|
204
|
+
function noteAdvisoryClaim(out, kid) {
|
|
205
|
+
out.advisory.add(kid);
|
|
206
|
+
}
|
|
207
|
+
function isLastPinnedKey(ctx, kid) {
|
|
208
|
+
return Boolean(ctx.pinned[kid]) && Object.keys(ctx.pinned).length === 1;
|
|
209
|
+
}
|
|
210
|
+
/** Last-root protection: honoring this would leave the box with no trust root at
|
|
211
|
+
* all — indistinguishable from the attack the signature requirement just
|
|
212
|
+
* closed. Refuse the claim whole, so we never hold a key that is tombstoned
|
|
213
|
+
* (unrecoverable) and still pinned (still trusted). */
|
|
214
|
+
function noteLastRootRefusal(ctx, kid) {
|
|
215
|
+
ctx.log?.warn?.(`[ekho] refusing the signed revocation of operator key ${kid}: it is the LAST pinned operator key, ` +
|
|
216
|
+
`and honoring it would leave this agent with no trust root at all. Endorse a replacement key ` +
|
|
217
|
+
`first — the replacement makes this revocation safe to honor on the next poll.`);
|
|
218
|
+
}
|
|
219
|
+
/** Honor a proven revocation: tombstone, then unpin.
|
|
220
|
+
*
|
|
221
|
+
* #14: the tombstone is the durable half. Unpinning alone never made a
|
|
222
|
+
* revocation stick — the config seed re-added the key on the very next wake,
|
|
223
|
+
* and TOFU/chaining would too. */
|
|
224
|
+
function dropRevokedKey(ctx, out, kid, at) {
|
|
225
|
+
if (!ctx.revokedLedger[kid]) {
|
|
226
|
+
ctx.revokedLedger[kid] = at;
|
|
227
|
+
out.ledgerChanged = true;
|
|
228
|
+
}
|
|
229
|
+
if (ctx.pinned[kid]) {
|
|
230
|
+
Reflect.deleteProperty(ctx.pinned, kid);
|
|
231
|
+
out.pinRemoved = true;
|
|
232
|
+
ctx.log?.warn?.(`[ekho] operator key ${kid} is revoked (signed, at ${at}); it is no longer pinned.`);
|
|
233
|
+
}
|
|
234
|
+
if (ctx.admissions[kid]) {
|
|
235
|
+
// The admission record answers "why is this key trusted here?" — keep it
|
|
236
|
+
// only while the answer is "it is".
|
|
237
|
+
Reflect.deleteProperty(ctx.admissions, kid);
|
|
238
|
+
out.admissionsChanged = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Update pinned operator keys from the inbox. Adds a new key ONLY if endorsed by
|
|
242
|
+
* an already-pinned key; removes one ONLY on a SIGNED revocation. Returns true
|
|
243
|
+
* if the identity changed (and so must be persisted).
|
|
244
|
+
*
|
|
245
|
+
* Trust mutates in one direction only, and never on the relay's say-so (#27).
|
|
246
|
+
* An unsigned `revoked: true` is a HINT: the key is skipped for new adoption
|
|
247
|
+
* and the claim is logged, but nothing is written and nothing is unpinned.
|
|
248
|
+
* Treating it as authoritative (the #14 regression) meant one poll from a
|
|
249
|
+
* compromised relay could tombstone and unpin an entire fleet's trust root,
|
|
250
|
+
* permanently — the tombstone survives restarts by design, so the damage was
|
|
251
|
+
* not even recoverable by restarting against an honest relay.
|
|
252
|
+
*
|
|
253
|
+
* One deliberate exception to "no relay TOFU" (#5): an identity that has never
|
|
254
|
+
* pinned ANY key can't grow a chain — endorsements need an already-pinned root,
|
|
255
|
+
* so verification stayed dormant forever on every agent nobody hand-configured
|
|
256
|
+
* (the Aug 2026 silent-drop incident). First contact with an empty pin set
|
|
257
|
+
* adopts the relay's current non-revoked keys as the trust root, exactly once
|
|
258
|
+
* (tofuAt latches). Enrollment already trusts the relay this much — it accepts
|
|
259
|
+
* the shared secret over the same channel. Pre-pinning via config/env skips
|
|
260
|
+
* TOFU entirely and stays the stronger option. */
|
|
261
|
+
export function syncPinnedOperatorKeys(identity, operatorKeys, fleetId, log = console) {
|
|
262
|
+
const pinned = { ...identity.pinnedOperatorKeys };
|
|
263
|
+
const revokedLedger = { ...(identity.revokedOperatorKeys ?? {}) };
|
|
264
|
+
const admissions = { ...(identity.operatorKeyAdmissions ?? {}) };
|
|
265
|
+
// The keys allowed to authorize a trust-root mutation this poll, snapshotted
|
|
266
|
+
// BEFORE any of them are applied. Frozen on purpose: verifying against the
|
|
267
|
+
// live map would make the outcome depend on the order the relay happened to
|
|
268
|
+
// serve its entries in, which is the relay's choice, not ours.
|
|
269
|
+
const authorities = { ...identity.pinnedOperatorKeys };
|
|
270
|
+
const signedByAPinnedKey = (payload, sig) => {
|
|
271
|
+
for (const pub of Object.values(authorities)) {
|
|
272
|
+
if (verifyCanonical(payload, sig, fromB64url(pub)))
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
};
|
|
277
|
+
// Phases 1 and 2 (#27) — mutates `pinned` / `revokedLedger` / `admissions` in
|
|
278
|
+
// place and reports back what it touched.
|
|
279
|
+
const claims = applyRelayKeyClaims({
|
|
280
|
+
operatorKeys,
|
|
281
|
+
fleetId,
|
|
282
|
+
log,
|
|
283
|
+
pinned,
|
|
284
|
+
revokedLedger,
|
|
285
|
+
admissions,
|
|
286
|
+
signedByAPinnedKey,
|
|
287
|
+
scopeKey: advisoryScopeKey(identity, fleetId)
|
|
288
|
+
});
|
|
289
|
+
const advisory = claims.advisory;
|
|
290
|
+
let changed = claims.pinRemoved;
|
|
291
|
+
let admissionsChanged = claims.admissionsChanged;
|
|
292
|
+
if (claims.ledgerChanged) {
|
|
293
|
+
identity.revokedOperatorKeys = revokedLedger;
|
|
294
|
+
changed = true; // must persist, even when the key was never pinned here
|
|
295
|
+
}
|
|
296
|
+
if (Object.keys(pinned).length === 0 && !identity.tofuAt) {
|
|
297
|
+
let adopted = false;
|
|
298
|
+
const at = new Date().toISOString();
|
|
299
|
+
for (const k of operatorKeys) {
|
|
300
|
+
if (k.key_id && k.public_key && !advisory.has(k.key_id) && !revokedLedger[k.key_id]) {
|
|
301
|
+
pinned[k.key_id] = k.public_key;
|
|
302
|
+
admissions[k.key_id] = { admitted_by: "tofu", admitted_at: at }; // #26
|
|
303
|
+
admissionsChanged = true;
|
|
304
|
+
adopted = true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (adopted) {
|
|
308
|
+
// Latch only when something was adopted — an empty roster now must not
|
|
309
|
+
// burn the one TOFU opportunity of a fresh identity. (Tracked separately
|
|
310
|
+
// from `changed`, which a revocation tombstone alone can now set.)
|
|
311
|
+
identity.tofuAt = at;
|
|
312
|
+
identity.pinnedOperatorKeys = pinned;
|
|
313
|
+
identity.operatorKeyAdmissions = admissions;
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const k of operatorKeys) {
|
|
318
|
+
const kid = k.key_id;
|
|
319
|
+
if (!kid)
|
|
320
|
+
continue;
|
|
321
|
+
if (pinned[kid])
|
|
322
|
+
continue;
|
|
323
|
+
if (revokedLedger[kid])
|
|
324
|
+
continue; // #14: a tombstoned key never comes back
|
|
325
|
+
if (advisory.has(kid))
|
|
326
|
+
continue; // #27: claimed-revoked → no NEW adoption
|
|
327
|
+
const endorser = k.endorsed_by_key_id;
|
|
328
|
+
const esig = k.endorsement_sig;
|
|
329
|
+
const pub = k.public_key;
|
|
330
|
+
if (endorser && esig && pub && fleetId && pinned[endorser]) {
|
|
331
|
+
if (verifyCanonical(endorsementPayload(fleetId, kid, pub), esig, fromB64url(pinned[endorser]))) {
|
|
332
|
+
pinned[kid] = pub;
|
|
333
|
+
changed = true;
|
|
334
|
+
// #26: keep the endorsement we just verified, so this box can answer
|
|
335
|
+
// "why is this key trusted here?" offline — the relay is exactly the
|
|
336
|
+
// party we would otherwise have to ask.
|
|
337
|
+
admissions[kid] = {
|
|
338
|
+
admitted_by: "chain",
|
|
339
|
+
endorsed_by_key_id: endorser,
|
|
340
|
+
endorsement_sig: esig,
|
|
341
|
+
admitted_at: new Date().toISOString()
|
|
342
|
+
};
|
|
343
|
+
admissionsChanged = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (changed)
|
|
348
|
+
identity.pinnedOperatorKeys = pinned;
|
|
349
|
+
if (admissionsChanged) {
|
|
350
|
+
identity.operatorKeyAdmissions = admissions;
|
|
351
|
+
changed = true;
|
|
352
|
+
}
|
|
353
|
+
return changed;
|
|
354
|
+
}
|
|
355
|
+
export function verifyBatch(messages, opts) {
|
|
356
|
+
const operatorKeys = { ...(opts.identity.pinnedOperatorKeys ?? {}) };
|
|
357
|
+
const out = {};
|
|
358
|
+
if (Object.keys(operatorKeys).length === 0 || !opts.fleetId) {
|
|
359
|
+
for (const m of messages)
|
|
360
|
+
out[String(m.message_id)] = null;
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
const rosterByAgent = {};
|
|
364
|
+
for (const r of opts.roster)
|
|
365
|
+
if (r.agent_id)
|
|
366
|
+
rosterByAgent[String(r.agent_id)] = r;
|
|
367
|
+
for (const m of messages) {
|
|
368
|
+
out[String(m.message_id)] = verifyInbound(m, {
|
|
369
|
+
selfAgentId: opts.selfAgentId,
|
|
370
|
+
fleetId: opts.fleetId,
|
|
371
|
+
operatorKeys,
|
|
372
|
+
rosterByAgent,
|
|
373
|
+
seenNonces: opts.seenNonces,
|
|
374
|
+
now: opts.now
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
export function parseRequireSignedMode(raw) {
|
|
380
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
381
|
+
return v === "require" || v === "off" ? v : "warn";
|
|
382
|
+
}
|
|
383
|
+
/** The execution-authority gate. Signed → act iff verified; signed-but-invalid →
|
|
384
|
+
* blocked; unsigned → per requireSigned mode (see above). */
|
|
385
|
+
export function shouldAutowake(msg, verification, operatorTrusted, peerEnabled, requireSigned = "warn") {
|
|
386
|
+
const isOperator = msg.sender_kind === "operator";
|
|
387
|
+
const signed = Boolean(isOperator ? msg.operator_sig : msg.agent_sig);
|
|
388
|
+
if (isOperator) {
|
|
389
|
+
if (signed && verification)
|
|
390
|
+
return Boolean(verification.verified);
|
|
391
|
+
return Boolean(operatorTrusted);
|
|
392
|
+
}
|
|
393
|
+
if (!peerEnabled)
|
|
394
|
+
return false;
|
|
395
|
+
if (requireSigned === "require") {
|
|
396
|
+
// Fail closed: signed AND verified, nothing less. A null verification
|
|
397
|
+
// (no pinned keys yet) is exactly the dormant state this mode refuses
|
|
398
|
+
// to treat as trustworthy.
|
|
399
|
+
return Boolean(signed && verification && verification.verified);
|
|
400
|
+
}
|
|
401
|
+
if (signed && verification)
|
|
402
|
+
return Boolean(verification.verified);
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
/** Sign an outbound peer message so recipients can verify it came from us.
|
|
406
|
+
*
|
|
407
|
+
* v2 (#9) extends coverage to message_type, priority and the attachment ids —
|
|
408
|
+
* previously a compromised relay could relabel a message (direct → alert) or
|
|
409
|
+
* swap its attachments under a still-valid signature. Compatibility is free in
|
|
410
|
+
* both directions: verifiers check the signature over the WHOLE canonical and
|
|
411
|
+
* only bind the fields they know, so old verifiers accept v2 envelopes and new
|
|
412
|
+
* verifiers enforce the extra bindings only when the envelope declares v>=2. */
|
|
413
|
+
export function buildSignedSendFields(opts) {
|
|
414
|
+
const seed = new Uint8Array(Buffer.from(opts.identity.seedHex, "hex"));
|
|
415
|
+
const kid = deriveKeyId(fromB64url(publicKeyB64urlFromSeed(seed)));
|
|
416
|
+
const canonical = {
|
|
417
|
+
v: 2,
|
|
418
|
+
fleet_id: opts.fleetId,
|
|
419
|
+
sender_agent_id: opts.selfAgentId,
|
|
420
|
+
key_id: kid,
|
|
421
|
+
recipient: opts.recipient,
|
|
422
|
+
conversation_id: opts.conversationId,
|
|
423
|
+
body_sha256: sha256Hex(opts.bodyText),
|
|
424
|
+
sent_at: opts.sentAt,
|
|
425
|
+
nonce: opts.nonce,
|
|
426
|
+
message_type: opts.messageType,
|
|
427
|
+
priority: opts.priority,
|
|
428
|
+
attachments: [...(opts.attachments ?? [])].sort()
|
|
429
|
+
};
|
|
430
|
+
return { agent_sig: signCanonical(canonical, seed), key_id: kid, sig_canonical: canonical };
|
|
431
|
+
}
|
|
432
|
+
/** The fail-closed default (#20): when verification cannot run — no pinned
|
|
433
|
+
* operator keys, no fleet id, no identity — every snapshot is UNVERIFIED.
|
|
434
|
+
* Never "it has a signature field, so call it signed". */
|
|
435
|
+
export const NO_SNAPSHOT_VERIFICATION = () => false;
|
|
436
|
+
/**
|
|
437
|
+
* Build the snapshot verifier for one turn, from the same trust root the
|
|
438
|
+
* autowake path uses: this agent's pinned operator keys plus the batch roster.
|
|
439
|
+
*
|
|
440
|
+
* Runs the ordinary inbound verifier (`verifyInbound`) over a snapshot mapped
|
|
441
|
+
* into its `SignedMessage` shape — there is deliberately no second, laxer
|
|
442
|
+
* verifier for quoted text. Two consequences worth naming:
|
|
443
|
+
*
|
|
444
|
+
* - An older relay that omits `sig_canonical` (or the v2-bound `message_type` /
|
|
445
|
+
* `priority` / `attachments`) simply fails the checks, so its snapshots read
|
|
446
|
+
* UNVERIFIED. That is the intended direction to fail in.
|
|
447
|
+
* - The nonce set is fresh per turn rather than the live seen-nonce set. A
|
|
448
|
+
* quote is by definition a re-presentation of a message that may already
|
|
449
|
+
* have been delivered and had its nonce burned; the burn exists to stop a
|
|
450
|
+
* replayed message WAKING a turn, and a snapshot can never wake one. The
|
|
451
|
+
* staleness window is left at the verifier's default, so a snapshot replayed
|
|
452
|
+
* from beyond it still reads UNVERIFIED.
|
|
453
|
+
*/
|
|
454
|
+
export function makeSnapshotVerifier(opts) {
|
|
455
|
+
const operatorKeys = { ...(opts.identity?.pinnedOperatorKeys ?? {}) };
|
|
456
|
+
const fleetId = opts.fleetId;
|
|
457
|
+
if (Object.keys(operatorKeys).length === 0 || !fleetId)
|
|
458
|
+
return NO_SNAPSHOT_VERIFICATION;
|
|
459
|
+
const rosterByAgent = {};
|
|
460
|
+
for (const r of opts.roster ?? [])
|
|
461
|
+
if (r.agent_id)
|
|
462
|
+
rosterByAgent[String(r.agent_id)] = r;
|
|
463
|
+
return (snapshot) => {
|
|
464
|
+
if (!snapshot || typeof snapshot !== "object")
|
|
465
|
+
return false;
|
|
466
|
+
const asMessage = {
|
|
467
|
+
message_id: snapshot.message_id,
|
|
468
|
+
sender_kind: snapshot.sender_kind,
|
|
469
|
+
sender_agent_id: snapshot.sender_agent_id,
|
|
470
|
+
message_type: snapshot.message_type,
|
|
471
|
+
priority: snapshot.priority,
|
|
472
|
+
// `text` -> `body.text`: body_sha256 must bind the snapshot's own text.
|
|
473
|
+
body: {
|
|
474
|
+
text: typeof snapshot.text === "string" ? snapshot.text : "",
|
|
475
|
+
attachments: Array.isArray(snapshot.attachments) ? snapshot.attachments : []
|
|
476
|
+
},
|
|
477
|
+
operator_sig: snapshot.operator_sig ?? null,
|
|
478
|
+
agent_sig: snapshot.agent_sig ?? null,
|
|
479
|
+
key_id: snapshot.key_id ?? null,
|
|
480
|
+
sig_canonical: snapshot.sig_canonical ?? null
|
|
481
|
+
};
|
|
482
|
+
return verifyInbound(asMessage, {
|
|
483
|
+
selfAgentId: opts.selfAgentId,
|
|
484
|
+
fleetId,
|
|
485
|
+
operatorKeys,
|
|
486
|
+
rosterByAgent,
|
|
487
|
+
seenNonces: new Set(),
|
|
488
|
+
now: opts.now
|
|
489
|
+
}).verified;
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=verification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verification.js","sourceRoot":"","sources":["../../../src/identity/verification.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface SignedMessage {
|
|
2
|
+
message_id?: string;
|
|
3
|
+
sender_kind?: string;
|
|
4
|
+
sender_agent_id?: string;
|
|
5
|
+
message_type?: string;
|
|
6
|
+
priority?: string;
|
|
7
|
+
body?: {
|
|
8
|
+
text?: string;
|
|
9
|
+
} & Record<string, unknown>;
|
|
10
|
+
operator_sig?: string | null;
|
|
11
|
+
agent_sig?: string | null;
|
|
12
|
+
key_id?: string | null;
|
|
13
|
+
sig_canonical?: Record<string, unknown> | null;
|
|
14
|
+
}
|
|
15
|
+
export interface RosterEntryLike {
|
|
16
|
+
agent_id?: string;
|
|
17
|
+
identity_public_key?: string | null;
|
|
18
|
+
key_id?: string | null;
|
|
19
|
+
endorsed_by_key_id?: string | null;
|
|
20
|
+
endorsement_sig?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface VerifyResult {
|
|
23
|
+
verified: boolean;
|
|
24
|
+
kind: "operator" | "peer";
|
|
25
|
+
reason: string | null;
|
|
26
|
+
keyId: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface VerifyOpts {
|
|
29
|
+
selfAgentId: string;
|
|
30
|
+
fleetId: string;
|
|
31
|
+
operatorKeys: Record<string, string>;
|
|
32
|
+
rosterByAgent: Record<string, RosterEntryLike>;
|
|
33
|
+
seenNonces: Set<string>;
|
|
34
|
+
now: Date;
|
|
35
|
+
maxSkewSeconds?: number;
|
|
36
|
+
maxFutureSkewSeconds?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare const MAX_PAST_SKEW_SECONDS: number;
|
|
39
|
+
export declare const MAX_FUTURE_SKEW_SECONDS = 300;
|
|
40
|
+
export declare function verifyInbound(msg: SignedMessage, opts: VerifyOpts): VerifyResult;
|
|
41
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/identity/verify.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAChD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,UAAU,GAAG,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC/C,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,GAAG,EAAE,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AASD,eAAO,MAAM,qBAAqB,QAAc,CAAC;AACjD,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,UAAU,GAAG,YAAY,CA8EhF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Inbound message verification — the agent independently decides whether to trust
|
|
2
|
+
// a message, rather than trusting the relay's attribution. Mirrors the Python
|
|
3
|
+
// plugin's ekho.verify byte-for-byte (operator key pinned; peer key must be
|
|
4
|
+
// operator-endorsed). Same 7 binding checks.
|
|
5
|
+
import { agentKeyEndorsementPayload, fromB64url, sha256Hex, verifyCanonical } from "./primitives.js";
|
|
6
|
+
// The past-window must cover the relay's max message TTL (86400s) plus delivery
|
|
7
|
+
// slack: the relay legitimately holds a message for its whole TTL while the
|
|
8
|
+
// recipient is down (gateway restart, sleeping laptop), and 300s of "staleness"
|
|
9
|
+
// was silently discarding every signed peer message delivered 5-15+ min late —
|
|
10
|
+
// acked but never waking the recipient. Replay is defended by the nonce burn
|
|
11
|
+
// and the relay's server-side replay_nonces, not by this window. The
|
|
12
|
+
// future-window stays tight: a sent_at ahead of our clock is only clock skew.
|
|
13
|
+
export const MAX_PAST_SKEW_SECONDS = 86400 + 300;
|
|
14
|
+
export const MAX_FUTURE_SKEW_SECONDS = 300;
|
|
15
|
+
export function verifyInbound(msg, opts) {
|
|
16
|
+
const isOperator = msg.sender_kind === "operator";
|
|
17
|
+
const kind = isOperator ? "operator" : "peer";
|
|
18
|
+
const keyIdOf = msg.key_id ?? null;
|
|
19
|
+
const fail = (reason) => ({ verified: false, kind, reason, keyId: keyIdOf });
|
|
20
|
+
const canonical = msg.sig_canonical;
|
|
21
|
+
const sig = isOperator ? msg.operator_sig : msg.agent_sig;
|
|
22
|
+
if (!sig || !canonical || !msg.key_id)
|
|
23
|
+
return fail("unsigned");
|
|
24
|
+
// 1. Resolve the signer's public key (peers must be operator-rooted).
|
|
25
|
+
let signerPub;
|
|
26
|
+
if (isOperator) {
|
|
27
|
+
const pub = opts.operatorKeys[msg.key_id];
|
|
28
|
+
if (!pub)
|
|
29
|
+
return fail("unknown-operator-key");
|
|
30
|
+
signerPub = pub;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const entry = opts.rosterByAgent[msg.sender_agent_id ?? ""];
|
|
34
|
+
if (!entry || !entry.identity_public_key || entry.key_id !== msg.key_id) {
|
|
35
|
+
return fail("unknown-sender-key");
|
|
36
|
+
}
|
|
37
|
+
if (!entry.endorsed_by_key_id || !entry.endorsement_sig)
|
|
38
|
+
return fail("sender-key-unendorsed");
|
|
39
|
+
const endorserPub = opts.operatorKeys[entry.endorsed_by_key_id];
|
|
40
|
+
if (!endorserPub)
|
|
41
|
+
return fail("endorser-not-pinned");
|
|
42
|
+
const endorsement = agentKeyEndorsementPayload(opts.fleetId, msg.sender_agent_id ?? "", entry.key_id ?? "", entry.identity_public_key);
|
|
43
|
+
if (!verifyCanonical(endorsement, entry.endorsement_sig, fromB64url(endorserPub))) {
|
|
44
|
+
return fail("bad-endorsement");
|
|
45
|
+
}
|
|
46
|
+
signerPub = entry.identity_public_key;
|
|
47
|
+
}
|
|
48
|
+
// 2. The signature itself must verify over the canonical payload.
|
|
49
|
+
if (!verifyCanonical(canonical, sig, fromB64url(signerPub)))
|
|
50
|
+
return fail("bad-signature");
|
|
51
|
+
// 3-7. Bind the signed payload to THIS message.
|
|
52
|
+
const c = canonical;
|
|
53
|
+
if (c.key_id !== msg.key_id)
|
|
54
|
+
return fail("key-id-mismatch");
|
|
55
|
+
if (c.fleet_id !== opts.fleetId)
|
|
56
|
+
return fail("fleet-mismatch");
|
|
57
|
+
if (!isOperator && c.sender_agent_id !== msg.sender_agent_id)
|
|
58
|
+
return fail("sender-mismatch");
|
|
59
|
+
const recipient = (c.recipient ?? {});
|
|
60
|
+
if (recipient.kind === "agent" && recipient.id !== opts.selfAgentId)
|
|
61
|
+
return fail("recipient-mismatch");
|
|
62
|
+
const text = typeof msg.body?.text === "string" ? msg.body.text : "";
|
|
63
|
+
if (c.body_sha256 !== sha256Hex(text))
|
|
64
|
+
return fail("body-mismatch");
|
|
65
|
+
// v2 envelopes (#9) additionally bind type, priority and attachments — a
|
|
66
|
+
// relay relabelling a message or swapping its attachments breaks the binding.
|
|
67
|
+
// v1 envelopes (older signers) skip these checks during the transition.
|
|
68
|
+
if (Number(c.v ?? 1) >= 2) {
|
|
69
|
+
if (c.message_type !== (msg.message_type ?? ""))
|
|
70
|
+
return fail("type-mismatch");
|
|
71
|
+
if (c.priority !== (msg.priority ?? ""))
|
|
72
|
+
return fail("priority-mismatch");
|
|
73
|
+
const declared = Array.isArray(c.attachments) ? c.attachments.map(String).sort() : [];
|
|
74
|
+
const actual = Array.isArray(msg.body?.attachments) ? msg.body.attachments.map(String).sort() : [];
|
|
75
|
+
if (declared.length !== actual.length || declared.some((a, i) => a !== actual[i])) {
|
|
76
|
+
return fail("attachments-mismatch");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const sentAt = Date.parse(String(c.sent_at));
|
|
80
|
+
if (Number.isNaN(sentAt))
|
|
81
|
+
return fail("bad-timestamp");
|
|
82
|
+
const ageMs = opts.now.getTime() - sentAt;
|
|
83
|
+
if (ageMs > (opts.maxSkewSeconds ?? MAX_PAST_SKEW_SECONDS) * 1000 ||
|
|
84
|
+
-ageMs > (opts.maxFutureSkewSeconds ?? MAX_FUTURE_SKEW_SECONDS) * 1000) {
|
|
85
|
+
return fail("stale");
|
|
86
|
+
}
|
|
87
|
+
const nonce = c.nonce;
|
|
88
|
+
if (!nonce || opts.seenNonces.has(String(nonce)))
|
|
89
|
+
return fail("replay");
|
|
90
|
+
return { verified: true, kind, reason: null, keyId: msg.key_id };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=verify.js.map
|