Skip to main content

monetize_embed/
fleet.rs

1//! **The pull channel's signed forms** (5.4, G1-pull): monetize never calls INTO an
2//! estate; every estate calls monetize (`Fleet.Report`, `Fleet.Orders`, `Fleet.Ask`) and
3//! monetize answers with [`SignedOrder`]s the box verifies under `monetize_pubkey`.
4//!
5//! Two forms, each a fixed binary layout (not JSON) so the estate's verifier
6//! (`gunnar_repair::order`) and this crate agree byte for byte with no canonical-JSON
7//! rule to share:
8//!
9//! * [`order_message`]: what monetize signs for ONE order to ONE box (`role`).
10//! * [`fleet_call_message`]: what a box signs with its own Ed25519 appliance key for each
11//!   call it makes to monetize.
12//!
13//! An order is only "please run verb V with these arguments": the estate runs it through the
14//! SAME function its local admin button and its boot check call, and says how it went in the
15//! next [`Report`] ([`OrderAck`]).
16
17use ed25519_dalek::{Signature, VerifyingKey};
18use serde::{Deserialize, Serialize};
19
20/// The domain tag every order signature starts with.
21pub const ORDER_DOMAIN: &[u8] = b"nordisk-order-v1\0";
22/// The domain tag every box's call signature starts with.
23pub const CALL_DOMAIN: &[u8] = b"nordisk-fleet-call-v1\0";
24/// How far a box's call time may be from monetize's clock.
25pub const CALL_FRESH_MS: u64 = 5 * 60 * 1000;
26/// How far ahead of the box's clock an order may be dated (the shared pump's and Gunnar's five
27/// minutes).
28pub const FUTURE_SKEW_MS: u64 = 5 * 60 * 1000;
29/// How long `Fleet.Orders` is held open at most before it answers empty.
30pub const LONG_POLL_MAX_MS: u64 = 25_000;
31/// **How long a front waits on the shop's `SoldEstate.Upgrade`**: the shop's whole keep boot,
32/// 2 h, the ONE constant the shop holds its call by (`monetize_product::KEEP_BOOT_WITHIN`; lane
33/// G9, A7 row 9, Gunnar b74ca801). A front that gave up first would count a slow keep boot as
34/// lost and ask for a Recharge while the shop still works.
35pub use monetize_product::KEEP_BOOT_WITHIN;
36
37/// The order kinds. The body is JSON; unknown fields are ignored. The relay products' guardian
38/// kinds ([`kind::GUARDIAN`], gunnar-repair's verbs) are defined here; the core's kinds are
39/// `monetize_product::pull`'s one list (A28 M25), re-exported here under their old names.
40pub mod kind {
41    /// `{main, twin, accept_lost, epoch, case?}`: the twin (or a fresh main) pumps the twin's
42    /// log into the main. The same verb the local "Recharge" button and the boot check run.
43    pub const RECHARGE_FROM_TWIN: &str = "recharge_from_twin";
44    /// `{primary, epoch, case?}`: the old primary may not serve past `epoch`.
45    pub const FENCE_PRIMARY: &str = "fence_primary";
46    /// `{twin, principal, case?}`: the primary takes the box at `twin` as its twin.
47    pub const ADOPT_TWIN: &str = "adopt_twin";
48    /// `{case?}`: the box reviews its twin and reports the `TwinReview`.
49    pub const REVIEW_TWIN: &str = "review_twin";
50    /// `{snapshot, case?}`: the money put back on a recharged main. `snapshot` is a
51    /// [`crate::Snapshot`] signed by monetize's key — what monetize's books hold for the
52    /// product. The box verifies it and every fact in it (`signing::verify_snapshot`), applies
53    /// each fact only if newer than the one it holds (a same fact is a no-op), deletes
54    /// nothing, and runs it only while it serves as the primary.
55    pub const RESTATE_ENTITLEMENTS: &str = "restate_entitlements";
56    /// [`super::GrowGoAhead`]: **a sold estate's paid data step** (G16-d, G1-pull row 17),
57    /// queued to the estate's main AND its twin once the step's money is captured. The box
58    /// verifies `go_ahead` under monetize's key (`signing::verify_estate_growth`: its estate,
59    /// good until `until_unix_ms`), then grows its own data disks to `data_gib` a disk: drain,
60    /// unmount, `Fleet.Ask{kind: ask::GROW_SET}` (monetize resizes them at the provider), look
61    /// again, remount. The twin first — its disk holds main's whole set. A newer go-ahead
62    /// supersedes an unacked older one; a growth already done is acked ok, nothing moved.
63    pub use monetize_product::pull::order::GROW_GO_AHEAD;
64    /// [`super::SignInAgain`]: **a sold estate's RESEND sign-in link** (S10, owner 2026-10-11
65    /// 05:53Z), queued to the estate's main when its buyer asks (`EstateOrder.SendSignInAgain`).
66    /// The order names whose passkey, which address and which console host — NO secret: the
67    /// main mints the fresh invite itself (only while the account holds no passkey) and mails
68    /// `https://{host}/login?invite=…&user=…` itself. The shop never holds a resend's login
69    /// secret (the FIRST link stays shop-minted at install, OM-21 decision A). A newer ask
70    /// supersedes an unacked older one.
71    pub use monetize_product::pull::order::SIGN_IN_AGAIN;
72
73    /// **The relay products' guardian kinds** (the twin's repair verbs): not core kinds; a
74    /// product whose guardian runs no twin never gets one.
75    pub const GUARDIAN: [&str; 5] = [RECHARGE_FROM_TWIN, FENCE_PRIMARY, ADOPT_TWIN, REVIEW_TWIN, RESTATE_ENTITLEMENTS];
76}
77
78/// The `Fleet.Ask` kinds that are not a remedy: answered at once, no case opened.
79pub mod ask {
80    /// [`super::GrowSetAsk`]: **size the calling box's own data disks** at monetize's provider
81    /// (G16-d, G1-pull row 18) — no more than is paid for its role, never smaller. The main
82    /// sizes main's disks, the twin (its own key, `role = twin`) the twin's.
83    pub use monetize_product::pull::ask::GROW_SET;
84    /// [`super::FrontAsk`] `{role, done}`: **the shop boots ONE standing box once from the
85    /// release's keep medium** (was `SoldEstate.Upgrade`; plan §8, A24 §5.1 row 27). Asked by
86    /// the estate's front; the answer is `EstateUpgradeResponse` in `FleetAskAnswer.answer`.
87    pub const UPGRADE: &str = "upgrade";
88    /// [`super::FrontAsk`] `{role, seed, done}`: **the shop lays ONE lost box again** (was
89    /// `SoldEstate.Recharge`, row 29); the answer is `EstateRechargeResponse`. Not the guardian's
90    /// `recharge_from_twin` remedy: this one is the cloud half, asked by the front.
91    pub const RECHARGE: &str = "recharge";
92    /// [`super::FrontAsk`] `{done}`: **the estate takes its own installer material** (was
93    /// `SoldEstate.TakeMaterial`, row 28); the answer is `EstateTakeMaterialResponse`.
94    pub const MATERIAL: &str = "material";
95    /// [`super::FrontAsk`] `{}`: **the front takes the open update** (was
96    /// `SoldEstate.UpdateTake`, row 25); the answer is `EstateUpdateTakeResponse`.
97    pub const UPDATE_TAKE: &str = "update_take";
98    /// [`super::FrontAsk`] `{role, carries}`: **the front reports one box's update step** (was
99    /// `SoldEstate.UpdateReport`, row 26); the answer is `EstateUpdateReportResponse`.
100    pub const UPDATE_REPORT: &str = "update_report";
101    /// **The front's asks** — the five `SoldEstate` calls folded into `Fleet.Ask` (plan §8,
102    /// decided 2026-10-24). Answered at once, no case opened; asked only by the estate's front.
103    pub const FRONT: [&str; 5] = [UPGRADE, RECHARGE, MATERIAL, UPDATE_TAKE, UPDATE_REPORT];
104
105    /// Pure: **is `kind` one of the front's asks** ([`FRONT`])?
106    pub fn is_front(kind: &str) -> bool {
107        FRONT.contains(&kind)
108    }
109}
110
111/// The calls a box makes ([`FleetCall::call`]).
112pub mod call {
113    pub const REPORT: &str = "report";
114    pub const ORDERS: &str = "orders";
115    pub const ASK: &str = "ask";
116}
117
118/// **One order to one box**, signed by the product's monetize key.
119#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
120pub struct SignedOrder {
121    /// Per instance, strictly rising; the box refuses any `seq` at or below the last it ran.
122    pub seq: u64,
123    pub product: String,
124    pub instance: String,
125    /// The box the order is for: `main` | `twin` | `front`. Signed, so an order for the twin
126    /// is never run by the main.
127    pub role: String,
128    pub kind: String,
129    /// JSON text.
130    pub body: String,
131    pub issued_unix_ms: u64,
132    pub not_after_unix_ms: u64,
133    /// 64-byte Ed25519 over [`order_message`].
134    #[serde(default)]
135    pub signature: Vec<u8>,
136}
137
138/// Why a box refuses an order.
139#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
140pub enum OrderRefusal {
141    #[error("the order is not signed by monetize")]
142    Signature,
143    #[error("order {seq} is not newer than {last}")]
144    Replayed { seq: u64, last: u64 },
145    #[error("order {0} has expired")]
146    Expired(u64),
147    #[error("order {0} is for {1}, not this box")]
148    NotForMe(u64, String),
149    #[error("order {0} is dated more than five minutes ahead of this box's clock")]
150    Ahead(u64),
151}
152
153fn field(out: &mut Vec<u8>, bytes: &[u8]) {
154    out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
155    out.extend_from_slice(bytes);
156}
157
158/// The bytes an order's signature covers: [`ORDER_DOMAIN`] ‖ seq ‖ issued ‖ not_after (u64 BE)
159/// ‖ then product, instance, role, kind, body, each as u32 BE length ‖ bytes.
160pub fn order_message(o: &SignedOrder) -> Vec<u8> {
161    let mut out = ORDER_DOMAIN.to_vec();
162    out.extend_from_slice(&o.seq.to_be_bytes());
163    out.extend_from_slice(&o.issued_unix_ms.to_be_bytes());
164    out.extend_from_slice(&o.not_after_unix_ms.to_be_bytes());
165    for f in [&o.product, &o.instance, &o.role, &o.kind, &o.body] {
166        field(&mut out, f.as_bytes());
167    }
168    out
169}
170
171/// **The box's check**, as strict as the shared pump's (`monetize_estate_side::pump::check`):
172/// signed under `key` with STRICT Ed25519 (no small-order key or `R`), checked first, so a
173/// forged order is always [`OrderRefusal::Signature`] and never moves the box's sequence; then
174/// for `(product, instance, role)`, newer than `last_seq`, inside its window at `now_unix_ms`
175/// (`not_after` 0 = none), and dated no more than [`FUTURE_SKEW_MS`] ahead of the box's clock.
176pub fn verify_order(o: &SignedOrder, key: &VerifyingKey, me: (&str, &str, &str), now_unix_ms: u64, last_seq: u64) -> Result<(), OrderRefusal> {
177    let (product, instance, role) = me;
178    if !check(key, &order_message(o), &o.signature) {
179        return Err(OrderRefusal::Signature);
180    }
181    if o.product != product || o.instance != instance || o.role != role {
182        return Err(OrderRefusal::NotForMe(o.seq, o.role.clone()));
183    }
184    if o.seq <= last_seq {
185        return Err(OrderRefusal::Replayed { seq: o.seq, last: last_seq });
186    }
187    if o.not_after_unix_ms != 0 && now_unix_ms > o.not_after_unix_ms {
188        return Err(OrderRefusal::Expired(o.seq));
189    }
190    if o.issued_unix_ms > now_unix_ms.saturating_add(FUTURE_SKEW_MS) {
191        return Err(OrderRefusal::Ahead(o.seq));
192    }
193    Ok(())
194}
195
196/// **One call from a box to monetize**, signed by the box's own appliance key.
197#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
198pub struct FleetCall {
199    pub product: String,
200    pub instance: String,
201    pub role: String,
202    /// [`call::REPORT`] | [`call::ORDERS`] | [`call::ASK`].
203    pub call: String,
204    /// JSON text: a [`Report`], an [`OrdersAfter`] or an [`Ask`].
205    pub body: String,
206    pub at_unix_ms: u64,
207}
208
209/// Why monetize refuses a box's call.
210#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
211pub enum CallRefusal {
212    #[error("the call is not signed by the box's key")]
213    Signature,
214    #[error("the call's time is too far from monetize's clock")]
215    Stale,
216}
217
218/// The bytes a box's call signature covers: [`CALL_DOMAIN`] ‖ at (u64 BE) ‖ then product,
219/// instance, role, call, body, each as u32 BE length ‖ bytes.
220pub fn fleet_call_message(c: &FleetCall) -> Vec<u8> {
221    let mut out = CALL_DOMAIN.to_vec();
222    out.extend_from_slice(&c.at_unix_ms.to_be_bytes());
223    for f in [&c.product, &c.instance, &c.role, &c.call, &c.body] {
224        field(&mut out, f.as_bytes());
225    }
226    out
227}
228
229/// Verify a box's call under its appliance `key`, fresh at `now_unix_ms`.
230pub fn verify_fleet_call(c: &FleetCall, signature: &[u8], key: &VerifyingKey, now_unix_ms: u64) -> Result<(), CallRefusal> {
231    if !check(key, &fleet_call_message(c), signature) {
232        return Err(CallRefusal::Signature);
233    }
234    if c.at_unix_ms.abs_diff(now_unix_ms) > CALL_FRESH_MS {
235        return Err(CallRefusal::Stale);
236    }
237    Ok(())
238}
239
240/// STRICT Ed25519 (`verify_strict`): a small-order key or `R` never verifies (T16, A28 M11).
241fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> bool {
242    Signature::from_slice(sig).map(|s| key.verify_strict(msg, &s).is_ok()).unwrap_or(false)
243}
244
245/// What a box says ran (or did not) — sent back in its next [`Report`].
246#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
247#[serde(default)]
248pub struct OrderAck {
249    pub seq: u64,
250    pub ok: bool,
251    pub detail: String,
252    /// gunnar_repair's `Receipt` / `TwinReview`, JSON text; empty when none.
253    pub receipt_json: String,
254}
255
256/// **The box's heartbeat** (`Fleet.Report` body). Its arrival time is the liveness signal.
257#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
258#[serde(default)]
259pub struct Report {
260    pub build: String,
261    /// The box's own health words, flat: `serving` (`yes` | `no` | `fenced`), `data` (`ok` |
262    /// `lost` | `diverged`), `serves_at` (`host:port`), `twin` (`paired` | `silent` | …),
263    /// `epoch`. Unknown words are kept and shown.
264    pub health: std::collections::BTreeMap<String, String>,
265    pub acked: Vec<OrderAck>,
266    /// The product's twin review (gunnar: `gunnar_repair::TwinReview`), JSON text, when it
267    /// made one.
268    pub twin_review: Option<String>,
269    /// **The product's own state** (G1 pull: tenants and owners, usage, servable, the paid
270    /// door, the room a raise would find), JSON text of `monetize_product::pull::ProductReport`.
271    /// Sent by the box that serves tenants (the main); what monetize used to dial it for.
272    pub product: Option<String>,
273    pub measured_unix_ms: u64,
274}
275
276/// `Fleet.Orders` body: orders after `after`, held up to `wait_ms` (≤ [`LONG_POLL_MAX_MS`]).
277#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
278#[serde(default)]
279pub struct OrdersAfter {
280    pub after: u64,
281    pub wait_ms: u64,
282}
283
284/// Body of a [`kind::GROW_GO_AHEAD`] order.
285#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
286#[serde(default)]
287pub struct GrowGoAhead {
288    /// The [`crate::signing::EstateGrowth`] JSON, signed by monetize's key.
289    pub go_ahead: String,
290    /// GB a data disk THIS box's role is paid to (main: the step; twin: main's whole set).
291    pub data_gib: u64,
292}
293
294/// Body of a [`kind::SIGN_IN_AGAIN`] order. Never a secret.
295#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
296#[serde(default)]
297pub struct SignInAgain {
298    /// The account whose first passkey the invite enrols (the order's account).
299    pub account: String,
300    /// The order's address: where the estate mails the link.
301    pub to: String,
302    /// The console's relying-party host the link points at (`<company>.<apex>`).
303    pub host: String,
304    /// How long the invite lives.
305    pub ttl_secs: u64,
306}
307
308/// `Fleet.Ask` body of [`ask::GROW_SET`]: the box's own data disks to `gib` a disk. No
309/// `members` = every laid data disk of the calling box; `gib` 0 = what its role is paid to.
310#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
311#[serde(default)]
312pub struct GrowSetAsk {
313    pub members: Vec<String>,
314    pub gib: u64,
315}
316
317/// **The body of a front's ask** ([`ask::FRONT`]): the fields the old `SoldEstate` request
318/// carried, less the company and the signature — the `FleetSigned` call names the estate
319/// (`instance`) and the box (`role = front`) and is signed by the front's key. `role` is the
320/// box the ask is about (`main` | `twin`; for `update_report`, the box whose step it is);
321/// `carries` is the update report's `<outcome>\n<words>`.
322#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
323#[serde(default)]
324pub struct FrontAsk {
325    #[serde(skip_serializing_if = "String::is_empty")]
326    pub role: String,
327    #[serde(skip_serializing_if = "std::ops::Not::not")]
328    pub done: bool,
329    #[serde(skip_serializing_if = "String::is_empty")]
330    pub seed: String,
331    #[serde(skip_serializing_if = "String::is_empty")]
332    pub carries: String,
333}
334
335/// `Fleet.Ask` body: a request from the estate (`new_twin`, `new_server`, `recharge`, …).
336#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
337#[serde(default)]
338pub struct Ask {
339    pub kind: String,
340    pub body: serde_json::Value,
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use ed25519_dalek::{Signer as _, SigningKey};
347
348    /// A28 M25: the core kinds are defined once, in `monetize_product::pull`; the kinds here
349    /// are that list's, and the rest of [`kind`] are the relay products' guardian kinds.
350    #[test]
351    fn the_core_kinds_here_are_monetize_products_one_list() {
352        use monetize_product::pull;
353        assert_eq!(kind::GROW_GO_AHEAD, pull::order::GROW_GO_AHEAD);
354        assert_eq!(kind::SIGN_IN_AGAIN, pull::order::SIGN_IN_AGAIN);
355        assert_eq!(ask::GROW_SET, pull::ask::GROW_SET);
356        for k in kind::GUARDIAN {
357            assert!(!pull::is_core(k), "{k} is a guardian kind, not a core one");
358        }
359        assert_eq!(kind::GUARDIAN, [kind::RECHARGE_FROM_TWIN, kind::FENCE_PRIMARY, kind::ADOPT_TWIN, kind::REVIEW_TWIN, kind::RESTATE_ENTITLEMENTS]);
360    }
361
362    /// ★ Plan §8 (the SoldEstate → Fleet fold): the front's five asks are named once, none of
363    /// them is a guardian remedy or `grow_set`, and their body round-trips through the `Ask`
364    /// JSON with only the fields it carries.
365    #[test]
366    fn the_fronts_asks_are_the_five_soldestate_calls_and_their_body_round_trips() {
367        assert_eq!(ask::FRONT, ["upgrade", "recharge", "material", "update_take", "update_report"]);
368        for k in ask::FRONT {
369            assert!(ask::is_front(k));
370        }
371        for k in [ask::GROW_SET, kind::RECHARGE_FROM_TWIN, kind::REVIEW_TWIN, "new_twin", "new_server", ""] {
372            assert!(!ask::is_front(k), "{k}");
373        }
374        // The ONE box's own door, not a product's kind: none is one of the core's 8 (A28 M25).
375        for k in ask::FRONT {
376            assert!(!monetize_product::pull::is_core(k), "{k}");
377        }
378        let a = FrontAsk { role: "main".into(), seed: "SEED".into(), ..Default::default() };
379        let text = serde_json::to_string(&Ask { kind: ask::RECHARGE.into(), body: serde_json::to_value(&a).unwrap() }).unwrap();
380        assert_eq!(text, r#"{"kind":"recharge","body":{"role":"main","seed":"SEED"}}"#);
381        let back: Ask = serde_json::from_str(&text).unwrap();
382        assert_eq!(serde_json::from_value::<FrontAsk>(back.body).unwrap(), a);
383        assert_eq!(serde_json::from_value::<FrontAsk>(serde_json::json!({})).unwrap(), FrontAsk::default(), "an empty body is the update take's");
384    }
385
386    /// ★ Lane G9, A7 row 9 (Gunnar b74ca801): a front waits on the shop's whole keep boot, 2 h,
387    /// the one constant the shop's cloud crate holds its Upgrade call by.
388    #[test]
389    fn a_front_waits_on_the_shops_whole_keep_boot() {
390        assert_eq!(KEEP_BOOT_WITHIN, std::time::Duration::from_secs(2 * 3600));
391        assert_eq!(KEEP_BOOT_WITHIN, monetize_product::KEEP_BOOT_WITHIN);
392    }
393
394    const GOLDEN_MESSAGE_HEX: &str = "6e6f726469736b2d6f726465722d7631000000000000000003000001a3185c5000000001a3186a0ba00000000667756e6e61720000000461636d65000000047477696e0000001272656368617267655f66726f6d5f7477696e000000517b226163636570745f6c6f7374223a66616c73652c2265706f6368223a352c226d61696e223a2231302e362e302e31303a3530303531222c227477696e223a2231302e362e302e31323a3530303531227d";
395    const GOLDEN_SIGNATURE_HEX: &str = "d4a3233158d9a3fbfa42f9aa80fd2ca420666ec2fe9f7fbf1a61abab533c09f6dab60c67367bc771a1d819d1e6bed28c59d6d56187a4748ba9b4b9988d13d50a";
396
397    fn key() -> SigningKey {
398        SigningKey::from_bytes(&[7u8; 32])
399    }
400
401    fn order() -> SignedOrder {
402        SignedOrder {
403            seq: 3,
404            product: "gunnar".into(),
405            instance: "acme".into(),
406            role: "twin".into(),
407            kind: kind::RECHARGE_FROM_TWIN.into(),
408            body: r#"{"accept_lost":false,"epoch":5,"main":"10.6.0.10:50051","twin":"10.6.0.12:50051"}"#.into(),
409            issued_unix_ms: 1_800_000_000_000,
410            not_after_unix_ms: 1_800_000_900_000,
411            signature: Vec::new(),
412        }
413    }
414
415    fn signed() -> SignedOrder {
416        let mut o = order();
417        o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
418        o
419    }
420
421    /// The golden vector gunnar-repair pastes into its own test: same order, same bytes.
422    #[test]
423    fn the_order_message_is_the_agreed_binary_layout() {
424        let m = order_message(&order());
425        assert!(m.starts_with(ORDER_DOMAIN));
426        let mut want = ORDER_DOMAIN.to_vec();
427        want.extend_from_slice(&3u64.to_be_bytes());
428        want.extend_from_slice(&1_800_000_000_000u64.to_be_bytes());
429        want.extend_from_slice(&1_800_000_900_000u64.to_be_bytes());
430        for f in ["gunnar", "acme", "twin", kind::RECHARGE_FROM_TWIN, &order().body] {
431            want.extend_from_slice(&(f.len() as u32).to_be_bytes());
432            want.extend_from_slice(f.as_bytes());
433        }
434        assert_eq!(m, want);
435        let sig = key().sign(&m).to_bytes();
436        let hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();
437        assert_eq!(hex, GOLDEN_SIGNATURE_HEX, "the golden vector moved: tell gunnar_repair::order");
438        let mhex: String = m.iter().map(|b| format!("{b:02x}")).collect();
439        assert_eq!(mhex, GOLDEN_MESSAGE_HEX);
440    }
441
442    #[test]
443    fn a_box_runs_only_a_fresh_order_signed_for_it() {
444        let pk = key().verifying_key();
445        let me = ("gunnar", "acme", "twin");
446        let now = 1_800_000_100_000;
447        assert_eq!(verify_order(&signed(), &pk, me, now, 2), Ok(()));
448        assert_eq!(verify_order(&signed(), &pk, me, now, 3), Err(OrderRefusal::Replayed { seq: 3, last: 3 }));
449        assert_eq!(verify_order(&signed(), &pk, me, 1_800_000_900_001, 0), Err(OrderRefusal::Expired(3)));
450        assert_eq!(verify_order(&signed(), &pk, ("gunnar", "acme", "main"), now, 0), Err(OrderRefusal::NotForMe(3, "twin".into())));
451        let mut forged = signed();
452        forged.role = "main".into();
453        assert_eq!(verify_order(&forged, &pk, ("gunnar", "acme", "main"), now, 0), Err(OrderRefusal::Signature));
454        let other = SigningKey::from_bytes(&[9u8; 32]).verifying_key();
455        assert_eq!(verify_order(&signed(), &other, me, now, 0), Err(OrderRefusal::Signature));
456    }
457
458    /// ★ **T16 (A28 M11): this check is as strict as the shared pump's** (`monetize_estate_side::
459    /// pump::check`, P4c). Under a small-order key (the identity point) the all-zero signature
460    /// verifies ANY message with plain `verify`; a strict check refuses it, and a real key's
461    /// signature still passes.
462    #[test]
463    fn a_weak_key_with_the_all_zero_signature_signs_no_order() {
464        let (weak, zero) = weak_key_and_zero_signature();
465        let mut o = order();
466        o.signature = zero;
467        let now = 1_800_000_100_000;
468        assert_eq!(verify_order(&o, &weak, ("gunnar", "acme", "twin"), now, 0), Err(OrderRefusal::Signature), "a weak key verifies nothing");
469        assert_eq!(verify_order(&signed(), &key().verifying_key(), ("gunnar", "acme", "twin"), now, 0), Ok(()));
470    }
471
472    /// ★ The same weak key never vouches for a box's call either.
473    #[test]
474    fn a_weak_box_key_with_the_all_zero_signature_signs_no_call() {
475        let (weak, zero) = weak_key_and_zero_signature();
476        let c = FleetCall { product: "gunnar".into(), instance: "acme".into(), role: "main".into(), call: call::REPORT.into(), body: "{}".into(), at_unix_ms: 1_800_000_000_000 };
477        assert_eq!(verify_fleet_call(&c, &zero, &weak, 1_800_000_000_000), Err(CallRefusal::Signature));
478    }
479
480    /// The identity point as a key, and the signature (R = identity, s = 0) that plain `verify`
481    /// accepts for every message under it.
482    fn weak_key_and_zero_signature() -> (VerifyingKey, Vec<u8>) {
483        let mut identity = [0u8; 32];
484        identity[0] = 1;
485        let weak = VerifyingKey::from_bytes(&identity).expect("the identity point decompresses");
486        let mut zero = vec![0u8; 64];
487        zero[0] = 1;
488        (weak, zero)
489    }
490
491    /// ★ Every signed field is covered: one tampered field, a flipped signature bit, a short or
492    /// missing signature, or another key is refused as not monetize's.
493    #[test]
494    fn a_tampered_order_is_not_monetizes() {
495        let pk = key().verifying_key();
496        let me = ("gunnar", "acme", "twin");
497        let now = 1_800_000_100_000;
498        type Tamper = Box<dyn Fn(&mut SignedOrder)>;
499        let tampered: Vec<(&str, Tamper)> = vec![
500            ("seq", Box::new(|o| o.seq += 1)),
501            ("kind", Box::new(|o| o.kind = kind::FENCE_PRIMARY.into())),
502            ("body", Box::new(|o| o.body = "{}".into())),
503            ("issued", Box::new(|o| o.issued_unix_ms -= 1)),
504            ("not_after", Box::new(|o| o.not_after_unix_ms += 1)),
505            ("bit", Box::new(|o| o.signature[5] ^= 1)),
506            ("short", Box::new(|o| o.signature.truncate(63))),
507            ("none", Box::new(|o| o.signature.clear())),
508        ];
509        for (what, f) in tampered {
510            let mut o = signed();
511            f(&mut o);
512            assert_eq!(verify_order(&o, &pk, me, now, 0), Err(OrderRefusal::Signature), "{what}");
513        }
514    }
515
516    /// ★ The signature is checked FIRST: a forged order addressed to another box is forged (it
517    /// never moves the box's sequence), not "not for me".
518    #[test]
519    fn a_forged_order_for_another_box_is_forged_not_misaddressed() {
520        let mut o = signed();
521        o.role = "main".into();
522        assert_eq!(verify_order(&o, &key().verifying_key(), ("gunnar", "acme", "twin"), 1_800_000_100_000, 0), Err(OrderRefusal::Signature));
523    }
524
525    /// ★ `not_after` 0 is no window (as the shared pump reads it), not "expired at the epoch".
526    #[test]
527    fn an_order_with_no_window_does_not_expire() {
528        let mut o = order();
529        o.not_after_unix_ms = 0;
530        o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
531        assert_eq!(verify_order(&o, &key().verifying_key(), ("gunnar", "acme", "twin"), 1_900_000_000_000, 0), Ok(()));
532    }
533
534    /// ★ An order dated more than [`FUTURE_SKEW_MS`] ahead of the box's clock is refused by
535    /// name; five minutes of skew is fine.
536    #[test]
537    fn an_order_dated_ahead_of_the_box_clock_is_refused() {
538        let pk = key().verifying_key();
539        let me = ("gunnar", "acme", "twin");
540        let issued = order().issued_unix_ms;
541        assert_eq!(verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS, 0), Ok(()), "five minutes of skew");
542        assert_eq!(verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS - 1, 0), Err(OrderRefusal::Ahead(3)));
543    }
544
545    #[test]
546    fn a_call_is_signed_by_the_box_and_fresh() {
547        let box_key = SigningKey::from_bytes(&[3u8; 32]);
548        let c = FleetCall { product: "gunnar".into(), instance: "acme".into(), role: "main".into(), call: call::REPORT.into(), body: "{}".into(), at_unix_ms: 1_800_000_000_000 };
549        let m = fleet_call_message(&c);
550        assert!(m.starts_with(CALL_DOMAIN));
551        let sig = box_key.sign(&m).to_bytes();
552        let pk = box_key.verifying_key();
553        assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_060_000), Ok(()));
554        assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_000_000 + CALL_FRESH_MS + 1), Err(CallRefusal::Stale));
555        let mut moved = c.clone();
556        moved.role = "twin".into();
557        assert_eq!(verify_fleet_call(&moved, &sig, &pk, 1_800_000_000_000), Err(CallRefusal::Signature));
558        assert_eq!(verify_fleet_call(&c, &sig[..10], &pk, 1_800_000_000_000), Err(CallRefusal::Signature));
559    }
560}