Skip to main content

agentmux_common/
jekt_sign.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Jekt sender signing/verification — three schemes, one signed-material
5//! format:
6//!   - Host-tier: HMAC-SHA256, one shared key per agent (`sign_jekt`/
7//!     `verify_jekt`). A 1:1 sender-to-local-srv relationship, so a
8//!     symmetric key is fine — the srv instance is the sole verifier.
9//!   - WAN, reagent only: Ed25519 against one pinned service keypair
10//!     (`verify_reagent_jekt`). Multi-party (any receiving instance must
11//!     verify without being able to forge), hence asymmetric.
12//!   - LAN, any agent: Ed25519, one keypair per agent
13//!     (`sign_lan_jekt`/`verify_lan_jekt`). Same multi-party reasoning as
14//!     WAN, but per-sender instead of one pinned key — see
15//!     docs/specs/SPEC_JEKT_LAN_TIER_SIGNING_2026_08_15.md.
16//!
17//! Shared between `agentmux-mcp` (the `SendMessage` tool signs an outgoing
18//! jekt with the calling agent's own key, read from its `AGENTMUX_JEKT_KEY`/
19//! `AGENTMUX_LAN_KEY` process env vars) and `agentmux-srv` (the reactive
20//! handler verifies an incoming jekt's claimed signature against the
21//! claimed sender's key, looked up server-side by agent_id — see
22//! `crates/srv/src/backend/storage/agent_jekt_keys.rs` and
23//! `agent_lan_keys.rs`). Living here, not in either binary, is what
24//! guarantees the two can never independently drift on the signed-material
25//! format.
26//!
27//! See docs/specs/SPEC_JEKT_TRUST_LAYER_COMPLETION_2026_08_13.md §2.2,
28//! `docs/specs/SPEC_JEKT_SECURITY_AND_VISIBILITY_2026_07_01.md` §5.3 (the
29//! original spec's never-built "Phase 5" this module implements), and
30//! `docs/specs/SPEC_JEKT_LAN_TIER_SIGNING_2026_08_15.md`.
31//!
32//! The signed material binds sender, target, message id, and timestamp
33//! together (not just msgid+payload) so a valid signature can't be replayed
34//! against a different target or reattributed to a different claimed sender.
35
36use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
37use ed25519_dalek::{Signature, Verifier, VerifyingKey};
38use hmac::{Hmac, Mac};
39use sha2::Sha256;
40
41type HmacSha256 = Hmac<Sha256>;
42
43/// Field separator inside the signed material — a control character, not a
44/// character any of the individual fields can plausibly contain, so
45/// concatenation can't be ambiguated by a field that happens to contain the
46/// separator (msgid/agent ids are validated elsewhere to be alnum/_/-; the
47/// message body is free text, but shifting the separator's *position* within
48/// the body can't produce a colliding signature for a different logical
49/// (source, target, msgid, ts) tuple, which is what actually matters here).
50pub(crate) const FIELD_SEP: char = '\u{1}';
51
52fn signed_material(msgid: &str, source_agent: &str, target_agent: &str, ts_secs: i64, message: &str) -> String {
53    format!("{msgid}{FIELD_SEP}{source_agent}{FIELD_SEP}{target_agent}{FIELD_SEP}{ts_secs}{FIELD_SEP}{message}")
54}
55
56/// Decode a base64-encoded signing key (e.g. from the `AGENTMUX_JEKT_KEY`
57/// process env var). Returns `None` on malformed input rather than erroring
58/// — callers should treat that the same as "no key available" (sign
59/// nothing, deliver unsigned rather than fail the send). Exposed here so
60/// `agentmux-mcp` doesn't need its own `base64` dependency just for this.
61pub fn decode_key(b64: &str) -> Option<Vec<u8>> {
62    BASE64.decode(b64).ok()
63}
64
65/// Sign a jekt with the sender's key. Returns a base64-encoded HMAC-SHA256
66/// tag. `key` of any length is accepted (HMAC's own key-hashing handles
67/// that) but `agent_jekt_keys::agent_jekt_key_ensure` always mints 32 bytes.
68pub fn sign_jekt(key: &[u8], msgid: &str, source_agent: &str, target_agent: &str, ts_secs: i64, message: &str) -> String {
69    let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
70    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts a key of any length");
71    mac.update(material.as_bytes());
72    BASE64.encode(mac.finalize().into_bytes())
73}
74
75/// Verify a claimed signature against the sender's stored key. Constant-time
76/// comparison via `Mac::verify_slice` (not a manual `==`), so this can't leak
77/// timing information about how much of the tag matched.
78pub fn verify_jekt(
79    key: &[u8],
80    msgid: &str,
81    source_agent: &str,
82    target_agent: &str,
83    ts_secs: i64,
84    message: &str,
85    sig_b64: &str,
86) -> bool {
87    let Ok(sig) = BASE64.decode(sig_b64) else { return false };
88    let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
89    let Ok(mut mac) = HmacSha256::new_from_slice(key) else { return false };
90    mac.update(material.as_bytes());
91    mac.verify_slice(&sig).is_ok()
92}
93
94// ---- WAN-tier sender signing (Ed25519) ----
95//
96// Distinct signing scheme from host-tier's HMAC above, and deliberately so:
97// host-tier's key is minted per-instance and never leaves the srv process
98// that owns it, so a symmetric secret shared only between "sign" and
99// "verify" on the very same machine is the right tool. A WAN-tier service
100// sender (e.g. the GitHub review-notification consumer, "reagent") is
101// verified by *every* AgentMux instance on the network, not just its own
102// account — an HMAC key distributed that widely is no longer meaningfully
103// secret. Asymmetric signing lets the private key stay in exactly one
104// place (the signing key held in AgentMux's private secret store) while
105// every client ships only the public key, openly.
106//
107// Reuses the exact same `signed_material` construction as host-tier HMAC —
108// same replay/reattribution-resistance properties (msgid+source+target+ts
109// bound into what's signed), just a different signature algorithm over it.
110
111/// Pinned Ed25519 public keys for AgentMux's own WAN-tier service senders.
112/// Keyed by `key_id` (carried alongside the signature in the wire format)
113/// so a future key rotation can add a new entry without invalidating
114/// verification of messages already in flight when signed under the old
115/// key. The matching private key is never present in this repo — it is
116/// the signing key held in AgentMux's private secret store, used only by
117/// the signing service.
118///
119/// `reagent-v1-dev` was a placeholder minted during initial implementation
120/// (its private half was generated in a local shell for wiring/testing
121/// purposes and was treated as already-exposed from the moment it was
122/// generated) — kept registered here, but the signing service no longer
123/// signs under it, so no genuine production traffic uses it. Left in place only
124/// so any message that happened to be signed under it before the rotation
125/// still verifies, matching this map's whole "add, don't replace" design.
126///
127/// `reagent-v1` (2026-08-14) is the real production key: generated so that
128/// the private key was never printed, logged, or otherwise appeared in any
129/// transcript — the private half is the signing key held in AgentMux's
130/// private secret store, consistent with the exposure lesson
131/// `reagent-v1-dev` was kept around specifically to document.
132fn reagent_public_key(key_id: &str) -> Option<[u8; 32]> {
133    match key_id {
134        "reagent-v1-dev" => Some([
135            104, 98, 241, 120, 99, 50, 230, 185, 228, 117, 241, 110, 130, 85, 252, 75, 141, 152,
136            224, 92, 125, 154, 123, 40, 96, 149, 214, 172, 94, 120, 116, 200,
137        ]),
138        "reagent-v1" => Some([
139            185, 72, 99, 0, 119, 36, 15, 50, 117, 0, 134, 93, 66, 39, 65, 21, 64, 229, 3, 157,
140            238, 188, 79, 40, 74, 42, 99, 108, 28, 101, 243, 205,
141        ]),
142        _ => None,
143    }
144}
145
146/// The one `key_id` whose matching private key is believed secret today.
147/// `reagent-v1-dev` stays registered in `reagent_public_key` (so
148/// `verify_reagent_jekt` still checks its signatures), but its private half
149/// is documented above as exposed since it was generated, so a signature
150/// under it proves nothing about the sender. Receivers record a reagent
151/// signature as verified only through [`verify_trusted_reagent_jekt`],
152/// which requires this key; any other key yields `reagent_verified =
153/// Some(false)` — an active verification failure, forced sensitive.
154pub fn is_reagent_trusted_signing_key(key_id: &str) -> bool {
155    key_id == "reagent-v1"
156}
157
158/// Verify a WAN-tier jekt's claimed `reagent_sig` against the pinned public
159/// key for `key_id`. Returns `false` (never panics) for an unknown
160/// `key_id`, malformed base64, a malformed/wrong-length signature, or a
161/// signature that simply doesn't verify — callers should treat all of
162/// these identically: "not verified," rendering `SIG=invalid` rather than
163/// `SIG=verified` in the marker (see `wrap_jekt_message`'s doc comment).
164///
165/// This function alone does NOT make a verified sender: a message signed
166/// under `reagent-v1-dev` verifies here exactly like one under the
167/// production key. Receivers must use [`verify_trusted_reagent_jekt`].
168pub fn verify_reagent_jekt(
169    key_id: &str,
170    msgid: &str,
171    source_agent: &str,
172    target_agent: &str,
173    ts_secs: i64,
174    message: &str,
175    sig_b64: &str,
176) -> bool {
177    let Some(pubkey_bytes) = reagent_public_key(key_id) else { return false };
178    let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_bytes) else { return false };
179    let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
180    let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
181    let signature = Signature::from_bytes(&sig_arr);
182    let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
183    verifying_key.verify(material.as_bytes(), &signature).is_ok()
184}
185
186/// The verification result a receiver records as `reagent_verified`: the
187/// signature checks out AND its key is the trusted production key. A
188/// signature under any other registered key is not a verified sender.
189pub fn verify_trusted_reagent_jekt(
190    key_id: &str,
191    msgid: &str,
192    source_agent: &str,
193    target_agent: &str,
194    ts_secs: i64,
195    message: &str,
196    sig_b64: &str,
197) -> bool {
198    is_reagent_trusted_signing_key(key_id)
199        && verify_reagent_jekt(key_id, msgid, source_agent, target_agent, ts_secs, message, sig_b64)
200}
201
202// ── LAN-tier per-agent Ed25519 signing (SPEC_JEKT_LAN_TIER_SIGNING_2026_08_15.md) ──
203//
204// Asymmetric, not HMAC like host-tier: LAN is multi-party (any receiving
205// instance must be able to verify without being able to forge — the same
206// reason reagent's WAN signing above is asymmetric, just per-agent instead
207// of one pinned service key). Signed material is the exact same
208// `signed_material` host-tier and reagent both already use — one format,
209// three signature schemes layered on top depending on tier.
210
211/// Generate a fresh Ed25519 keypair for a newly-registered agent's LAN
212/// signing key. Returns `(public_key_bytes, private_key_seed_bytes)`, both
213/// 32 bytes raw — callers base64-encode for storage/transport (mirrors
214/// the `random_seed_bytes` + `BASE64.encode` split in the srv crate's
215/// `agent_lan_keys.rs`).
216///
217/// No RNG dependency needed beyond what's already used for HMAC key
218/// generation: `SigningKey::from_bytes` accepts any 32 bytes of randomness
219/// as a valid seed (deterministic key derivation from the seed, not a
220/// call into an RNG itself) — reusing the two-v4-UUIDs CSPRNG source
221/// `agent_lan_keys.rs::random_seed_bytes` already established avoids
222/// adding a `rand`/`getrandom` dependency here too.
223pub fn generate_lan_keypair(seed: [u8; 32]) -> ([u8; 32], [u8; 32]) {
224    let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
225    (signing_key.verifying_key().to_bytes(), seed)
226}
227
228/// Sign a jekt with the sending agent's own LAN private key (read from its
229/// `AGENTMUX_LAN_KEY` process env var, mirroring `sign_jekt`'s
230/// `AGENTMUX_JEKT_KEY` — called client-side, inside `agentmux-mcp`, never
231/// server-side). `private_key` must be exactly 32 bytes (the seed
232/// `generate_lan_keypair` produced) — returns `None` on any other length
233/// rather than panicking, same "missing/malformed key must never block
234/// sending" policy `sign_jekt`'s callers already rely on.
235pub fn sign_lan_jekt(
236    private_key: &[u8],
237    msgid: &str,
238    source_agent: &str,
239    target_agent: &str,
240    ts_secs: i64,
241    message: &str,
242) -> Option<String> {
243    let seed: [u8; 32] = private_key.try_into().ok()?;
244    let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
245    let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
246    let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
247    Some(BASE64.encode(signature.to_bytes()))
248}
249
250/// Verify a claimed LAN signature against the claimed sender's public key
251/// (looked up server-side by `source_agent`, over the LAN peer-lookup RPC —
252/// see `LanDiscovery::find_agent_lan_pubkey`). Returns `false` (never
253/// panics) for a malformed public key, malformed base64, a
254/// malformed/wrong-length signature, or a signature that simply doesn't
255/// verify — same "treat all of these identically as not verified" policy
256/// `verify_reagent_jekt` already documents.
257pub fn verify_lan_jekt(
258    public_key: &[u8],
259    msgid: &str,
260    source_agent: &str,
261    target_agent: &str,
262    ts_secs: i64,
263    message: &str,
264    sig_b64: &str,
265) -> bool {
266    let Ok(pubkey_arr) = <[u8; 32]>::try_from(public_key) else { return false };
267    let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_arr) else { return false };
268    let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
269    let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
270    let signature = Signature::from_bytes(&sig_arr);
271    let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
272    verifying_key.verify(material.as_bytes(), &signature).is_ok()
273}
274
275// ── Cross-channel per-agent Ed25519 signing (SPEC_JEKT_CROSS_CHANNEL_TRUST_2026_09_02.md §D5) ──
276//
277// Same machine, different AgentMux instance. Asymmetric for the same reason
278// LAN is: the receiving instance must be able to verify without being able to
279// forge — and unlike host-tier's HMAC, the key can therefore be published in
280// the host-global shared registry, which is what makes cross-channel
281// verification possible at all (§5 of the spec explains why globalizing the
282// symmetric host key instead would be strictly worse).
283//
284// Reuses the agent's EXISTING LAN keypair — no new key material, no new
285// provisioning step. What it does NOT reuse is the signed material.
286
287/// Domain separator for cross-channel signatures.
288///
289/// Without this, a cross-channel signature and a LAN signature over the same
290/// (msgid, source, target, ts, message) tuple would be byte-identical, signed
291/// by the byte-identical key — so either could be replayed as the other. The
292/// tiers make different trust claims (`TRUST=lan-verified` vs
293/// `TRUST=channel-verified`) and are verified against different key sources, so
294/// they must not be interchangeable. Cheap now; a flag day later.
295const CHANNEL_DOMAIN: &str = "amx-jekt-channel-v1";
296
297/// Signed material for a cross-channel jekt.
298///
299/// Differs from [`signed_material`] in two ways, both load-bearing:
300///  - the [`CHANNEL_DOMAIN`] prefix (see above), and
301///  - `source_channel`, which binds the signature to the channel that minted
302///    it. Without it, a signature legitimately produced by an agent running in
303///    channel A could be replayed as that same agent speaking from channel B —
304///    and since one agent name can legitimately be live in several channels at
305///    once (`AgentEntry::channel` exists precisely because of that), the
306///    receiver has no other way to tell those apart.
307fn channel_signed_material(
308    msgid: &str,
309    source_agent: &str,
310    source_channel: &str,
311    target_agent: &str,
312    ts_secs: i64,
313    message: &str,
314) -> String {
315    format!(
316        "{CHANNEL_DOMAIN}{FIELD_SEP}{msgid}{FIELD_SEP}{source_agent}{FIELD_SEP}\
317         {source_channel}{FIELD_SEP}{target_agent}{FIELD_SEP}{ts_secs}{FIELD_SEP}{message}"
318    )
319}
320
321/// Sign a cross-channel jekt with the sending agent's own LAN private key
322/// (`AGENTMUX_LAN_KEY`) — called client-side in `agentmux-mcp`, never
323/// server-side, exactly like [`sign_lan_jekt`].
324///
325/// `private_key` must be exactly 32 bytes (the seed [`generate_lan_keypair`]
326/// produced); returns `None` on any other length rather than panicking — same
327/// "a missing or malformed key must never block sending" policy the other
328/// signers use. A message that can't be signed is delivered unsigned, and the
329/// receiver decides what that means.
330pub fn sign_channel_jekt(
331    private_key: &[u8],
332    msgid: &str,
333    source_agent: &str,
334    source_channel: &str,
335    target_agent: &str,
336    ts_secs: i64,
337    message: &str,
338) -> Option<String> {
339    let seed: [u8; 32] = private_key.try_into().ok()?;
340    let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
341    let material =
342        channel_signed_material(msgid, source_agent, source_channel, target_agent, ts_secs, message);
343    let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
344    Some(BASE64.encode(signature.to_bytes()))
345}
346
347/// Verify a claimed cross-channel signature against the claimed sender's
348/// published public key (read server-side from the host-global shared registry
349/// entry — `AgentEntry::jekt_public_key`).
350///
351/// Returns `false`, never panics, for a malformed public key, malformed
352/// base64, a wrong-length signature, or a signature that simply doesn't verify
353/// — the same "all of these are identically 'not verified'" policy
354/// [`verify_lan_jekt`] and [`verify_reagent_jekt`] already document. The
355/// caller distinguishes "couldn't check" from "checked and failed"; this
356/// function only answers the second question.
357#[allow(clippy::too_many_arguments)]
358pub fn verify_channel_jekt(
359    public_key: &[u8],
360    msgid: &str,
361    source_agent: &str,
362    source_channel: &str,
363    target_agent: &str,
364    ts_secs: i64,
365    message: &str,
366    sig_b64: &str,
367) -> bool {
368    let Ok(pubkey_arr) = <[u8; 32]>::try_from(public_key) else { return false };
369    let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_arr) else { return false };
370    let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
371    let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
372    let signature = Signature::from_bytes(&sig_arr);
373    let material =
374        channel_signed_material(msgid, source_agent, source_channel, target_agent, ts_secs, message);
375    verifying_key.verify(material.as_bytes(), &signature).is_ok()
376}
377
378// ── Cross-channel v2: bound to the sender's UID (identity M4d-6) ──
379mod channel_v2;
380pub use channel_v2::{sign_channel_jekt_v2, verify_channel_jekt_v2};
381mod lan_v2;
382pub use lan_v2::{sign_lan_jekt_v2, verify_lan_jekt_v2};
383
384// ── General agent-to-agent WAN signing ──
385//
386// Issue #2586's second half. Asymmetric for the same reason LAN and
387// cross-channel are — the receiving instance must verify without being able to
388// forge — but with a different key-distribution story: the public half is
389// published to muxbus's agent-ownership registry rather than fetched from a
390// peer, because a WAN counterpart is by definition unreachable directly.
391//
392// **Only the signing primitives live here.** Publication, lookup, and
393// verification wiring are deliberately NOT part of this change: verifying a
394// WAN signature means resolving `(sender_account, source_agent)` to exactly
395// one key, which waits on relay-side work tracked in the private cloud repo.
396// Minting and signing are safe
397// and useful ahead of that — an agent only gets a key when it is spawned, so
398// starting now is what makes verification meaningful later instead of
399// silently applying to nobody.
400
401/// Domain separator for general agent-to-agent WAN signatures.
402///
403/// Exactly the same reasoning as [`CHANNEL_DOMAIN`], and load-bearing for the
404/// same reason: `db_agent_wan_keys` is a *separate* keypair from
405/// `db_agent_lan_keys`, but nothing structurally prevents a future change from
406/// reusing one key across tiers, and an undomained signature over the shared
407/// (msgid, source, target, ts, message) tuple would then be replayable from
408/// one tier to the other. The tiers make different trust claims
409/// (`TRUST=lan-verified` vs `TRUST=wan-verified`) against different key
410/// sources — a LAN peer's own answer vs. the cloud registry — so they must
411/// never be interchangeable.
412const WAN_DOMAIN: &str = "amx-jekt-wan-v1";
413
414/// Signed material for a general agent-to-agent WAN jekt.
415///
416/// Binds the sending **instance** (`source_host` + `source_channel`) as well
417/// as the agent, for the same reason [`channel_signed_material`] binds
418/// `source_channel`: one agent name can
419/// legitimately be live on several instances under one account — a different
420/// machine, or a different build channel on the same machine — and **each
421/// mints its own keypair**, because each srv instance has its own database.
422/// The instance therefore selects which key a signature is checked against,
423/// so it has to be part of what the signature covers; otherwise a mislabelled
424/// instance would silently select a different key rather than fail.
425///
426/// Deliberately does NOT bind the sender's muxbus account, even though
427/// verification resolves the key by `(account, host, channel, agent)`: the
428/// account is a cloud-side fact the signing process has no trustworthy view
429/// of at sign time, and binding a client-asserted account into the signature
430/// would let a sender pick which account's key it is checked against. The
431/// account is resolved server-side from the authenticated caller instead
432/// (§3.4.1) and selects the key; the signature then proves the agent and its
433/// instance. Host and channel are different: both are injected into the
434/// agent's env at spawn, so the sending process knows them reliably, and a
435/// wrong value selects a key the signature cannot verify under — it grants
436/// nothing.
437fn wan_signed_material(
438    msgid: &str,
439    source_agent: &str,
440    source_host: &str,
441    source_channel: &str,
442    target_agent: &str,
443    ts_secs: i64,
444    message: &str,
445) -> String {
446    format!(
447        "{WAN_DOMAIN}{FIELD_SEP}{msgid}{FIELD_SEP}{source_agent}{FIELD_SEP}\
448         {source_host}{FIELD_SEP}{source_channel}{FIELD_SEP}\
449         {target_agent}{FIELD_SEP}{ts_secs}{FIELD_SEP}{message}"
450    )
451}
452
453/// Generate an agent's WAN keypair from 32 bytes of seed entropy, returning
454/// `(public_key, private_key)`.
455///
456/// Identical construction to [`generate_lan_keypair`] — same curve, same seed
457/// handling — but kept as its own entry point because the two keys are
458/// independent by design and must be minted separately: LAN public keys are
459/// permanently trust-on-first-use pinned by peers
460/// (`db_lan_peer_pubkey_pins`), which is exactly why
461/// `SPEC_JEKT_HOST_KEY_TTL_ROTATION_2026_09_14.md` §4 declined to rotate them,
462/// whereas WAN keys are served from an authoritative registry and are intended
463/// to rotate. One key cannot have both lifecycles.
464pub fn generate_wan_keypair(seed: [u8; 32]) -> ([u8; 32], [u8; 32]) {
465    let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
466    (signing_key.verifying_key().to_bytes(), seed)
467}
468
469/// Sign a jekt with the sending agent's own WAN private key (read from its
470/// `AGENTMUX_WAN_KEY` process env var, client-side inside `agentmux-mcp`,
471/// never server-side — same placement as [`sign_lan_jekt`]).
472///
473/// `private_key` must be exactly 32 bytes (the seed [`generate_wan_keypair`]
474/// produced); any other length returns `None` rather than panicking, matching
475/// the "a missing or malformed key must never block sending" policy every
476/// other signer here follows.
477pub fn sign_wan_jekt(
478    private_key: &[u8],
479    msgid: &str,
480    source_agent: &str,
481    source_host: &str,
482    source_channel: &str,
483    target_agent: &str,
484    ts_secs: i64,
485    message: &str,
486) -> Option<String> {
487    let seed: [u8; 32] = private_key.try_into().ok()?;
488    let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
489    let material = wan_signed_material(
490        msgid,
491        source_agent,
492        source_host,
493        source_channel,
494        target_agent,
495        ts_secs,
496        message,
497    );
498    let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
499    Some(BASE64.encode(signature.to_bytes()))
500}
501
502/// Verify a claimed WAN signature against the claimed sender's published
503/// public key.
504///
505/// Returns `false` (never panics) for a malformed key, malformed base64, a
506/// wrong-length signature, or a signature that simply doesn't verify — the
507/// same "treat all of these identically as not verified" policy
508/// [`verify_lan_jekt`] and [`verify_reagent_jekt`] document. Note that the
509/// *caller* is responsible for the distinction that actually matters to tier
510/// escalation: "no key could be resolved" (nothing to check against) is not
511/// the same as "a key was resolved and this failed" (active forgery).
512pub fn verify_wan_jekt(
513    public_key: &[u8],
514    msgid: &str,
515    source_agent: &str,
516    source_host: &str,
517    source_channel: &str,
518    target_agent: &str,
519    ts_secs: i64,
520    message: &str,
521    sig_b64: &str,
522) -> bool {
523    let Ok(pubkey_arr) = <[u8; 32]>::try_from(public_key) else { return false };
524    let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_arr) else { return false };
525    let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
526    let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
527    let signature = Signature::from_bytes(&sig_arr);
528    let material = wan_signed_material(
529        msgid,
530        source_agent,
531        source_host,
532        source_channel,
533        target_agent,
534        ts_secs,
535        message,
536    );
537    verifying_key.verify(material.as_bytes(), &signature).is_ok()
538}
539
540// ── W3-S: same-account WAN verification (SPEC_WAN_JEKT_VERIFICATION_2026_09_24.md) ──
541//
542// The pure half of W3-S: identifiers, the instance-signed agent-key
543// certificate, the revocation record, and the checks a receiver runs on the
544// fields the cloud relay carried. Storage, publishing, directory lookup and
545// replay tracking live in agentmux-srv; the cloud re-implements the chain
546// check in TypeScript and must match the
547// byte formats here exactly — the fixed vectors in the tests below are the
548// contract between the two.
549
550/// Domain separator for an instance's certificate over one agent key (§2.2).
551const WAN_CERT_DOMAIN: &str = "amx-wan-agent-cert-v1";
552
553/// Domain separator for an instance revocation record (§2.2).
554const WAN_REVOKE_DOMAIN: &str = "amx-wan-instance-revoke-v1";
555
556/// How old a carried `wan_ts_secs` may be: the relay's own 1800 s delivery
557/// TTL plus 300 s of slack (§2.4). A shorter window would reject messages
558/// the relay is still legitimately holding.
559pub const WAN_SIG_MAX_AGE_SECS: i64 = 1800 + 300;
560
561/// How far in the future a carried `wan_ts_secs` may be — desktop clock skew
562/// between sender and receiver (§2.4).
563pub const WAN_SIG_MAX_FUTURE_SKEW_SECS: i64 = 300;
564
565/// Length of an instance id: 128 bits as unpadded base32.
566pub const WAN_INSTANCE_ID_LEN: usize = 26;
567
568/// Length of a key fingerprint: 256 bits as unpadded base64url.
569pub const WAN_KEY_FP_LEN: usize = 43;
570
571const BASE32_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
572
573/// RFC 4648 base32, lowercase, no padding.
574fn base32_lower_nopad(bytes: &[u8]) -> String {
575    let mut out = String::with_capacity((bytes.len() * 8).div_ceil(5));
576    let mut buffer: u32 = 0;
577    let mut bits = 0;
578    for &byte in bytes {
579        buffer = (buffer << 8) | u32::from(byte);
580        bits += 8;
581        while bits >= 5 {
582            bits -= 5;
583            out.push(BASE32_ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
584        }
585    }
586    if bits > 0 {
587        out.push(BASE32_ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
588    }
589    out
590}
591
592/// An instance's id: the first 128 bits of `SHA-256(instance public key)`,
593/// lowercase unpadded base32 — 26 chars of `[a-z2-7]` (§2.2).
594///
595/// Not secret. It names the key, so anything signed by the instance key can
596/// be checked against the id without trusting whoever supplied the key.
597pub fn wan_instance_id(instance_public_key: &[u8; 32]) -> String {
598    use sha2::Digest;
599    let digest = Sha256::digest(instance_public_key);
600    base32_lower_nopad(&digest[..16])
601}
602
603/// An agent key's fingerprint: `SHA-256(agent public key)`, unpadded
604/// base64url — 43 chars. Carried as `wan_key_fp`, an unsigned lookup hint
605/// (§2.1): a wrong value finds a key the signature won't verify under.
606pub fn wan_key_fingerprint(agent_public_key: &[u8; 32]) -> String {
607    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
608    use sha2::Digest;
609    URL_SAFE_NO_PAD.encode(Sha256::digest(agent_public_key))
610}
611
612/// Whether `host_hint` is safe to render: `[a-z0-9.-]{1,48}` (§2.6). The
613/// hint is sender-chosen; anything else renders as `?`.
614pub fn is_valid_wan_host_hint(host_hint: &str) -> bool {
615    (1..=48).contains(&host_hint.len())
616        && host_hint.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'.' || b == b'-')
617}
618
619/// The label a human sees for a verified instance: `<host_hint>~<id8>`
620/// (§2.6). The suffix comes from the id the chain proved, never from the
621/// sender's own label, so two instances claiming the same hostname still
622/// render differently.
623pub fn wan_instance_display_label(host_hint: &str, verified_instance_id: &str) -> String {
624    let hint = if is_valid_wan_host_hint(host_hint) { host_hint } else { "?" };
625    let id8: String = verified_instance_id.chars().take(8).collect();
626    format!("{hint}~{id8}")
627}
628
629/// Whether a carried `wan_ts_secs` is inside the freshness window (§2.4).
630/// Both bounds inclusive. Outside it the verdict is `None` (stale), not
631/// `Some(false)`: a stale signature grants nothing that dropping it wouldn't.
632pub fn wan_sig_is_fresh(wan_ts_secs: i64, now_secs: i64) -> bool {
633    now_secs.saturating_sub(wan_ts_secs) <= WAN_SIG_MAX_AGE_SECS
634        && wan_ts_secs.saturating_sub(now_secs) <= WAN_SIG_MAX_FUTURE_SKEW_SECS
635}
636
637/// ASCII case-insensitive equality after trimming — the one comparison every
638/// W3-S identifier check uses (§2.1). Agent ids are ASCII-only, and the cloud
639/// lowercases most of them but stores `github*` sources raw.
640fn wan_id_eq(a: &str, b: &str) -> bool {
641    a.trim().eq_ignore_ascii_case(b.trim())
642}
643
644fn wan_cert_material(
645    instance_id: &str,
646    agent_id: &str,
647    channel: &str,
648    agent_public_key_b64: &str,
649    host_hint: &str,
650    issued_at: i64,
651) -> String {
652    let agent_id = agent_id.to_ascii_lowercase();
653    format!(
654        "{WAN_CERT_DOMAIN}{FIELD_SEP}{instance_id}{FIELD_SEP}{agent_id}{FIELD_SEP}\
655         {channel}{FIELD_SEP}{agent_public_key_b64}{FIELD_SEP}{host_hint}{FIELD_SEP}{issued_at}"
656    )
657}
658
659fn wan_revoke_material(old_instance_id: &str, new_instance_id: &str, revoked_at: i64) -> String {
660    format!("{WAN_REVOKE_DOMAIN}{FIELD_SEP}{old_instance_id}{FIELD_SEP}{new_instance_id}{FIELD_SEP}{revoked_at}")
661}
662
663pub(crate) fn ed25519_sign_b64(private_key: &[u8], material: &str) -> Option<String> {
664    let seed: [u8; 32] = private_key.try_into().ok()?;
665    let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
666    let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
667    Some(BASE64.encode(signature.to_bytes()))
668}
669
670pub(crate) fn ed25519_verify_b64(public_key: &[u8; 32], material: &str, sig_b64: &str) -> bool {
671    let Ok(verifying_key) = VerifyingKey::from_bytes(public_key) else { return false };
672    let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
673    let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
674    verifying_key.verify(material.as_bytes(), &Signature::from_bytes(&sig_arr)).is_ok()
675}
676
677pub(crate) fn decode_public_key_b64(b64: &str) -> Option<[u8; 32]> {
678    BASE64.decode(b64).ok()?.try_into().ok()
679}
680
681/// One entry in the per-account key directory (§2.2): an agent key,
682/// certified by the instance that minted it. Public keys are standard
683/// base64 of the raw 32 bytes; `cert_sig` is standard base64 Ed25519.
684///
685/// Every field is untrusted until [`WanKeyRecord::check_chain`] passes —
686/// the record arrives from the cloud, which may be tampering.
687#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
688pub struct WanKeyRecord {
689    pub instance_id: String,
690    pub agent_id: String,
691    pub channel: String,
692    pub key_fp: String,
693    pub instance_pubkey: String,
694    pub agent_pubkey: String,
695    pub host_hint: String,
696    pub issued_at: i64,
697    pub cert_sig: String,
698}
699
700impl WanKeyRecord {
701    /// Certify `agent_public_key` with the instance key. Called by the srv
702    /// that owns the instance; returns `None` for a malformed instance
703    /// private key, never panics.
704    pub fn certify(
705        instance_private_key: &[u8],
706        agent_id: &str,
707        channel: &str,
708        agent_public_key: &[u8; 32],
709        host_hint: &str,
710        issued_at: i64,
711    ) -> Option<Self> {
712        let seed: [u8; 32] = instance_private_key.try_into().ok()?;
713        let instance_public_key = ed25519_dalek::SigningKey::from_bytes(&seed).verifying_key().to_bytes();
714        let instance_id = wan_instance_id(&instance_public_key);
715        let agent_pubkey = BASE64.encode(agent_public_key);
716        let material = wan_cert_material(&instance_id, agent_id, channel, &agent_pubkey, host_hint, issued_at);
717        Some(Self {
718            instance_id,
719            agent_id: agent_id.to_ascii_lowercase(),
720            channel: channel.to_string(),
721            key_fp: wan_key_fingerprint(agent_public_key),
722            instance_pubkey: BASE64.encode(instance_public_key),
723            agent_pubkey,
724            host_hint: host_hint.to_string(),
725            issued_at,
726            cert_sig: ed25519_sign_b64(&seed, &material)?,
727        })
728    }
729
730    /// The chain every holder of a record checks — the cloud on `PUT`
731    /// (hygiene) and the receiver before trusting it (the real anchor):
732    /// the instance id is the hash of the instance key, the fingerprint is
733    /// the hash of the agent key, and the instance key signed the
734    /// certificate. Returns the decoded agent public key on success.
735    pub fn check_chain(&self) -> Option<[u8; 32]> {
736        let instance_public_key = decode_public_key_b64(&self.instance_pubkey)?;
737        let agent_public_key = decode_public_key_b64(&self.agent_pubkey)?;
738        if wan_instance_id(&instance_public_key) != self.instance_id
739            || wan_key_fingerprint(&agent_public_key) != self.key_fp
740            || self.agent_id != self.agent_id.to_ascii_lowercase()
741        {
742            return None;
743        }
744        let material = wan_cert_material(
745            &self.instance_id,
746            &self.agent_id,
747            &self.channel,
748            &self.agent_pubkey,
749            &self.host_hint,
750            self.issued_at,
751        );
752        ed25519_verify_b64(&instance_public_key, &material, &self.cert_sig).then_some(agent_public_key)
753    }
754}
755
756/// An instance revocation (§2.2), signed by the **old** instance key so only
757/// the owner of that key can retire it. Sticky: nothing un-revokes an id.
758#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
759pub struct WanRevocation {
760    pub old_instance_id: String,
761    pub old_instance_pubkey: String,
762    pub new_instance_id: String,
763    pub revoked_at: i64,
764    pub sig: String,
765}
766
767impl WanRevocation {
768    /// Sign a revocation of the instance whose private key is `old_private_key`.
769    pub fn sign(old_private_key: &[u8], new_instance_id: &str, revoked_at: i64) -> Option<Self> {
770        let seed: [u8; 32] = old_private_key.try_into().ok()?;
771        let old_public_key = ed25519_dalek::SigningKey::from_bytes(&seed).verifying_key().to_bytes();
772        let old_instance_id = wan_instance_id(&old_public_key);
773        let material = wan_revoke_material(&old_instance_id, new_instance_id, revoked_at);
774        Some(Self {
775            old_instance_id,
776            old_instance_pubkey: BASE64.encode(old_public_key),
777            new_instance_id: new_instance_id.to_string(),
778            revoked_at,
779            sig: ed25519_sign_b64(&seed, &material)?,
780        })
781    }
782
783    /// The old id is the hash of the old key, and the old key signed it.
784    pub fn verify(&self) -> bool {
785        let Some(old_public_key) = decode_public_key_b64(&self.old_instance_pubkey) else { return false };
786        wan_instance_id(&old_public_key) == self.old_instance_id
787            && ed25519_verify_b64(
788                &old_public_key,
789                &wan_revoke_material(&self.old_instance_id, &self.new_instance_id, self.revoked_at),
790                &self.sig,
791            )
792    }
793}
794
795/// The signed tuple as carried through the relay (§2.1). All of it is
796/// sender-supplied; `key_fp` is a lookup hint and is not signed.
797#[derive(Debug, Clone, PartialEq, Eq)]
798pub struct WanCarried<'a> {
799    pub sig: &'a str,
800    pub msg_id: &'a str,
801    pub ts_secs: i64,
802    pub source_agent: &'a str,
803    pub target_agent: &'a str,
804    pub source_host: &'a str,
805    pub source_channel: &'a str,
806    pub key_fp: &'a str,
807}
808
809/// Why a carried signature did not verify. Each maps to the §2.3 verdict the
810/// caller records; the ones that are `None` there are "couldn't check",
811/// never a forgery signal.
812#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813pub enum WanCheckFailure {
814    /// The carried source or target isn't the one actually delivered →
815    /// `Some(false)`.
816    EnvelopeMismatch,
817    /// Outside the freshness window → `None`, audit `wan_sig_stale`.
818    Stale,
819    /// The directory record's chain is broken → `Some(false)`, audit
820    /// `wan_cert_invalid`. Only a tampering cloud or a bug produces this.
821    CertInvalid,
822    /// The record is for a different instance, channel, agent or key than
823    /// the carried one → `Some(false)`.
824    RecordMismatch,
825    /// The message signature doesn't verify under the certified key →
826    /// `Some(false)`.
827    BadSignature,
828}
829
830impl WanCheckFailure {
831    /// The `wan_verified` value this failure records: `None` for "couldn't
832    /// check", `Some(false)` for an active verification failure.
833    pub fn verdict(self) -> Option<bool> {
834        match self {
835            Self::Stale => None,
836            Self::EnvelopeMismatch | Self::CertInvalid | Self::RecordMismatch | Self::BadSignature => Some(false),
837        }
838    }
839}
840
841/// The checks that need no directory record, in the §2.3 order: the
842/// envelope binding, then freshness. `row_source_agent` is the cloud row's
843/// `source_agent`; `polled_agent_id` is the agent the subscriber is
844/// delivering to — **not** a row field, so a cloud can't move a message
845/// signed for one agent into another's queue.
846pub fn check_wan_envelope(
847    carried: &WanCarried<'_>,
848    row_source_agent: &str,
849    polled_agent_id: &str,
850    now_secs: i64,
851) -> Result<(), WanCheckFailure> {
852    if !wan_id_eq(carried.source_agent, row_source_agent) || !wan_id_eq(carried.target_agent, polled_agent_id) {
853        return Err(WanCheckFailure::EnvelopeMismatch);
854    }
855    if !wan_sig_is_fresh(carried.ts_secs, now_secs) {
856        return Err(WanCheckFailure::Stale);
857    }
858    Ok(())
859}
860
861/// The checks against a directory record, in the §2.3 order: the chain, the
862/// record matching what was carried, then the message signature. Run after
863/// [`check_wan_envelope`] passes and a record was found. Replay is the
864/// caller's (it needs state).
865pub fn verify_wan_against_record(
866    carried: &WanCarried<'_>,
867    record: &WanKeyRecord,
868    message: &str,
869) -> Result<(), WanCheckFailure> {
870    let agent_public_key = record.check_chain().ok_or(WanCheckFailure::CertInvalid)?;
871    // ASCII case-insensitive for instance, channel and agent, as §2.3
872    // specifies (Codex P2 on #3727). Safe: the signature still covers the
873    // carried values exactly, the certificate the record's, and a channel that
874    // differs only in case is a different data dir, hence a different
875    // instance id.
876    if !wan_id_eq(&record.instance_id, carried.source_host)
877        || !wan_id_eq(&record.channel, carried.source_channel)
878        || !wan_id_eq(&record.agent_id, carried.source_agent)
879        || record.key_fp != carried.key_fp
880    {
881        return Err(WanCheckFailure::RecordMismatch);
882    }
883    if !verify_wan_jekt(
884        &agent_public_key,
885        carried.msg_id,
886        carried.source_agent,
887        carried.source_host,
888        carried.source_channel,
889        carried.target_agent,
890        carried.ts_secs,
891        message,
892        carried.sig,
893    ) {
894        return Err(WanCheckFailure::BadSignature);
895    }
896    Ok(())
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902
903    fn key() -> Vec<u8> {
904        vec![7u8; 32]
905    }
906
907    #[test]
908    fn a_correctly_signed_message_verifies() {
909        let k = key();
910        let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
911        assert!(verify_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
912    }
913
914    #[test]
915    fn wrong_key_fails_verification() {
916        let sig = sign_jekt(&key(), "msg-1", "agentx", "agenty", 1_000, "hello");
917        let other_key = vec![9u8; 32];
918        assert!(!verify_jekt(&other_key, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
919    }
920
921    #[test]
922    fn tampered_message_fails_verification() {
923        let k = key();
924        let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
925        assert!(!verify_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "goodbye", &sig));
926    }
927
928    #[test]
929    fn signature_cannot_be_replayed_against_a_different_target() {
930        // The whole point of binding target_agent into the signed material:
931        // a signature legitimately produced for agenty must not verify when
932        // replayed claiming a different target.
933        let k = key();
934        let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
935        assert!(!verify_jekt(&k, "msg-1", "agentx", "agentz", 1_000, "hello", &sig));
936    }
937
938    #[test]
939    fn signature_cannot_be_reattributed_to_a_different_claimed_sender() {
940        let k = key();
941        let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
942        assert!(!verify_jekt(&k, "msg-1", "agentz", "agenty", 1_000, "hello", &sig));
943    }
944
945    #[test]
946    fn different_msgid_fails_verification() {
947        let k = key();
948        let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
949        assert!(!verify_jekt(&k, "msg-2", "agentx", "agenty", 1_000, "hello", &sig));
950    }
951
952    #[test]
953    fn different_timestamp_fails_verification() {
954        let k = key();
955        let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
956        assert!(!verify_jekt(&k, "msg-1", "agentx", "agenty", 1_001, "hello", &sig));
957    }
958
959    #[test]
960    fn malformed_base64_signature_fails_verification_not_panics() {
961        assert!(!verify_jekt(&key(), "msg-1", "agentx", "agenty", 1_000, "hello", "not-valid-base64!!"));
962    }
963
964    #[test]
965    fn empty_signature_fails_verification() {
966        assert!(!verify_jekt(&key(), "msg-1", "agentx", "agenty", 1_000, "hello", ""));
967    }
968
969    // ---- verify_reagent_jekt (Ed25519) ----
970    //
971    // Fixture signature produced offline against the "reagent-v1-dev" pinned
972    // public key's matching private key, over signed_material("msg-1",
973    // "github-consumer", "agentx", 1000, "hello") — i.e. the exact material
974    // `signed_material()` above constructs for those arguments. Exercises the
975    // real pinned key path end to end rather than a separate test-only key.
976    const FIXTURE_SIG_B64: &str =
977        "QehidZjJa2jYLPIPYSsVxUlm86W5Fdbr9PV3P4HJyZwJ68/HZR9EaAL0MpcVtTuZJW2+MMGebc0RH9HITNJGCw==";
978
979    #[test]
980    fn a_correctly_signed_reagent_message_verifies() {
981        assert!(verify_reagent_jekt(
982            "reagent-v1-dev",
983            "msg-1",
984            "github-consumer",
985            "agentx",
986            1_000,
987            "hello",
988            FIXTURE_SIG_B64,
989        ));
990    }
991
992    // Same fixture material, signed under the real production key
993    // (`reagent-v1`, provisioned 2026-08-14 — see this key's own doc comment
994    // on `reagent_public_key` for how the private half was generated and
995    // provisioned without ever appearing in a transcript). Proves the
996    // production key is genuinely wired up and verifiable end to end, not
997    // just registered as an unused map entry.
998    const PROD_FIXTURE_SIG_B64: &str =
999        "FCFjcvAzla329a39u8fFxOvRWaH1R2fUn8RsGtP9RaIbLbaS3aXgAQ7YB4ssWV5TDvAeGrwSkHfoeGi11iCPBg==";
1000
1001    #[test]
1002    fn a_correctly_signed_reagent_message_verifies_under_the_production_key() {
1003        assert!(verify_reagent_jekt(
1004            "reagent-v1",
1005            "msg-1",
1006            "github-consumer",
1007            "agentx",
1008            1_000,
1009            "hello",
1010            PROD_FIXTURE_SIG_B64,
1011        ));
1012    }
1013
1014    #[test]
1015    fn the_dev_and_prod_reagent_keys_are_not_interchangeable() {
1016        // The prod fixture must NOT verify against the dev key id, and
1017        // vice versa -- proves these are genuinely two distinct keys, not
1018        // the same key registered twice under different names.
1019        assert!(!verify_reagent_jekt(
1020            "reagent-v1-dev",
1021            "msg-1",
1022            "github-consumer",
1023            "agentx",
1024            1_000,
1025            "hello",
1026            PROD_FIXTURE_SIG_B64,
1027        ));
1028        assert!(!verify_reagent_jekt(
1029            "reagent-v1",
1030            "msg-1",
1031            "github-consumer",
1032            "agentx",
1033            1_000,
1034            "hello",
1035            FIXTURE_SIG_B64,
1036        ));
1037    }
1038
1039    // ---- is_reagent_trusted_signing_key ----
1040    //
1041    // The dev key verifies signatures fine (it's a real registered key) but
1042    // must never be treated as authorization to relax TIER=sensitive — its
1043    // private half is documented as exposed since generation. Only the
1044    // production key is "trusted" in that stronger sense.
1045
1046    #[test]
1047    fn the_production_key_is_trusted_for_tier_relaxation() {
1048        assert!(is_reagent_trusted_signing_key("reagent-v1"));
1049    }
1050
1051    #[test]
1052    fn the_exposed_dev_key_is_not_trusted_for_tier_relaxation_even_though_it_verifies() {
1053        assert!(!is_reagent_trusted_signing_key("reagent-v1-dev"));
1054        // Sanity check: it's not untrusted because it's unregistered — it
1055        // genuinely verifies signatures, it's just not the trusted one.
1056        assert!(verify_reagent_jekt(
1057            "reagent-v1-dev",
1058            "msg-1",
1059            "github-consumer",
1060            "agentx",
1061            1_000,
1062            "hello",
1063            FIXTURE_SIG_B64,
1064        ));
1065    }
1066
1067    #[test]
1068    fn an_unregistered_key_id_is_not_trusted_for_tier_relaxation() {
1069        assert!(!is_reagent_trusted_signing_key("reagent-v2-does-not-exist"));
1070    }
1071
1072    // ---- verify_trusted_reagent_jekt ----
1073
1074    #[test]
1075    fn a_production_key_signature_is_a_trusted_verification() {
1076        assert!(verify_trusted_reagent_jekt(
1077            "reagent-v1",
1078            "msg-1",
1079            "github-consumer",
1080            "agentx",
1081            1_000,
1082            "hello",
1083            PROD_FIXTURE_SIG_B64,
1084        ));
1085    }
1086
1087    #[test]
1088    fn a_valid_dev_key_signature_is_not_a_trusted_verification() {
1089        assert!(!verify_trusted_reagent_jekt(
1090            "reagent-v1-dev",
1091            "msg-1",
1092            "github-consumer",
1093            "agentx",
1094            1_000,
1095            "hello",
1096            FIXTURE_SIG_B64,
1097        ));
1098    }
1099
1100    #[test]
1101    fn a_tampered_production_key_message_is_not_a_trusted_verification() {
1102        assert!(!verify_trusted_reagent_jekt(
1103            "reagent-v1",
1104            "msg-1",
1105            "github-consumer",
1106            "agentx",
1107            1_000,
1108            "hello, tampered",
1109            PROD_FIXTURE_SIG_B64,
1110        ));
1111    }
1112
1113    #[test]
1114    fn reagent_unknown_key_id_fails_verification() {
1115        assert!(!verify_reagent_jekt(
1116            "reagent-v2-does-not-exist",
1117            "msg-1",
1118            "github-consumer",
1119            "agentx",
1120            1_000,
1121            "hello",
1122            FIXTURE_SIG_B64,
1123        ));
1124    }
1125
1126    #[test]
1127    fn reagent_tampered_message_fails_verification() {
1128        assert!(!verify_reagent_jekt(
1129            "reagent-v1-dev",
1130            "msg-1",
1131            "github-consumer",
1132            "agentx",
1133            1_000,
1134            "goodbye",
1135            FIXTURE_SIG_B64,
1136        ));
1137    }
1138
1139    #[test]
1140    fn reagent_wrong_target_fails_verification() {
1141        // Same reattribution/replay resistance as host-tier HMAC — the
1142        // signature is bound to target_agent, so it can't be replayed
1143        // claiming delivery to a different agent.
1144        assert!(!verify_reagent_jekt(
1145            "reagent-v1-dev",
1146            "msg-1",
1147            "github-consumer",
1148            "agenty",
1149            1_000,
1150            "hello",
1151            FIXTURE_SIG_B64,
1152        ));
1153    }
1154
1155    #[test]
1156    fn reagent_wrong_source_fails_verification() {
1157        assert!(!verify_reagent_jekt(
1158            "reagent-v1-dev",
1159            "msg-1",
1160            "someone-else",
1161            "agentx",
1162            1_000,
1163            "hello",
1164            FIXTURE_SIG_B64,
1165        ));
1166    }
1167
1168    #[test]
1169    fn reagent_wrong_timestamp_fails_verification() {
1170        assert!(!verify_reagent_jekt(
1171            "reagent-v1-dev",
1172            "msg-1",
1173            "github-consumer",
1174            "agentx",
1175            1_001,
1176            "hello",
1177            FIXTURE_SIG_B64,
1178        ));
1179    }
1180
1181    #[test]
1182    fn reagent_wrong_msgid_fails_verification() {
1183        assert!(!verify_reagent_jekt(
1184            "reagent-v1-dev",
1185            "msg-2",
1186            "github-consumer",
1187            "agentx",
1188            1_000,
1189            "hello",
1190            FIXTURE_SIG_B64,
1191        ));
1192    }
1193
1194    #[test]
1195    fn reagent_malformed_base64_signature_fails_verification_not_panics() {
1196        assert!(!verify_reagent_jekt(
1197            "reagent-v1-dev",
1198            "msg-1",
1199            "github-consumer",
1200            "agentx",
1201            1_000,
1202            "hello",
1203            "not-valid-base64!!",
1204        ));
1205    }
1206
1207    #[test]
1208    fn reagent_wrong_length_signature_fails_verification_not_panics() {
1209        assert!(!verify_reagent_jekt(
1210            "reagent-v1-dev",
1211            "msg-1",
1212            "github-consumer",
1213            "agentx",
1214            1_000,
1215            "hello",
1216            "dG9vc2hvcnQ=", // "tooshort" base64 — decodes fine, wrong length
1217        ));
1218    }
1219
1220    #[test]
1221    fn reagent_empty_signature_fails_verification() {
1222        assert!(!verify_reagent_jekt(
1223            "reagent-v1-dev",
1224            "msg-1",
1225            "github-consumer",
1226            "agentx",
1227            1_000,
1228            "hello",
1229            "",
1230        ));
1231    }
1232
1233    // ---- LAN-tier Ed25519 signing ----
1234
1235    fn lan_seed(byte: u8) -> [u8; 32] {
1236        [byte; 32]
1237    }
1238
1239    #[test]
1240    fn lan_keypair_is_deterministic_from_seed_and_public_differs_from_private() {
1241        let (pub1, priv1) = generate_lan_keypair(lan_seed(3));
1242        let (pub2, priv2) = generate_lan_keypair(lan_seed(3));
1243        assert_eq!(pub1, pub2, "same seed must derive the same keypair");
1244        assert_eq!(priv1, priv2);
1245        assert_ne!(pub1, priv1, "public and private halves must differ");
1246    }
1247
1248    #[test]
1249    fn different_seeds_give_different_keypairs() {
1250        let (pub_a, _) = generate_lan_keypair(lan_seed(1));
1251        let (pub_b, _) = generate_lan_keypair(lan_seed(2));
1252        assert_ne!(pub_a, pub_b);
1253    }
1254
1255    #[test]
1256    fn a_correctly_signed_lan_message_verifies() {
1257        let (public_key, private_key) = generate_lan_keypair(lan_seed(5));
1258        let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1259        assert!(verify_lan_jekt(&public_key, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
1260    }
1261
1262    #[test]
1263    fn lan_wrong_key_fails_verification() {
1264        let (_, private_key) = generate_lan_keypair(lan_seed(5));
1265        let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1266        let (other_public, _) = generate_lan_keypair(lan_seed(9));
1267        assert!(!verify_lan_jekt(&other_public, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
1268    }
1269
1270    #[test]
1271    fn lan_tampered_message_fails_verification() {
1272        let (public_key, private_key) = generate_lan_keypair(lan_seed(5));
1273        let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1274        assert!(!verify_lan_jekt(&public_key, "msg-1", "agentx", "agenty", 1_000, "goodbye", &sig));
1275    }
1276
1277    #[test]
1278    fn lan_tampered_claimed_sender_fails_verification() {
1279        // Same signature, different claimed source_agent — binding sender
1280        // into the signed material means a captured signature can't be
1281        // reattributed to a different identity.
1282        let (public_key, private_key) = generate_lan_keypair(lan_seed(5));
1283        let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1284        assert!(!verify_lan_jekt(&public_key, "msg-1", "someoneelse", "agenty", 1_000, "hello", &sig));
1285    }
1286
1287    #[test]
1288    fn lan_signing_with_wrong_length_key_returns_none_not_panics() {
1289        assert!(sign_lan_jekt(&[1u8; 16], "msg-1", "agentx", "agenty", 1_000, "hello").is_none());
1290        assert!(sign_lan_jekt(&[], "msg-1", "agentx", "agenty", 1_000, "hello").is_none());
1291    }
1292
1293    #[test]
1294    fn lan_verify_with_wrong_length_public_key_fails_not_panics() {
1295        let (_, private_key) = generate_lan_keypair(lan_seed(5));
1296        let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1297        assert!(!verify_lan_jekt(&[1u8; 16], "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
1298    }
1299
1300    #[test]
1301    fn lan_verify_with_malformed_base64_signature_fails_not_panics() {
1302        let (public_key, _) = generate_lan_keypair(lan_seed(5));
1303        assert!(!verify_lan_jekt(
1304            &public_key,
1305            "msg-1",
1306            "agentx",
1307            "agenty",
1308            1_000,
1309            "hello",
1310            "not-valid-base64!!",
1311        ));
1312    }
1313
1314    #[test]
1315    fn lan_verify_with_wrong_length_signature_fails_not_panics() {
1316        let (public_key, _) = generate_lan_keypair(lan_seed(5));
1317        assert!(!verify_lan_jekt(
1318            &public_key,
1319            "msg-1",
1320            "agentx",
1321            "agenty",
1322            1_000,
1323            "hello",
1324            "dG9vc2hvcnQ=", // "tooshort" base64 — decodes fine, wrong length
1325        ));
1326    }
1327
1328    #[test]
1329    fn lan_verify_with_empty_signature_fails_not_panics() {
1330        let (public_key, _) = generate_lan_keypair(lan_seed(5));
1331        assert!(!verify_lan_jekt(&public_key, "msg-1", "agentx", "agenty", 1_000, "hello", ""));
1332    }
1333
1334    #[test]
1335    fn a_lan_signature_does_not_verify_against_reagent_or_host_schemes() {
1336        // Cross-scheme confusion check: a LAN Ed25519 signature must not be
1337        // mistakable for anything the HMAC verifier would accept (different
1338        // algorithm entirely, so this is really just documenting the type
1339        // boundary — verify_jekt takes a raw HMAC key, not an Ed25519 one).
1340        let (_, private_key) = generate_lan_keypair(lan_seed(5));
1341        let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1342        assert!(!verify_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
1343    }
1344
1345    // ---- Cross-channel Ed25519 signing (SPEC_JEKT_CROSS_CHANNEL_TRUST_2026_09_02.md §D5) ----
1346
1347    #[test]
1348    fn a_correctly_signed_channel_message_verifies() {
1349        let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1350        let sig =
1351            sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1352                .unwrap();
1353        assert!(verify_channel_jekt(
1354            &public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &sig
1355        ));
1356    }
1357
1358    #[test]
1359    fn channel_wrong_key_fails_verification() {
1360        let (_, private_key) = generate_lan_keypair(lan_seed(9));
1361        let (other_public, _) = generate_lan_keypair(lan_seed(10));
1362        let sig =
1363            sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1364                .unwrap();
1365        assert!(!verify_channel_jekt(
1366            &other_public, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &sig
1367        ));
1368    }
1369
1370    #[test]
1371    fn channel_tampered_fields_fail_verification() {
1372        let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1373        let sig =
1374            sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1375                .unwrap();
1376        // Every signed field must actually be covered.
1377        for (msgid, src, chan, tgt, ts, msg) in [
1378            ("msg-2", "agentx", "chan-a", "agenty", 1_000, "hello"),
1379            ("msg-1", "agentz", "chan-a", "agenty", 1_000, "hello"),
1380            ("msg-1", "agentx", "chan-a", "agentz", 1_000, "hello"),
1381            ("msg-1", "agentx", "chan-a", "agenty", 1_001, "hello"),
1382            ("msg-1", "agentx", "chan-a", "agenty", 1_000, "goodbye"),
1383        ] {
1384            assert!(
1385                !verify_channel_jekt(&public_key, msgid, src, chan, tgt, ts, msg, &sig),
1386                "tampering with a signed field must fail: {msgid}/{src}/{chan}/{tgt}/{ts}/{msg}"
1387            );
1388        }
1389    }
1390
1391    #[test]
1392    fn a_signature_minted_for_one_channel_does_not_verify_as_another() {
1393        // §D5 channel binding. One agent name can be live in several channels
1394        // at once; without `source_channel` in the signed material, a signature
1395        // legitimately produced in chan-a could be replayed as that same agent
1396        // speaking from chan-b, and the receiver would have no way to tell.
1397        let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1398        let sig =
1399            sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1400                .unwrap();
1401        assert!(!verify_channel_jekt(
1402            &public_key, "msg-1", "agentx", "chan-b", "agenty", 1_000, "hello", &sig
1403        ));
1404    }
1405
1406    #[test]
1407    fn lan_and_channel_signatures_are_not_interchangeable_in_either_direction() {
1408        // §D5 domain separation — the test the separator exists for. Same key,
1409        // same (msgid, source, target, ts, message) tuple: without
1410        // CHANNEL_DOMAIN these two signatures would be byte-identical and each
1411        // would verify as the other, collapsing two different trust claims
1412        // (TRUST=lan-verified vs TRUST=channel-verified) into one.
1413        let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1414        let lan_sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1415        let channel_sig =
1416            sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1417                .unwrap();
1418
1419        assert_ne!(lan_sig, channel_sig, "the two schemes must not produce identical bytes");
1420
1421        // A LAN signature replayed as a cross-channel one.
1422        assert!(!verify_channel_jekt(
1423            &public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &lan_sig
1424        ));
1425        // A cross-channel signature replayed as a LAN one.
1426        assert!(!verify_lan_jekt(
1427            &public_key, "msg-1", "agentx", "agenty", 1_000, "hello", &channel_sig
1428        ));
1429    }
1430
1431    #[test]
1432    fn channel_malformed_inputs_fail_rather_than_panic() {
1433        let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1434        let sig =
1435            sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1436                .unwrap();
1437        // Wrong-length private key → None, never a panic.
1438        assert!(sign_channel_jekt(&[1u8; 16], "m", "a", "c", "b", 1, "x").is_none());
1439        // Malformed public key / base64 / signature length → false, never a panic.
1440        assert!(!verify_channel_jekt(&[0u8; 16], "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &sig));
1441        assert!(!verify_channel_jekt(&public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", "!!not base64!!"));
1442        assert!(!verify_channel_jekt(&public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", ""));
1443    }
1444
1445    #[test]
1446    fn an_empty_source_channel_is_still_bound_not_ignored() {
1447        // A pre-upgrade or unknown channel id must not act as a wildcard that
1448        // matches any channel — it is just another distinct value.
1449        let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1450        let sig = sign_channel_jekt(&private_key, "msg-1", "agentx", "", "agenty", 1_000, "hello").unwrap();
1451        assert!(verify_channel_jekt(&public_key, "msg-1", "agentx", "", "agenty", 1_000, "hello", &sig));
1452        assert!(!verify_channel_jekt(&public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &sig));
1453    }
1454
1455    // ── WAN tier ──
1456
1457    #[test]
1458    fn a_correctly_signed_wan_message_verifies() {
1459        let (public_key, private_key) = generate_wan_keypair(lan_seed(11));
1460        let sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1461        assert!(verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &sig));
1462    }
1463
1464    #[test]
1465    fn a_wan_signature_does_not_verify_under_another_agents_key() {
1466        let (_, private_key) = generate_wan_keypair(lan_seed(11));
1467        let (other_public, _) = generate_wan_keypair(lan_seed(12));
1468        let sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1469        assert!(!verify_wan_jekt(&other_public, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &sig));
1470    }
1471
1472    #[test]
1473    fn tampering_with_any_wan_field_breaks_verification() {
1474        let (public_key, private_key) = generate_wan_keypair(lan_seed(11));
1475        let sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1476        assert!(!verify_wan_jekt(&public_key, "msg-2", "agentx", "narko", "stable", "agenty", 1_000, "hello", &sig));
1477        assert!(!verify_wan_jekt(&public_key, "msg-1", "someoneelse", "narko", "stable", "agenty", 1_000, "hello", &sig));
1478        assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agentz", 1_000, "hello", &sig));
1479        assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_001, "hello", &sig));
1480        assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "goodbye", &sig));
1481    }
1482
1483    #[test]
1484    fn wan_malformed_inputs_fail_rather_than_panic() {
1485        let (public_key, private_key) = generate_wan_keypair(lan_seed(11));
1486        let sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1487        assert!(sign_wan_jekt(&[1u8; 16], "m", "a", "h", "c", "b", 1, "x").is_none());
1488        assert!(!verify_wan_jekt(&[0u8; 16], "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &sig));
1489        assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", "!!not base64!!"));
1490        assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", ""));
1491    }
1492
1493    #[test]
1494    fn a_wan_signature_is_bound_to_the_sending_instance() {
1495        // One account can run
1496        // the same agent name on several instances — another machine, or
1497        // another build channel on the same machine — and each mints its own
1498        // keypair. The instance selects which key a signature is checked
1499        // against, so it must be part of what the signature covers: otherwise
1500        // a mislabelled instance would silently select a different key instead
1501        // of failing.
1502        let (public_key, private_key) = generate_wan_keypair(lan_seed(14));
1503        let sig =
1504            sign_wan_jekt(&private_key, "msg-1", "lark", "narko", "stable", "agenty", 1_000, "hello")
1505                .unwrap();
1506
1507        assert!(verify_wan_jekt(
1508            &public_key, "msg-1", "lark", "narko", "stable", "agenty", 1_000, "hello", &sig
1509        ));
1510        // Same agent, same account, different machine → not the same claim.
1511        assert!(!verify_wan_jekt(
1512            &public_key, "msg-1", "lark", "area54", "stable", "agenty", 1_000, "hello", &sig
1513        ));
1514        // Same agent, same machine, different build channel → also distinct,
1515        // because that is a different data dir, database, and keypair.
1516        assert!(!verify_wan_jekt(
1517            &public_key, "msg-1", "lark", "narko", "local-dev", "agenty", 1_000, "hello", &sig
1518        ));
1519    }
1520
1521    #[test]
1522    fn a_wan_signature_is_not_interchangeable_with_lan_or_channel() {
1523        // The whole point of WAN_DOMAIN. Even with the SAME key bytes and the
1524        // SAME (msgid, source, target, ts, message) tuple, a signature minted
1525        // for one tier must not verify as another — otherwise a signature
1526        // captured off the LAN could be replayed to claim `TRUST=wan-verified`,
1527        // which is a materially stronger claim verified against a different
1528        // key source.
1529        let seed = lan_seed(13);
1530        let (public_key, private_key) = generate_wan_keypair(seed);
1531
1532        let wan_sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1533        let lan_sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1534        let channel_sig =
1535            sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1536                .unwrap();
1537
1538        // Each verifies only under its own tier's verifier.
1539        assert!(verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &wan_sig));
1540        assert!(!verify_lan_jekt(&public_key, "msg-1", "agentx", "agenty", 1_000, "hello", &wan_sig));
1541        assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &lan_sig));
1542        assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &channel_sig
1543        ));
1544        assert_ne!(wan_sig, lan_sig);
1545    }
1546
1547    // ── W3-S ──
1548    //
1549    // The vectors below were computed by an independent implementation
1550    // (Node's `crypto`: raw Ed25519 from a PKCS#8-wrapped seed, SHA-256,
1551    // hand-rolled base32) and pasted in. The cloud's TypeScript chain check
1552    // asserts the same values, so a byte
1553    // drift on either side fails a test instead of every verification.
1554    // Instance seed [1; 32], agent seed [2; 32], successor instance [3; 32].
1555
1556    const V_INSTANCE_PUBKEY: &str = "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=";
1557    const V_INSTANCE_ID: &str = "gr2q7gf5lh6pzfdnurnkvputhm";
1558    const V_AGENT_PUBKEY: &str = "gTl3Dqh9F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5Q=";
1559    const V_KEY_FP: &str = "ajgD1fBZkCocba-8m6RykhL3yqwIY0zDrnaydSnwOCc";
1560    const V_CERT_SIG: &str =
1561        "ighbn1y/kyisIolSHwNyRtBqC2u95zQEBtGc3wxwksBUBHrDoUvXmz8yEXY20TFzOBC6IDEsG51WzTNJCrgNAA==";
1562    const V_NEW_INSTANCE_ID: &str = "wyxim75c6m5p4ywv22ywilqwee";
1563    const V_REVOKE_SIG: &str =
1564        "DOQzFIVNGItL3mkN7IPlEk5lBrmM96Dgub7jbL5JmTR5oDnCwWlfKzuYsyvZcUJJMRs56glIBhrDVHIeclqEDg==";
1565    const V_MSG_SIG: &str =
1566        "49VODE2M7pORgWgcQPl2p+M8W2ccaXJ1O8Zu1XeR4qRiJDsshawzq8eMpSkKTz+CJE0CXz2oTlXsNDobpgsRBw==";
1567    const V_ISSUED_AT: i64 = 1_790_000_000;
1568    const V_MSG_TS: i64 = 1_790_000_100;
1569
1570    fn vector_record() -> WanKeyRecord {
1571        let (agent_public_key, _) = generate_wan_keypair([2; 32]);
1572        WanKeyRecord::certify(&[1; 32], "camper", "stable", &agent_public_key, "narko", V_ISSUED_AT).unwrap()
1573    }
1574
1575    fn vector_carried() -> WanCarried<'static> {
1576        WanCarried {
1577            sig: V_MSG_SIG,
1578            msg_id: "msg-1",
1579            ts_secs: V_MSG_TS,
1580            source_agent: "camper",
1581            target_agent: "agent2",
1582            source_host: V_INSTANCE_ID,
1583            source_channel: "stable",
1584            key_fp: V_KEY_FP,
1585        }
1586    }
1587
1588    #[test]
1589    fn w3s_identifiers_and_certificate_match_the_cross_language_vectors() {
1590        let record = vector_record();
1591        assert_eq!(record.instance_pubkey, V_INSTANCE_PUBKEY);
1592        assert_eq!(record.instance_id, V_INSTANCE_ID);
1593        assert_eq!(record.instance_id.len(), WAN_INSTANCE_ID_LEN);
1594        assert_eq!(record.agent_pubkey, V_AGENT_PUBKEY);
1595        assert_eq!(record.key_fp, V_KEY_FP);
1596        assert_eq!(record.key_fp.len(), WAN_KEY_FP_LEN);
1597        assert_eq!(record.cert_sig, V_CERT_SIG);
1598        assert!(record.check_chain().is_some());
1599
1600        let (new_instance_public_key, _) = generate_wan_keypair([3; 32]);
1601        assert_eq!(wan_instance_id(&new_instance_public_key), V_NEW_INSTANCE_ID);
1602        let revocation = WanRevocation::sign(&[1; 32], V_NEW_INSTANCE_ID, 1_790_000_500).unwrap();
1603        assert_eq!(revocation.sig, V_REVOKE_SIG);
1604        assert!(revocation.verify());
1605
1606        let (_, agent_private_key) = generate_wan_keypair([2; 32]);
1607        let msg_sig = sign_wan_jekt(&agent_private_key, "msg-1", "camper", V_INSTANCE_ID, "stable", "agent2", V_MSG_TS, "hello");
1608        assert_eq!(msg_sig.as_deref(), Some(V_MSG_SIG));
1609    }
1610
1611    #[test]
1612    fn base32_matches_rfc4648_lowercased() {
1613        // RFC 4648 §10 test vectors, lowercased and unpadded.
1614        for (input, expected) in
1615            [("", ""), ("f", "my"), ("fo", "mzxq"), ("foo", "mzxw6"), ("foob", "mzxw6yq"), ("fooba", "mzxw6ytb"), ("foobar", "mzxw6ytboi")]
1616        {
1617            assert_eq!(base32_lower_nopad(input.as_bytes()), expected, "input {input:?}");
1618        }
1619    }
1620
1621    #[test]
1622    fn a_certificate_chain_breaks_on_any_substituted_field() {
1623        let good = vector_record();
1624        let (other_public_key, _) = generate_wan_keypair([9; 32]);
1625        let other_b64 = BASE64.encode(other_public_key);
1626        type Mutation<'a> = (&'a str, Box<dyn Fn(&mut WanKeyRecord) + 'a>);
1627        let mutations: Vec<Mutation> = vec![
1628            // A cloud swapping in its own agent key, with a matching fingerprint.
1629            ("agent key", Box::new(|r| {
1630                r.agent_pubkey = other_b64.clone();
1631                r.key_fp = wan_key_fingerprint(&other_public_key);
1632            })),
1633            // A cloud swapping the instance key: the id no longer matches.
1634            ("instance key", Box::new(|r| r.instance_pubkey = other_b64.clone())),
1635            // ...or relabelling the id to match its own key: the cert no longer verifies.
1636            ("instance id + key", Box::new(|r| {
1637                r.instance_pubkey = other_b64.clone();
1638                r.instance_id = wan_instance_id(&other_public_key);
1639            })),
1640            ("fingerprint", Box::new(|r| r.key_fp = wan_key_fingerprint(&other_public_key))),
1641            ("agent id", Box::new(|r| r.agent_id = "lark".into())),
1642            ("uppercased agent id", Box::new(|r| r.agent_id = "Camper".into())),
1643            ("channel", Box::new(|r| r.channel = "beta".into())),
1644            ("host hint", Box::new(|r| r.host_hint = "area54".into())),
1645            ("issued_at", Box::new(|r| r.issued_at += 1)),
1646            ("cert sig", Box::new(|r| r.cert_sig = V_REVOKE_SIG.into())),
1647            ("garbage key", Box::new(|r| r.agent_pubkey = "not base64".into())),
1648        ];
1649        for (name, mutate) in mutations {
1650            let mut record = good.clone();
1651            mutate(&mut record);
1652            assert!(record.check_chain().is_none(), "{name} substitution passed the chain");
1653        }
1654    }
1655
1656    #[test]
1657    fn certify_lowercases_the_agent_id_and_rejects_a_bad_seed() {
1658        let (agent_public_key, _) = generate_wan_keypair([2; 32]);
1659        let record = WanKeyRecord::certify(&[1; 32], "Camper", "stable", &agent_public_key, "narko", V_ISSUED_AT).unwrap();
1660        assert_eq!(record, vector_record());
1661        assert!(WanKeyRecord::certify(&[1; 31], "camper", "stable", &agent_public_key, "narko", 0).is_none());
1662    }
1663
1664    #[test]
1665    fn a_revocation_verifies_only_as_signed_by_the_old_key() {
1666        let good = WanRevocation::sign(&[1; 32], V_NEW_INSTANCE_ID, 1_790_000_500).unwrap();
1667        let mut retargeted = good.clone();
1668        retargeted.new_instance_id = V_INSTANCE_ID.into();
1669        assert!(!retargeted.verify());
1670        let mut redated = good.clone();
1671        redated.revoked_at += 1;
1672        assert!(!redated.verify());
1673        // Someone else's key revoking this instance: the id doesn't match the key.
1674        let mut foreign = WanRevocation::sign(&[9; 32], V_NEW_INSTANCE_ID, 1_790_000_500).unwrap();
1675        foreign.old_instance_id = V_INSTANCE_ID.into();
1676        assert!(!foreign.verify());
1677    }
1678
1679    #[test]
1680    fn freshness_window_boundaries() {
1681        let now = 1_790_000_000;
1682        assert!(wan_sig_is_fresh(now, now));
1683        assert!(wan_sig_is_fresh(now - WAN_SIG_MAX_AGE_SECS, now));
1684        assert!(!wan_sig_is_fresh(now - WAN_SIG_MAX_AGE_SECS - 1, now));
1685        assert!(wan_sig_is_fresh(now + WAN_SIG_MAX_FUTURE_SKEW_SECS, now));
1686        assert!(!wan_sig_is_fresh(now + WAN_SIG_MAX_FUTURE_SKEW_SECS + 1, now));
1687        assert_eq!(WAN_SIG_MAX_AGE_SECS, 2100);
1688        // Extremes saturate instead of overflowing into "fresh".
1689        assert!(!wan_sig_is_fresh(i64::MIN, now));
1690        assert!(!wan_sig_is_fresh(i64::MAX, now));
1691    }
1692
1693    #[test]
1694    fn the_envelope_binds_source_to_the_row_and_target_to_the_polled_agent() {
1695        let carried = vector_carried();
1696        assert_eq!(check_wan_envelope(&carried, "camper", "agent2", V_MSG_TS), Ok(()));
1697        // Trimmed and case-folded, as the cloud normalises (or, for github*, doesn't).
1698        assert_eq!(check_wan_envelope(&carried, " Camper ", "AGENT2", V_MSG_TS), Ok(()));
1699        // A cloud moving a message signed for agent2 into lark's queue.
1700        assert_eq!(check_wan_envelope(&carried, "camper", "lark", V_MSG_TS), Err(WanCheckFailure::EnvelopeMismatch));
1701        assert_eq!(check_wan_envelope(&carried, "clamk", "agent2", V_MSG_TS), Err(WanCheckFailure::EnvelopeMismatch));
1702        // The binding is checked before freshness: a moved stale message is still a mismatch.
1703        assert_eq!(
1704            check_wan_envelope(&carried, "camper", "lark", V_MSG_TS + 10_000),
1705            Err(WanCheckFailure::EnvelopeMismatch)
1706        );
1707        assert_eq!(check_wan_envelope(&carried, "camper", "agent2", V_MSG_TS + 2_101), Err(WanCheckFailure::Stale));
1708        assert_eq!(WanCheckFailure::Stale.verdict(), None);
1709        assert_eq!(WanCheckFailure::EnvelopeMismatch.verdict(), Some(false));
1710    }
1711
1712    #[test]
1713    fn a_carried_message_verifies_against_its_certified_record() {
1714        assert_eq!(verify_wan_against_record(&vector_carried(), &vector_record(), "hello"), Ok(()));
1715    }
1716
1717    #[test]
1718    fn each_carried_field_altered_in_turn_fails() {
1719        let record = vector_record();
1720        let cases: Vec<(&str, WanCarried<'static>, WanCheckFailure)> = vec![
1721            ("msg_id", WanCarried { msg_id: "msg-2", ..vector_carried() }, WanCheckFailure::BadSignature),
1722            ("ts", WanCarried { ts_secs: V_MSG_TS + 1, ..vector_carried() }, WanCheckFailure::BadSignature),
1723            ("target", WanCarried { target_agent: "lark", ..vector_carried() }, WanCheckFailure::BadSignature),
1724            ("sig", WanCarried { sig: V_CERT_SIG, ..vector_carried() }, WanCheckFailure::BadSignature),
1725            ("source", WanCarried { source_agent: "lark", ..vector_carried() }, WanCheckFailure::RecordMismatch),
1726            ("host", WanCarried { source_host: V_NEW_INSTANCE_ID, ..vector_carried() }, WanCheckFailure::RecordMismatch),
1727            ("channel", WanCarried { source_channel: "beta", ..vector_carried() }, WanCheckFailure::RecordMismatch),
1728            ("key_fp", WanCarried { key_fp: "x", ..vector_carried() }, WanCheckFailure::RecordMismatch),
1729        ];
1730        for (name, carried, expected) in cases {
1731            assert_eq!(verify_wan_against_record(&carried, &record, "hello"), Err(expected), "altered {name}");
1732        }
1733        assert_eq!(
1734            verify_wan_against_record(&vector_carried(), &record, "hello!"),
1735            Err(WanCheckFailure::BadSignature)
1736        );
1737    }
1738
1739    #[test]
1740    fn the_record_match_is_ascii_case_insensitive_for_instance_channel_and_agent() {
1741        // A record certified for channel `Stable` and a message signed under
1742        // `stable`: both halves are individually valid, so the only question is
1743        // the §2.3 match, which is case-insensitive.
1744        let (agent_public_key, agent_private_key) = generate_wan_keypair([2; 32]);
1745        let record = WanKeyRecord::certify(&[1; 32], "camper", "Stable", &agent_public_key, "narko", V_ISSUED_AT).unwrap();
1746        let sig = sign_wan_jekt(&agent_private_key, "msg-1", "Camper", V_INSTANCE_ID, "stable", "agent2", V_MSG_TS, "hello").unwrap();
1747        let carried = WanCarried {
1748            sig: &sig,
1749            source_agent: "Camper",
1750            source_channel: "stable",
1751            ..vector_carried()
1752        };
1753        assert_eq!(verify_wan_against_record(&carried, &record, "hello"), Ok(()));
1754    }
1755
1756    #[test]
1757    fn a_tampered_record_is_cert_invalid_before_anything_else() {
1758        let mut record = vector_record();
1759        record.host_hint = "evil".into();
1760        assert_eq!(
1761            verify_wan_against_record(&vector_carried(), &record, "hello"),
1762            Err(WanCheckFailure::CertInvalid)
1763        );
1764    }
1765
1766    #[test]
1767    fn host_hints_and_display_labels() {
1768        assert!(is_valid_wan_host_hint("narko"));
1769        assert!(is_valid_wan_host_hint("host-1.lan"));
1770        for bad in ["", "Narko", "narko~x", "has space", "ümlaut", &"a".repeat(49)] {
1771            assert!(!is_valid_wan_host_hint(bad), "{bad:?}");
1772        }
1773        assert_eq!(wan_instance_display_label("narko", V_INSTANCE_ID), "narko~gr2q7gf5");
1774        // A sender-chosen label that tries to smuggle its own suffix renders as `?`.
1775        assert_eq!(wan_instance_display_label("narko~abcdefgh", V_INSTANCE_ID), "?~gr2q7gf5");
1776    }
1777}
1778
1779/// Cross-version wire compatibility for jekt signatures. The fleet never
1780/// upgrades all at once, so a signature made by one ed25519-dalek major must
1781/// verify on another. Ed25519 signing is deterministic (RFC 8032), so the
1782/// strongest check is byte equality: these goldens were produced by
1783/// ed25519-dalek 2.2.0 (2026-10-01) and must stay identical across upgrades.
1784/// If one changes, older peers would reject the new signatures — that is a
1785/// fleet-breaking change, not a test to update.
1786#[cfg(test)]
1787mod cross_version_goldens {
1788    use super::*;
1789
1790    const SEED: [u8; 32] = [42u8; 32];
1791    const TS: i64 = 1_790_000_000;
1792    const PK: &str = "GX9rI+FshTLGq8g4+s1ep4m+DHaykgM0A5v6iz02jWE=";
1793    const LAN: &str = "EXTYmZi7R4FlXQU5olRQng9i3JvoyPaNk+5hsgz/pwLVYBoIP5/fKSlN8YCAViv/j18Gr9fCp0W9fS2NYrFrAQ==";
1794    const CHAN: &str = "lSTM+FmFR/QsqzmTiVvO8KLa5jmFrnQhpuQ7i543/ezCoeOMaAll+N2NEs2wfL727/vwzOYFDt3Exkk6h7yKCw==";
1795    const WAN: &str = "IeNtT8Ivh6k84poizAS0a1oBzBQsDRteO72vlsviWieBDHu/ET8XZNwjLNlZGFyjMWA4GnbZbOn9S02pvbYjCw==";
1796
1797    fn hex(s: &str) -> Vec<u8> {
1798        (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
1799    }
1800
1801    #[test]
1802    fn rfc8032_test_1_matches() {
1803        let seed: [u8; 32] = hex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
1804            .try_into()
1805            .unwrap();
1806        let key = ed25519_dalek::SigningKey::from_bytes(&seed);
1807        assert_eq!(
1808            key.verifying_key().to_bytes().to_vec(),
1809            hex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")
1810        );
1811        let sig = ed25519_dalek::Signer::sign(&key, b"");
1812        assert_eq!(
1813            sig.to_bytes().to_vec(),
1814            hex("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b")
1815        );
1816    }
1817
1818    #[test]
1819    fn public_key_matches_golden() {
1820        assert_eq!(BASE64.encode(generate_lan_keypair(SEED).0), PK);
1821    }
1822
1823    #[test]
1824    fn lan_signature_is_byte_identical_and_verifies() {
1825        let sig = sign_lan_jekt(&SEED, "msg-golden", "agentx", "manoz", TS, "hello fleet").unwrap();
1826        assert_eq!(sig, LAN);
1827        let pk = BASE64.decode(PK).unwrap();
1828        assert!(verify_lan_jekt(&pk, "msg-golden", "agentx", "manoz", TS, "hello fleet", LAN));
1829        assert!(!verify_lan_jekt(&pk, "msg-golden", "agentx", "manoz", TS, "hello fleet!", LAN));
1830    }
1831
1832    #[test]
1833    fn channel_signature_is_byte_identical_and_verifies() {
1834        let sig = sign_channel_jekt(&SEED, "msg-golden", "agentx", "local-main", "manoz", TS, "hello fleet").unwrap();
1835        assert_eq!(sig, CHAN);
1836        let pk = BASE64.decode(PK).unwrap();
1837        assert!(verify_channel_jekt(&pk, "msg-golden", "agentx", "local-main", "manoz", TS, "hello fleet", CHAN));
1838    }
1839
1840    #[test]
1841    fn wan_signature_is_byte_identical_and_verifies() {
1842        let sig =
1843            sign_wan_jekt(&SEED, "msg-golden", "agentx", "narko", "stable", "manoz", TS, "hello fleet").unwrap();
1844        assert_eq!(sig, WAN);
1845        let pk = BASE64.decode(PK).unwrap();
1846        assert!(verify_wan_jekt(&pk, "msg-golden", "agentx", "narko", "stable", "manoz", TS, "hello fleet", WAN));
1847    }
1848}