crates/monetize-embed/src/fleet.rs
//! **The pull channel's signed forms** (5.4, G1-pull): monetize never calls INTO an
//! estate; every estate calls monetize (`Fleet.Report`, `Fleet.Orders`, `Fleet.Ask`) and
//! monetize answers with [`SignedOrder`]s the box verifies under `monetize_pubkey`.
//!
//! Two forms, each a fixed binary layout (not JSON) so the estate's verifier
//! (`gunnar_repair::order`) and this crate agree byte for byte with no canonical-JSON
//! rule to share:
//!
//! * [`order_message`]: what monetize signs for ONE order to ONE box (`role`).
//! * [`fleet_call_message`]: what a box signs with its own Ed25519 appliance key for each
//! call it makes to monetize.
//!
//! An order is only "please run verb V with these arguments": the estate runs it through the
//! SAME function its local admin button and its boot check call, and says how it went in the
//! next [`Report`] ([`OrderAck`]).
use ed25519_dalek::{Signature, VerifyingKey};
use serde::{Deserialize, Serialize};
/// The domain tag every order signature starts with.
pub const ORDER_DOMAIN: &[u8] = b"nordisk-order-v1\0";
/// The domain tag every box's call signature starts with.
pub const CALL_DOMAIN: &[u8] = b"nordisk-fleet-call-v1\0";
/// How far a box's call time may be from monetize's clock.
pub const CALL_FRESH_MS: u64 = 5 * 60 * 1000;
/// How far ahead of the box's clock an order may be dated (the shared pump's and Gunnar's five
/// minutes).
pub const FUTURE_SKEW_MS: u64 = 5 * 60 * 1000;
/// How long `Fleet.Orders` is held open at most before it answers empty.
pub const LONG_POLL_MAX_MS: u64 = 25_000;
/// **How long a front waits on the shop's `SoldEstate.Upgrade`**: the shop's whole keep boot,
/// 2 h, the ONE constant the shop holds its call by (`monetize_product::KEEP_BOOT_WITHIN`; lane
/// G9, A7 row 9, Gunnar b74ca801). A front that gave up first would count a slow keep boot as
/// lost and ask for a Recharge while the shop still works.
pub use monetize_product::KEEP_BOOT_WITHIN;
/// **The shop's words for an Upgrade still running** (`an upgrade of <host> is already running
/// here`, an UNAVAILABLE answer): the front that hears them asks again later (REL-T2-1: the
/// Upgrade is started, then polled). The ONE box says them; the front's door reads them.
pub const UPGRADE_RUNNING: &str = "is already running here";
/// The order kinds. The body is JSON; unknown fields are ignored. The relay products' guardian
/// kinds ([`kind::GUARDIAN`], gunnar-repair's verbs) are defined here; the core's kinds are
/// `monetize_product::pull`'s one list (A28 M25), re-exported here under their old names.
pub mod kind {
/// `{main, twin, accept_lost, epoch, case?}`: the twin (or a fresh main) pumps the twin's
/// log into the main. The same verb the local "Recharge" button and the boot check run.
pub const RECHARGE_FROM_TWIN: &str = "recharge_from_twin";
/// `{primary, epoch, case?}`: the old primary may not serve past `epoch`.
pub const FENCE_PRIMARY: &str = "fence_primary";
/// `{twin, principal, case?}`: the primary takes the box at `twin` as its twin.
pub const ADOPT_TWIN: &str = "adopt_twin";
/// `{case?}`: the box reviews its twin and reports the `TwinReview`.
pub const REVIEW_TWIN: &str = "review_twin";
/// `{snapshot, case?}`: the money put back on a recharged main. `snapshot` is a
/// [`crate::Snapshot`] signed by monetize's key — what monetize's books hold for the
/// product. The box verifies it and every fact in it (`signing::verify_snapshot`), applies
/// each fact only if newer than the one it holds (a same fact is a no-op), deletes
/// nothing, and runs it only while it serves as the primary.
pub const RESTATE_ENTITLEMENTS: &str = "restate_entitlements";
/// [`super::GrowGoAhead`]: **a sold estate's paid data step** (G16-d, G1-pull row 17),
/// queued to the estate's main AND its twin once the step's money is captured. The box
/// verifies `go_ahead` under monetize's key (`signing::verify_estate_growth`: its estate,
/// good until `until_unix_ms`), then grows its own data disks to `data_gib` a disk: drain,
/// unmount, `Fleet.Ask{kind: ask::GROW_SET}` (monetize resizes them at the provider), look
/// again, remount. The twin first — its disk holds main's whole set. A newer go-ahead
/// supersedes an unacked older one; a growth already done is acked ok, nothing moved.
pub use monetize_product::pull::order::GROW_GO_AHEAD;
/// [`super::Refeed`]: **estate keys fed again to an already-laid estate** (MAIL-RELAY +
/// NIGHT-PUBREPO, 2026-10-28), queued to the estate's main and twin when the operator asks
/// (`EstateOrder.Refeed`). The box takes each key it knows by name ([`super::refeed`]) and
/// refuses the order by name otherwise. The body may CARRY A SECRET (the relay's token): never
/// logged; a newer one supersedes an unacked older one.
pub use monetize_product::pull::order::REFEED;
/// [`super::SignInAgain`]: **a sold estate's RESEND sign-in link** (S10, owner 2026-10-11
/// 05:53Z), queued to the estate's main when its buyer asks (`EstateOrder.SendSignInAgain`).
/// The order names whose passkey, which address and which console host — NO secret: the
/// main mints the fresh invite itself (only while the account holds no passkey) and mails
/// `https://{host}/login?invite=…&user=…` itself. The shop never holds a resend's login
/// secret (the FIRST link stays shop-minted at install, OM-21 decision A). A newer ask
/// supersedes an unacked older one.
pub use monetize_product::pull::order::SIGN_IN_AGAIN;
/// **The relay products' guardian kinds** (the twin's repair verbs): not core kinds; a
/// product whose guardian runs no twin never gets one.
pub const GUARDIAN: [&str; 5] = [
RECHARGE_FROM_TWIN,
FENCE_PRIMARY,
ADOPT_TWIN,
REVIEW_TWIN,
RESTATE_ENTITLEMENTS,
];
}
/// The `Fleet.Ask` kinds that are not a remedy: answered at once, no case opened.
/// A product's own order ([`ProductOrder`]) and the file it may carry ([`Fetch`]), MONETIZE-FIX.
pub use monetize_product::pull::{Fetch, FetchAsk, ProductOrder};
pub mod ask {
/// `monetize_product::pull::FetchAsk` `{id}`: **the box pulls the file a product order
/// carries** (MONETIZE-FIX); the bytes come back in `FleetAskAnswer.answer`.
pub use monetize_product::pull::ask::FETCH;
/// [`super::GrowSetAsk`]: **size the calling box's own data disks** at monetize's provider
/// (G16-d, G1-pull row 18) — no more than is paid for its role, never smaller. The main
/// sizes main's disks, the twin (its own key, `role = twin`) the twin's.
pub use monetize_product::pull::ask::GROW_SET;
/// [`super::FrontAsk`] `{role, done}`: **the shop boots ONE standing box once from the
/// release's keep medium** (was `SoldEstate.Upgrade`; plan §8, A24 §5.1 row 27). Asked by
/// the estate's front; the answer is `EstateUpgradeResponse` in `FleetAskAnswer.answer`.
pub const UPGRADE: &str = "upgrade";
/// [`super::FrontAsk`] `{role, seed, done}`: **the shop lays ONE lost box again** (was
/// `SoldEstate.Recharge`, row 29); the answer is `EstateRechargeResponse`. Not the guardian's
/// `recharge_from_twin` remedy: this one is the cloud half, asked by the front.
pub const RECHARGE: &str = "recharge";
/// [`super::FrontAsk`] `{done}`: **the estate takes its own installer material** (was
/// `SoldEstate.TakeMaterial`, row 28); the answer is `EstateTakeMaterialResponse`.
pub const MATERIAL: &str = "material";
/// [`super::FrontAsk`] `{}`: **the front takes the open update** (was
/// `SoldEstate.UpdateTake`, row 25); the answer is `EstateUpdateTakeResponse`.
pub const UPDATE_TAKE: &str = "update_take";
/// [`super::FrontAsk`] `{role, carries}`: **the front reports one box's update step** (was
/// `SoldEstate.UpdateReport`, row 26); the answer is `EstateUpdateReportResponse`.
pub const UPDATE_REPORT: &str = "update_report";
/// **The front's asks** — the five `SoldEstate` calls folded into `Fleet.Ask` (plan §8,
/// decided 2026-10-24). Answered at once, no case opened; asked only by the estate's front.
pub const FRONT: [&str; 5] = [UPGRADE, RECHARGE, MATERIAL, UPDATE_TAKE, UPDATE_REPORT];
/// Pure: **is `kind` one of the front's asks** ([`FRONT`])?
pub fn is_front(kind: &str) -> bool {
FRONT.contains(&kind)
}
}
/// The calls a box makes ([`FleetCall::call`]).
pub mod call {
pub const REPORT: &str = "report";
pub const ORDERS: &str = "orders";
pub const ASK: &str = "ask";
}
/// **One order to one box**, signed by the product's monetize key.
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct SignedOrder {
/// Per instance, strictly rising; the box refuses any `seq` at or below the last it ran.
pub seq: u64,
pub product: String,
pub instance: String,
/// The box the order is for: `main` | `twin` | `front`. Signed, so an order for the twin
/// is never run by the main.
pub role: String,
pub kind: String,
/// JSON text.
pub body: String,
pub issued_unix_ms: u64,
pub not_after_unix_ms: u64,
/// 64-byte Ed25519 over [`order_message`].
#[serde(default)]
pub signature: Vec<u8>,
}
/// Why a box refuses an order.
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
pub enum OrderRefusal {
#[error("the order is not signed by monetize")]
Signature,
#[error("order {seq} is not newer than {last}")]
Replayed { seq: u64, last: u64 },
#[error("order {0} has expired")]
Expired(u64),
#[error("order {0} is for {1}, not this box")]
NotForMe(u64, String),
#[error("order {0} is dated more than five minutes ahead of this box's clock")]
Ahead(u64),
}
fn field(out: &mut Vec<u8>, bytes: &[u8]) {
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
out.extend_from_slice(bytes);
}
/// The bytes an order's signature covers: [`ORDER_DOMAIN`] ‖ seq ‖ issued ‖ not_after (u64 BE)
/// ‖ then product, instance, role, kind, body, each as u32 BE length ‖ bytes.
pub fn order_message(o: &SignedOrder) -> Vec<u8> {
let mut out = ORDER_DOMAIN.to_vec();
out.extend_from_slice(&o.seq.to_be_bytes());
out.extend_from_slice(&o.issued_unix_ms.to_be_bytes());
out.extend_from_slice(&o.not_after_unix_ms.to_be_bytes());
for f in [&o.product, &o.instance, &o.role, &o.kind, &o.body] {
field(&mut out, f.as_bytes());
}
out
}
/// **The box's check**, as strict as the shared pump's (`monetize_estate_side::pump::check`):
/// signed under `key` with STRICT Ed25519 (no small-order key or `R`), checked first, so a
/// forged order is always [`OrderRefusal::Signature`] and never moves the box's sequence; then
/// for `(product, instance, role)`, newer than `last_seq`, inside its window at `now_unix_ms`
/// (`not_after` 0 = none), and dated no more than [`FUTURE_SKEW_MS`] ahead of the box's clock.
pub fn verify_order(
o: &SignedOrder,
key: &VerifyingKey,
me: (&str, &str, &str),
now_unix_ms: u64,
last_seq: u64,
) -> Result<(), OrderRefusal> {
let (product, instance, role) = me;
if !check(key, &order_message(o), &o.signature) {
return Err(OrderRefusal::Signature);
}
if o.product != product || o.instance != instance || o.role != role {
return Err(OrderRefusal::NotForMe(o.seq, o.role.clone()));
}
if o.seq <= last_seq {
return Err(OrderRefusal::Replayed {
seq: o.seq,
last: last_seq,
});
}
if o.not_after_unix_ms != 0 && now_unix_ms > o.not_after_unix_ms {
return Err(OrderRefusal::Expired(o.seq));
}
if o.issued_unix_ms > now_unix_ms.saturating_add(FUTURE_SKEW_MS) {
return Err(OrderRefusal::Ahead(o.seq));
}
Ok(())
}
/// **One call from a box to monetize**, signed by the box's own appliance key.
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct FleetCall {
pub product: String,
pub instance: String,
pub role: String,
/// [`call::REPORT`] | [`call::ORDERS`] | [`call::ASK`].
pub call: String,
/// JSON text: a [`Report`], an [`OrdersAfter`] or an [`Ask`].
pub body: String,
pub at_unix_ms: u64,
}
/// Why monetize refuses a box's call.
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
pub enum CallRefusal {
#[error("the call is not signed by the box's key")]
Signature,
#[error("the call's time is too far from monetize's clock")]
Stale,
}
/// The bytes a box's call signature covers: [`CALL_DOMAIN`] ‖ at (u64 BE) ‖ then product,
/// instance, role, call, body, each as u32 BE length ‖ bytes.
pub fn fleet_call_message(c: &FleetCall) -> Vec<u8> {
let mut out = CALL_DOMAIN.to_vec();
out.extend_from_slice(&c.at_unix_ms.to_be_bytes());
for f in [&c.product, &c.instance, &c.role, &c.call, &c.body] {
field(&mut out, f.as_bytes());
}
out
}
/// Verify a box's call under its appliance `key`, fresh at `now_unix_ms`.
pub fn verify_fleet_call(
c: &FleetCall,
signature: &[u8],
key: &VerifyingKey,
now_unix_ms: u64,
) -> Result<(), CallRefusal> {
if !check(key, &fleet_call_message(c), signature) {
return Err(CallRefusal::Signature);
}
if c.at_unix_ms.abs_diff(now_unix_ms) > CALL_FRESH_MS {
return Err(CallRefusal::Stale);
}
Ok(())
}
/// STRICT Ed25519 (`verify_strict`): a small-order key or `R` never verifies (T16, A28 M11).
fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> bool {
Signature::from_slice(sig)
.map(|s| key.verify_strict(msg, &s).is_ok())
.unwrap_or(false)
}
/// What a box says ran (or did not) — sent back in its next [`Report`].
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct OrderAck {
pub seq: u64,
pub ok: bool,
pub detail: String,
/// gunnar_repair's `Receipt` / `TwinReview`, JSON text; empty when none.
pub receipt_json: String,
}
/// **The box's heartbeat** (`Fleet.Report` body). Its arrival time is the liveness signal.
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Report {
pub build: String,
/// The box's own health words, flat: `serving` (`yes` | `no` | `fenced`), `data` (`ok` |
/// `lost` | `diverged`), `serves_at` (`host:port`), `twin` (`paired` | `silent` | …),
/// `epoch`. Unknown words are kept and shown.
pub health: std::collections::BTreeMap<String, String>,
pub acked: Vec<OrderAck>,
/// The product's twin review (gunnar: `gunnar_repair::TwinReview`), JSON text, when it
/// made one.
pub twin_review: Option<String>,
/// **The product's own state** (G1 pull: tenants and owners, usage, servable, the paid
/// door, the room a raise would find), JSON text of `monetize_product::pull::ProductReport`.
/// Sent by the box that serves tenants (the main); what monetize used to dial it for.
pub product: Option<String>,
pub measured_unix_ms: u64,
}
/// `Fleet.Orders` body: orders after `after`, held up to `wait_ms` (≤ [`LONG_POLL_MAX_MS`]).
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct OrdersAfter {
pub after: u64,
pub wait_ms: u64,
}
/// Body of a [`kind::GROW_GO_AHEAD`] order.
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct GrowGoAhead {
/// The [`crate::signing::EstateGrowth`] JSON, signed by monetize's key.
pub go_ahead: String,
/// GB a data disk THIS box's role is paid to (main: the step; twin: main's whole set).
pub data_gib: u64,
}
/// Body of a [`kind::SIGN_IN_AGAIN`] order. Never a secret.
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct SignInAgain {
/// The account whose first passkey the invite enrols (the order's account).
pub account: String,
/// The order's address: where the estate mails the link.
pub to: String,
/// The console's relying-party host the link points at (`<company>.<apex>`).
pub host: String,
/// How long the invite lives.
pub ttl_secs: u64,
}
/// **The estate keys a [`kind::REFEED`] order may carry**, in the words of the feed a box is
/// laid with: the mains' mail relay (`secret.smtp_url`, `secret.smtp_from`, and `secret.smtp_ca`
/// for a relay on a private CA; monetize-mail `FEED_SMTP_*`) and a site's public repositories
/// (`public_repositories`, comma-separated). A box takes the ones it knows and refuses any
/// other by name.
pub mod refeed {
pub const SMTP_URL: &str = "secret.smtp_url";
pub const SMTP_FROM: &str = "secret.smtp_from";
pub const SMTP_CA: &str = "secret.smtp_ca";
pub const PUBLIC_REPOSITORIES: &str = "public_repositories";
/// The site's public-site switch, `on` | `off` (GRS-PUBLIC 2026-10-28; the lay's own front
/// feed word, monetize-shop-pairing `contract::FEED_PUBLIC_SITE`).
pub const PUBLIC_SITE: &str = "console_public_site";
/// Every key a refeed may carry.
pub const KNOWN: [&str; 5] = [
SMTP_URL,
SMTP_FROM,
SMTP_CA,
PUBLIC_REPOSITORIES,
PUBLIC_SITE,
];
/// The keys whose values are secrets (never shown).
pub fn is_secret(key: &str) -> bool {
key.starts_with("secret.")
}
}
/// Body of a [`kind::REFEED`] order (MAIL-RELAY + NIGHT-PUBREPO, 2026-10-28): the estate keys
/// fed again, key → value. **A `secret.` value is a secret**: the `Debug` names the keys only.
#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Refeed {
pub keys: std::collections::BTreeMap<String, String>,
}
impl std::fmt::Debug for Refeed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Refeed")
.field("keys", &self.keys.keys().collect::<Vec<_>>())
.finish()
}
}
/// `Fleet.Ask` body of [`ask::GROW_SET`]: the box's own data disks to `gib` a disk. No
/// `members` = every laid data disk of the calling box; `gib` 0 = what its role is paid to.
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct GrowSetAsk {
pub members: Vec<String>,
pub gib: u64,
}
/// **The body of a front's ask** ([`ask::FRONT`]): the fields the old `SoldEstate` request
/// carried, less the company and the signature — the `FleetSigned` call names the estate
/// (`instance`) and the box (`role = front`) and is signed by the front's key. `role` is the
/// box the ask is about (`main` | `twin`; for `update_report`, the box whose step it is);
/// `carries` is the update report's `<outcome>\n<words>`.
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct FrontAsk {
#[serde(skip_serializing_if = "String::is_empty")]
pub role: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub done: bool,
#[serde(skip_serializing_if = "String::is_empty")]
pub seed: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub carries: String,
}
/// `Fleet.Ask` body: a request from the estate (`new_twin`, `new_server`, `recharge`, …).
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Ask {
pub kind: String,
pub body: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer as _, SigningKey};
/// A28 M25: the core kinds are defined once, in `monetize_product::pull`; the kinds here
/// are that list's, and the rest of [`kind`] are the relay products' guardian kinds.
#[test]
fn the_core_kinds_here_are_monetize_products_one_list() {
use monetize_product::pull;
assert_eq!(kind::GROW_GO_AHEAD, pull::order::GROW_GO_AHEAD);
assert_eq!(kind::SIGN_IN_AGAIN, pull::order::SIGN_IN_AGAIN);
assert_eq!(kind::REFEED, pull::order::REFEED);
assert_eq!(ask::GROW_SET, pull::ask::GROW_SET);
for k in kind::GUARDIAN {
assert!(!pull::is_core(k), "{k} is a guardian kind, not a core one");
}
assert_eq!(
kind::GUARDIAN,
[
kind::RECHARGE_FROM_TWIN,
kind::FENCE_PRIMARY,
kind::ADOPT_TWIN,
kind::REVIEW_TWIN,
kind::RESTATE_ENTITLEMENTS
]
);
}
/// ★ Plan §8 (the SoldEstate → Fleet fold): the front's five asks are named once, none of
/// them is a guardian remedy or `grow_set`, and their body round-trips through the `Ask`
/// JSON with only the fields it carries.
#[test]
fn the_fronts_asks_are_the_five_soldestate_calls_and_their_body_round_trips() {
assert_eq!(
ask::FRONT,
[
"upgrade",
"recharge",
"material",
"update_take",
"update_report"
]
);
for k in ask::FRONT {
assert!(ask::is_front(k));
}
for k in [
ask::GROW_SET,
kind::RECHARGE_FROM_TWIN,
kind::REVIEW_TWIN,
"new_twin",
"new_server",
"",
] {
assert!(!ask::is_front(k), "{k}");
}
// The ONE box's own door, not a product's kind: none is one of the core's 8 (A28 M25).
for k in ask::FRONT {
assert!(!monetize_product::pull::is_core(k), "{k}");
}
let a = FrontAsk {
role: "main".into(),
seed: "SEED".into(),
..Default::default()
};
let text = serde_json::to_string(&Ask {
kind: ask::RECHARGE.into(),
body: serde_json::to_value(&a).unwrap(),
})
.unwrap();
assert_eq!(
text,
r#"{"kind":"recharge","body":{"role":"main","seed":"SEED"}}"#
);
let back: Ask = serde_json::from_str(&text).unwrap();
assert_eq!(serde_json::from_value::<FrontAsk>(back.body).unwrap(), a);
assert_eq!(
serde_json::from_value::<FrontAsk>(serde_json::json!({})).unwrap(),
FrontAsk::default(),
"an empty body is the update take's"
);
}
/// ★ Lane G9, A7 row 9 (Gunnar b74ca801): a front waits on the shop's whole keep boot, 2 h,
/// the one constant the shop's cloud crate holds its Upgrade call by.
#[test]
fn a_front_waits_on_the_shops_whole_keep_boot() {
assert_eq!(KEEP_BOOT_WITHIN, std::time::Duration::from_secs(2 * 3600));
assert_eq!(KEEP_BOOT_WITHIN, monetize_product::KEEP_BOOT_WITHIN);
}
const GOLDEN_MESSAGE_HEX: &str = "6e6f726469736b2d6f726465722d7631000000000000000003000001a3185c5000000001a3186a0ba00000000667756e6e61720000000461636d65000000047477696e0000001272656368617267655f66726f6d5f7477696e000000517b226163636570745f6c6f7374223a66616c73652c2265706f6368223a352c226d61696e223a2231302e362e302e31303a3530303531222c227477696e223a2231302e362e302e31323a3530303531227d";
const GOLDEN_SIGNATURE_HEX: &str = "d4a3233158d9a3fbfa42f9aa80fd2ca420666ec2fe9f7fbf1a61abab533c09f6dab60c67367bc771a1d819d1e6bed28c59d6d56187a4748ba9b4b9988d13d50a";
fn key() -> SigningKey {
SigningKey::from_bytes(&[7u8; 32])
}
fn order() -> SignedOrder {
SignedOrder {
seq: 3,
product: "gunnar".into(),
instance: "acme".into(),
role: "twin".into(),
kind: kind::RECHARGE_FROM_TWIN.into(),
body: r#"{"accept_lost":false,"epoch":5,"main":"10.6.0.10:50051","twin":"10.6.0.12:50051"}"#.into(),
issued_unix_ms: 1_800_000_000_000,
not_after_unix_ms: 1_800_000_900_000,
signature: Vec::new(),
}
}
fn signed() -> SignedOrder {
let mut o = order();
o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
o
}
/// The golden vector gunnar-repair pastes into its own test: same order, same bytes.
#[test]
fn the_order_message_is_the_agreed_binary_layout() {
let m = order_message(&order());
assert!(m.starts_with(ORDER_DOMAIN));
let mut want = ORDER_DOMAIN.to_vec();
want.extend_from_slice(&3u64.to_be_bytes());
want.extend_from_slice(&1_800_000_000_000u64.to_be_bytes());
want.extend_from_slice(&1_800_000_900_000u64.to_be_bytes());
for f in [
"gunnar",
"acme",
"twin",
kind::RECHARGE_FROM_TWIN,
&order().body,
] {
want.extend_from_slice(&(f.len() as u32).to_be_bytes());
want.extend_from_slice(f.as_bytes());
}
assert_eq!(m, want);
let sig = key().sign(&m).to_bytes();
let hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(
hex, GOLDEN_SIGNATURE_HEX,
"the golden vector moved: tell gunnar_repair::order"
);
let mhex: String = m.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(mhex, GOLDEN_MESSAGE_HEX);
}
#[test]
fn a_box_runs_only_a_fresh_order_signed_for_it() {
let pk = key().verifying_key();
let me = ("gunnar", "acme", "twin");
let now = 1_800_000_100_000;
assert_eq!(verify_order(&signed(), &pk, me, now, 2), Ok(()));
assert_eq!(
verify_order(&signed(), &pk, me, now, 3),
Err(OrderRefusal::Replayed { seq: 3, last: 3 })
);
assert_eq!(
verify_order(&signed(), &pk, me, 1_800_000_900_001, 0),
Err(OrderRefusal::Expired(3))
);
assert_eq!(
verify_order(&signed(), &pk, ("gunnar", "acme", "main"), now, 0),
Err(OrderRefusal::NotForMe(3, "twin".into()))
);
let mut forged = signed();
forged.role = "main".into();
assert_eq!(
verify_order(&forged, &pk, ("gunnar", "acme", "main"), now, 0),
Err(OrderRefusal::Signature)
);
let other = SigningKey::from_bytes(&[9u8; 32]).verifying_key();
assert_eq!(
verify_order(&signed(), &other, me, now, 0),
Err(OrderRefusal::Signature)
);
}
/// ★ **T16 (A28 M11): this check is as strict as the shared pump's** (`monetize_estate_side::
/// pump::check`, P4c). Under a small-order key (the identity point) the all-zero signature
/// verifies ANY message with plain `verify`; a strict check refuses it, and a real key's
/// signature still passes.
#[test]
fn a_weak_key_with_the_all_zero_signature_signs_no_order() {
let (weak, zero) = weak_key_and_zero_signature();
let mut o = order();
o.signature = zero;
let now = 1_800_000_100_000;
assert_eq!(
verify_order(&o, &weak, ("gunnar", "acme", "twin"), now, 0),
Err(OrderRefusal::Signature),
"a weak key verifies nothing"
);
assert_eq!(
verify_order(
&signed(),
&key().verifying_key(),
("gunnar", "acme", "twin"),
now,
0
),
Ok(())
);
}
/// ★ The same weak key never vouches for a box's call either.
#[test]
fn a_weak_box_key_with_the_all_zero_signature_signs_no_call() {
let (weak, zero) = weak_key_and_zero_signature();
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,
};
assert_eq!(
verify_fleet_call(&c, &zero, &weak, 1_800_000_000_000),
Err(CallRefusal::Signature)
);
}
/// The identity point as a key, and the signature (R = identity, s = 0) that plain `verify`
/// accepts for every message under it.
fn weak_key_and_zero_signature() -> (VerifyingKey, Vec<u8>) {
let mut identity = [0u8; 32];
identity[0] = 1;
let weak = VerifyingKey::from_bytes(&identity).expect("the identity point decompresses");
let mut zero = vec![0u8; 64];
zero[0] = 1;
(weak, zero)
}
/// ★ Every signed field is covered: one tampered field, a flipped signature bit, a short or
/// missing signature, or another key is refused as not monetize's.
#[test]
fn a_tampered_order_is_not_monetizes() {
let pk = key().verifying_key();
let me = ("gunnar", "acme", "twin");
let now = 1_800_000_100_000;
type Tamper = Box<dyn Fn(&mut SignedOrder)>;
let tampered: Vec<(&str, Tamper)> = vec![
("seq", Box::new(|o| o.seq += 1)),
("kind", Box::new(|o| o.kind = kind::FENCE_PRIMARY.into())),
("body", Box::new(|o| o.body = "{}".into())),
("issued", Box::new(|o| o.issued_unix_ms -= 1)),
("not_after", Box::new(|o| o.not_after_unix_ms += 1)),
("bit", Box::new(|o| o.signature[5] ^= 1)),
("short", Box::new(|o| o.signature.truncate(63))),
("none", Box::new(|o| o.signature.clear())),
];
for (what, f) in tampered {
let mut o = signed();
f(&mut o);
assert_eq!(
verify_order(&o, &pk, me, now, 0),
Err(OrderRefusal::Signature),
"{what}"
);
}
}
/// ★ The signature is checked FIRST: a forged order addressed to another box is forged (it
/// never moves the box's sequence), not "not for me".
#[test]
fn a_forged_order_for_another_box_is_forged_not_misaddressed() {
let mut o = signed();
o.role = "main".into();
assert_eq!(
verify_order(
&o,
&key().verifying_key(),
("gunnar", "acme", "twin"),
1_800_000_100_000,
0
),
Err(OrderRefusal::Signature)
);
}
/// ★ `not_after` 0 is no window (as the shared pump reads it), not "expired at the epoch".
#[test]
fn an_order_with_no_window_does_not_expire() {
let mut o = order();
o.not_after_unix_ms = 0;
o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
assert_eq!(
verify_order(
&o,
&key().verifying_key(),
("gunnar", "acme", "twin"),
1_900_000_000_000,
0
),
Ok(())
);
}
/// ★ An order dated more than [`FUTURE_SKEW_MS`] ahead of the box's clock is refused by
/// name; five minutes of skew is fine.
#[test]
fn an_order_dated_ahead_of_the_box_clock_is_refused() {
let pk = key().verifying_key();
let me = ("gunnar", "acme", "twin");
let issued = order().issued_unix_ms;
assert_eq!(
verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS, 0),
Ok(()),
"five minutes of skew"
);
assert_eq!(
verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS - 1, 0),
Err(OrderRefusal::Ahead(3))
);
}
#[test]
fn a_call_is_signed_by_the_box_and_fresh() {
let box_key = SigningKey::from_bytes(&[3u8; 32]);
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,
};
let m = fleet_call_message(&c);
assert!(m.starts_with(CALL_DOMAIN));
let sig = box_key.sign(&m).to_bytes();
let pk = box_key.verifying_key();
assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_060_000), Ok(()));
assert_eq!(
verify_fleet_call(&c, &sig, &pk, 1_800_000_000_000 + CALL_FRESH_MS + 1),
Err(CallRefusal::Stale)
);
let mut moved = c.clone();
moved.role = "twin".into();
assert_eq!(
verify_fleet_call(&moved, &sig, &pk, 1_800_000_000_000),
Err(CallRefusal::Signature)
);
assert_eq!(
verify_fleet_call(&c, &sig[..10], &pk, 1_800_000_000_000),
Err(CallRefusal::Signature)
);
}
}