Skip to main content

monetize_embed/
signing.rs

1//! **The one canonical form** an entitlement is signed over, and the verify side.
2//!
3//! Signing lives in `monetize` (it has the key); verifying lives here because a product
4//! must check a fact without linking the core (redb, vendors). Core depends on this
5//! crate for [`fact_message`] so there is exactly one canonical form.
6//!
7//! Canonical JSON: the fact's fields minus `signature`, objects with keys sorted
8//! bytewise, no whitespace. `serde_json`'s map is sorted by default but that is a cargo
9//! feature (`preserve_order`) any crate in the build could flip, so the sort is done
10//! here, explicitly, and does not depend on it.
11
12use ed25519_dalek::{Signature, Verifier, VerifyingKey};
13use monetize_product::EntitlementFact;
14use serde_json::Value;
15
16/// A signed set of facts, the file monetize hands a product so it can start (or
17/// restart) with the full picture before any push arrives.
18#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
19pub struct Snapshot {
20    /// Monotonic: a cache refuses a snapshot older than the one it holds (replay).
21    pub issued_unix_ms: u64,
22    pub facts: Vec<EntitlementFact>,
23    /// Ed25519 over [`snapshot_message`], by monetize's key.
24    pub signature: Vec<u8>,
25}
26
27#[derive(Debug, thiserror::Error, PartialEq, Eq)]
28pub enum SignatureError {
29    #[error("signature is not 64 bytes")]
30    Malformed,
31    #[error("signature does not verify for tenant {0}")]
32    Fact(String),
33    /// The fact's ISSUE TIME is not signed by the same key, or the fact carries
34    /// one half of the stamp without the other. Told apart from [`Self::Fact`]
35    /// on purpose: the six fields were genuine and the monotonic half was not,
36    /// which is a tampering attempt with a different shape and a different
37    /// remedy from a wholly forged fact.
38    #[error("the issue time on the fact for tenant {0} is not signed by the same key")]
39    Issued(String),
40    #[error("snapshot envelope signature does not verify")]
41    Envelope,
42    #[error("go-ahead signature does not verify for nonce {0}")]
43    GoAhead(String),
44    /// An actor ticket was refused. The string is the OPERATOR's reason and may
45    /// name the ticket's own fields; see `crate::ticket::verify_ticket` for why
46    /// none of them is a customer's to read.
47    #[error("the actor ticket was refused: {0}")]
48    Ticket(String),
49    /// A shop's go-ahead to grow a sold estate's data does not verify.
50    #[error("the shop's growth go-ahead for {0} does not verify")]
51    Growth(String),
52    /// A sold estate's call to its shop is not signed by the estate's key.
53    #[error("the call is not signed by {0}'s key")]
54    Call(String),
55}
56
57/// Write `value` as canonical JSON: keys sorted, no whitespace.
58pub fn canonical_json(value: &Value, out: &mut String) {
59    match value {
60        Value::Object(map) => {
61            let mut keys: Vec<&String> = map.keys().collect();
62            keys.sort();
63            out.push('{');
64            for (i, k) in keys.iter().enumerate() {
65                if i > 0 {
66                    out.push(',');
67                }
68                // A key is a string: written as the JSON string serde_json writes for it.
69                out.push_str(&Value::String((*k).clone()).to_string());
70                out.push(':');
71                canonical_json(&map[*k], out);
72            }
73            out.push('}');
74        }
75        Value::Array(items) => {
76            out.push('[');
77            for (i, v) in items.iter().enumerate() {
78                if i > 0 {
79                    out.push(',');
80                }
81                canonical_json(v, out);
82            }
83            out.push(']');
84        }
85        other => out.push_str(&other.to_string()),
86    }
87}
88
89/// Canonical JSON of the fact with `drop` removed. The ONE writer of a fact's
90/// signed bytes; the two forms below differ only in what they drop, so they
91/// cannot drift apart.
92fn fact_form(fact: &EntitlementFact, drop: &[&str]) -> Vec<u8> {
93    let mut v = serde_json::to_value(fact).expect("fact serializes");
94    let object = v.as_object_mut().expect("fact is an object");
95    for key in drop {
96        object.remove(*key);
97    }
98    let mut s = String::new();
99    canonical_json(&v, &mut s);
100    s.into_bytes()
101}
102
103/// **The V1 form**: every field except `signature`, `issued_unix_ms` and
104/// `issued_signature`.
105///
106/// This is byte for byte what it was before the issue time existed, and it must
107/// stay that way for ever: it is the form an appliance built before 2026-09-17
108/// computes from the seven proto fields it knows, and every fact ever signed
109/// carries a signature over it. Adding the issue time HERE instead of in a
110/// second form would have invalidated every one of them at once.
111pub fn fact_message(fact: &EntitlementFact) -> Vec<u8> {
112    fact_form(fact, &["signature", "issued_unix_ms", "issued_signature"])
113}
114
115/// **The V2 form**: [`fact_message`] plus `issued_unix_ms`. What
116/// [`EntitlementFact::issued_signature`] covers, and the reason the issue time
117/// cannot be added, moved or bumped by anyone but monetize.
118pub fn fact_message_issued(fact: &EntitlementFact) -> Vec<u8> {
119    fact_form(fact, &["signature", "issued_signature"])
120}
121
122/// The bytes a snapshot's envelope signature covers. The facts inside keep their own
123/// signatures (they are part of the message), so a snapshot vouches for the *set*.
124pub fn snapshot_message(issued_unix_ms: u64, facts: &[EntitlementFact]) -> Vec<u8> {
125    let v = serde_json::json!({ "issued_unix_ms": issued_unix_ms, "facts": facts });
126    let mut s = String::new();
127    canonical_json(&v, &mut s);
128    s.into_bytes()
129}
130
131pub(crate) fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> Result<bool, SignatureError> {
132    let sig = Signature::from_slice(sig).map_err(|_| SignatureError::Malformed)?;
133    Ok(key.verify(msg, &sig).is_ok())
134}
135
136/// **Both halves.** The V1 signature always, and the V2 signature whenever the
137/// fact carries an issue time.
138///
139/// The two are checked together and never apart, which is what stops the issue
140/// time being editable in transit. Three shapes are refused here and not later:
141///
142/// * a stamped fact whose `issued_signature` does not verify — someone ADDED or
143///   BUMPED an issue time. Bumping matters as much as adding: a fact stamped
144///   far in the future would latch a product's high-water mark past every
145///   legitimate fact monetize will ever mint for that tenant, which is a lasting
146///   denial of service dressed as a plan change;
147/// * `issued_signature` with no `issued_unix_ms`, and
148/// * `issued_unix_ms` with no `issued_signature` — an unsigned issue time is not
149///   a weaker fact, it is a forged one.
150pub fn verify_fact(fact: &EntitlementFact, key: &VerifyingKey) -> Result<(), SignatureError> {
151    if !check(key, &fact_message(fact), &fact.signature)? {
152        return Err(SignatureError::Fact(fact.tenant.0.clone()));
153    }
154    match (fact.issued_unix_ms, fact.issued_signature.is_empty()) {
155        // Unstamped: a fact signed before the issue time existed. Legal.
156        (None, true) => Ok(()),
157        (Some(_), false) => {
158            if check(key, &fact_message_issued(fact), &fact.issued_signature)? {
159                Ok(())
160            } else {
161                Err(SignatureError::Issued(fact.tenant.0.clone()))
162            }
163        }
164        (None, false) | (Some(_), true) => Err(SignatureError::Issued(fact.tenant.0.clone())),
165    }
166}
167
168/// Envelope first, then every fact: one forged fact rejects the whole snapshot.
169pub fn verify_snapshot(snap: &Snapshot, key: &VerifyingKey) -> Result<(), SignatureError> {
170    if !check(key, &snapshot_message(snap.issued_unix_ms, &snap.facts), &snap.signature)? {
171        return Err(SignatureError::Envelope);
172    }
173    snap.facts.iter().try_for_each(|f| verify_fact(f, key))
174}
175
176// ── the growth go-ahead (`DATA-SET-GROWTH-FLOW.md` D3 / T14) ───────────────
177
178/// **The go-ahead a data-set growth is started on**, as gunnar's
179/// `gunnar_server::grow::go_ahead::GoAhead` parses it:
180///
181/// ```json
182/// {"v":1,"signer":"monetize","target_sectors":<u64>,"nonce":"<one line ≤256 B>",
183///  "issued_unix_ms":<i64>,"signature":"<base64, standard alphabet, padded>"}
184/// ```
185///
186/// The signature is Ed25519 by monetize's signing key over
187/// [`go_ahead_message`]: the canonical JSON (keys sorted bytewise, no
188/// whitespace) of the object MINUS `signature` — the same form and the same
189/// key an [`EntitlementFact`] is signed with, so a metered gunnar verifies it
190/// with the `--monetize-pubkey` it already holds. gunnar checks the structure
191/// (`v`, a known `signer`, a non-empty nonce, `target_sectors` equal to the
192/// request's, the nonce unspent on that box); a `monetize` policy plugged into
193/// its `GoAheadPolicy` calls [`verify_go_ahead`].
194#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
195pub struct GoAhead {
196    pub v: u32,
197    pub signer: String,
198    pub target_sectors: u64,
199    pub nonce: String,
200    pub issued_unix_ms: i64,
201    #[serde(default)]
202    pub signature: String,
203}
204
205/// The signer word a monetize-minted go-ahead carries.
206pub const GO_AHEAD_SIGNER_MONETIZE: &str = "monetize";
207
208/// The bytes a go-ahead's signature covers: every field except `signature`,
209/// canonical.
210pub fn go_ahead_message(go: &GoAhead) -> Vec<u8> {
211    let mut v = serde_json::to_value(go).expect("go-ahead serializes");
212    v.as_object_mut().expect("go-ahead is an object").remove("signature");
213    let mut s = String::new();
214    canonical_json(&v, &mut s);
215    s.into_bytes()
216}
217
218/// Parse the bytes handed on the wire and verify the signature under `key`.
219/// Structure first (so a refusal names what is wrong), then the signature.
220pub fn verify_go_ahead(bytes: &[u8], key: &VerifyingKey) -> Result<GoAhead, SignatureError> {
221    let go: GoAhead = serde_json::from_slice(bytes).map_err(|_| SignatureError::Malformed)?;
222    if go.v != 1 || go.signer != GO_AHEAD_SIGNER_MONETIZE || go.nonce.trim().is_empty() {
223        return Err(SignatureError::Malformed);
224    }
225    let sig = base64_decode(&go.signature).ok_or(SignatureError::Malformed)?;
226    if check(key, &go_ahead_message(&go), &sig)? {
227        Ok(go)
228    } else {
229        Err(SignatureError::GoAhead(go.nonce.clone()))
230    }
231}
232
233const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
234
235/// Standard base64, padded — the alphabet `GoAhead.signature` is written in.
236/// Twenty lines here rather than a dependency the two crates that need it
237/// (this one and `monetize`) would otherwise add for one field.
238pub fn base64_encode(bytes: &[u8]) -> String {
239    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
240    for chunk in bytes.chunks(3) {
241        let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
242        let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
243        out.push(B64[(n >> 18) as usize & 63] as char);
244        out.push(B64[(n >> 12) as usize & 63] as char);
245        out.push(if chunk.len() > 1 { B64[(n >> 6) as usize & 63] as char } else { '=' });
246        out.push(if chunk.len() > 2 { B64[n as usize & 63] as char } else { '=' });
247    }
248    out
249}
250
251/// The inverse; `None` on anything that is not padded standard base64.
252pub fn base64_decode(text: &str) -> Option<Vec<u8>> {
253    let text = text.trim();
254    if text.len() % 4 != 0 {
255        return None;
256    }
257    let val = |c: u8| B64.iter().position(|b| *b == c).map(|p| p as u32);
258    let mut out = Vec::with_capacity(text.len() / 4 * 3);
259    for chunk in text.as_bytes().chunks(4) {
260        let pad = chunk.iter().rev().take_while(|c| **c == b'=').count();
261        if pad > 2 || chunk[..4 - pad].contains(&b'=') {
262            return None;
263        }
264        let mut n = 0u32;
265        for (i, c) in chunk.iter().enumerate() {
266            let v = if i >= 4 - pad { 0 } else { val(*c)? };
267            n = (n << 6) | v;
268        }
269        out.push((n >> 16) as u8);
270        if pad < 2 {
271            out.push((n >> 8) as u8);
272        }
273        if pad < 1 {
274            out.push(n as u8);
275        }
276    }
277    Some(out)
278}
279
280// ── a sold estate grows through its shop (one shop, 2026-10-07) ───────────
281
282/// **The shop's go-ahead to grow a sold estate's data**: what the shop sold
283/// (main's data GB per disk), for which estate, until when. The shop's
284/// monetize signs it with its fact key once the step is paid; the estate
285/// pulls it in a `grow_go_ahead` order (G16-d; no estate runs a monetize) and verifies it under the ONE monetize's
286/// public key, fed at install, before its growth runs:
287///
288/// ```json
289/// {"v":1,"signer":"shop","estate":"acme","main_data_gib_per_member":25,
290///  "until_unix_ms":<i64>,"reference":"estate/acme/change-2",
291///  "issued_unix_ms":<i64>,"signature":"<base64, standard alphabet, padded>"}
292/// ```
293///
294/// Signed as [`GoAhead`] is: Ed25519 over the canonical JSON of every field
295/// but `signature`.
296#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
297pub struct EstateGrowth {
298    pub v: u32,
299    pub signer: String,
300    /// The estate's company name (`acme` of `acme.gunnar.rs`).
301    pub estate: String,
302    /// GB per data disk on main, as paid. The twin's data disk holds them all.
303    pub main_data_gib_per_member: u64,
304    /// The go-ahead is good until then: the end of the period it was paid for.
305    pub until_unix_ms: i64,
306    /// The shop's order reference the step was paid on.
307    pub reference: String,
308    pub issued_unix_ms: i64,
309    #[serde(default)]
310    pub signature: String,
311}
312
313/// The signer word a shop-minted growth go-ahead carries.
314pub const GROWTH_SIGNER_SHOP: &str = "shop";
315
316/// The bytes an [`EstateGrowth`]'s signature covers.
317pub fn estate_growth_message(g: &EstateGrowth) -> Vec<u8> {
318    let mut v = serde_json::to_value(g).expect("a growth go-ahead serializes");
319    v.as_object_mut().expect("a growth go-ahead is an object").remove("signature");
320    let mut s = String::new();
321    canonical_json(&v, &mut s);
322    s.into_bytes()
323}
324
325/// Parse the bytes and verify them under the shop's `key`. Structure first,
326/// then the signature. Expiry is the caller's: it holds the clock.
327pub fn verify_estate_growth(bytes: &[u8], key: &VerifyingKey) -> Result<EstateGrowth, SignatureError> {
328    let g: EstateGrowth = serde_json::from_slice(bytes).map_err(|_| SignatureError::Malformed)?;
329    if g.v != 1 || g.signer != GROWTH_SIGNER_SHOP || g.estate.trim().is_empty() || g.main_data_gib_per_member == 0 {
330        return Err(SignatureError::Malformed);
331    }
332    let sig = base64_decode(&g.signature).ok_or(SignatureError::Malformed)?;
333    if check(key, &estate_growth_message(&g), &sig)? {
334        Ok(g)
335    } else {
336        Err(SignatureError::Growth(g.estate.clone()))
337    }
338}
339
340/// **A sold estate's call to its shop** (`SoldEstate.Recharge`, `Upgrade`, `TakeMaterial`,
341/// `UpdateTake`, `UpdateReport`): what is asked, by which estate, when. The estate's
342/// monetize signs it with the client key minted for it at install (kept in
343/// the shop's custody); the shop verifies it under the key's public half,
344/// recorded on the order. A call is fresh for [`CALL_FRESH_MS`] either side of
345/// the shop's clock.
346#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
347pub struct EstateCall {
348    pub v: u32,
349    /// `grow-go-ahead` | `grow-set`.
350    pub call: String,
351    pub estate: String,
352    /// `grow-set`: the data disks to size, by the estate's own names.
353    #[serde(default)]
354    pub members: Vec<String>,
355    /// `grow-set`: GB each member is sized to.
356    #[serde(default)]
357    pub gib: u64,
358    pub at_unix_ms: i64,
359    /// `recharge` (F4, 2026-10-09): what the call carries besides its words — the seed a
360    /// recharged box is laid with — so the signature covers it. Empty, and then absent from the
361    /// canonical JSON, on every other call: their signatures are as before.
362    #[serde(default, skip_serializing_if = "String::is_empty")]
363    pub carries: String,
364}
365
366/// How far a call's time may be from the shop's clock.
367pub const CALL_FRESH_MS: i64 = 5 * 60 * 1000;
368
369/// The bytes an [`EstateCall`]'s signature covers: its canonical JSON.
370pub fn estate_call_message(c: &EstateCall) -> Vec<u8> {
371    let v = serde_json::to_value(c).expect("an estate call serializes");
372    let mut s = String::new();
373    canonical_json(&v, &mut s);
374    s.into_bytes()
375}
376
377/// Verify a call's `signature` under the estate's client `key`, and that it
378/// is fresh at `now_unix_ms`.
379pub fn verify_estate_call(c: &EstateCall, signature: &[u8], key: &VerifyingKey, now_unix_ms: i64) -> Result<(), SignatureError> {
380    if c.v != 1 || c.estate.trim().is_empty() || (c.at_unix_ms - now_unix_ms).abs() > CALL_FRESH_MS {
381        return Err(SignatureError::Call(c.estate.clone()));
382    }
383    if check(key, &estate_call_message(c), signature)? {
384        Ok(())
385    } else {
386        Err(SignatureError::Call(c.estate.clone()))
387    }
388}
389
390#[cfg(test)]
391mod go_ahead_tests {
392    use super::*;
393
394    #[test]
395    fn base64_round_trips_every_padding_shape_and_refuses_junk() {
396        for n in 0..10 {
397            let bytes: Vec<u8> = (0..n).map(|i| (i * 37 + 11) as u8).collect();
398            let enc = base64_encode(&bytes);
399            assert_eq!(enc.len() % 4, 0);
400            assert_eq!(base64_decode(&enc).unwrap(), bytes, "{enc}");
401        }
402        assert_eq!(base64_encode(b"Man"), "TWFu");
403        assert_eq!(base64_encode(b"Ma"), "TWE=");
404        assert_eq!(base64_encode(b"M"), "TQ==");
405        assert_eq!(base64_decode("TQ="), None);
406        assert_eq!(base64_decode("T@=="), None);
407        assert_eq!(base64_decode("TQ=x"), None);
408    }
409
410    #[test]
411    fn the_go_ahead_message_is_canonical_and_excludes_the_signature() {
412        let go = GoAhead { v: 1, signer: "monetize".into(), target_sectors: 134_217_728, nonce: "n-1".into(), issued_unix_ms: 1_800_000_000_000, signature: "zzz".into() };
413        let msg = String::from_utf8(go_ahead_message(&go)).unwrap();
414        assert_eq!(msg, r#"{"issued_unix_ms":1800000000000,"nonce":"n-1","signer":"monetize","target_sectors":134217728,"v":1}"#);
415    }
416}
417
418// ── the two compatibility directions ───────────────────────────────────────
419
420/// **The issue time was added on 2026-09-17, and nothing signed before it may
421/// break.** These tests are the promise, written down where the forms are.
422///
423/// The two directions a change to a signed form has to answer:
424///
425/// * an OLD fact meeting NEW code — every signature ever minted still verifies,
426///   because the V1 form is untouched;
427/// * a NEW fact meeting an OLD appliance — it reads seven proto fields, computes
428///   the V1 form and checks `signature`, which is exactly what
429///   [`v1_of_a_stamped_fact_is_byte_for_byte_the_old_form`] measures.
430///
431/// Neither direction ends in service withdrawn from a paying customer. The
432/// guard itself is the product's (gunnar's `EntitlementSlot::set`); what is
433/// promised here is only that both generations can still read the fact.
434#[cfg(test)]
435mod stamp_tests {
436    use super::*;
437    use ed25519_dalek::{Signer as _, SigningKey};
438    use monetize_product::{State, TenantId};
439
440    fn key() -> SigningKey {
441        SigningKey::from_bytes(&[3u8; 32])
442    }
443
444    fn bare() -> EntitlementFact {
445        EntitlementFact {
446            tenant: TenantId("team/sub".into()),
447            plan: "gunnar/team/sub/2026-09-03".into(),
448            state: State::Paid,
449            paid_until_unix_ms: Some(1_790_812_800_000),
450            caps: [("pack_bytes".to_string(), 10u64 << 30)].into(),
451            source: "payment:invoice:ocr-42".into(),
452            signature: vec![],
453            issued_unix_ms: None,
454            issued_signature: vec![],
455        }
456    }
457
458    /// A fact as it was signed before the field existed: V1 only.
459    fn v1(k: &SigningKey) -> EntitlementFact {
460        let mut f = bare();
461        f.signature = k.sign(&fact_message(&f)).to_bytes().to_vec();
462        f
463    }
464
465    /// A fact as monetize mints one now: both signatures, stamped.
466    fn stamped(k: &SigningKey, issued: u64) -> EntitlementFact {
467        let mut f = bare();
468        f.issued_unix_ms = Some(issued);
469        f.signature = k.sign(&fact_message(&f)).to_bytes().to_vec();
470        f.issued_signature = k.sign(&fact_message_issued(&f)).to_bytes().to_vec();
471        f
472    }
473
474    /// **The old form is frozen.** The bytes below are what `fact_message`
475    /// produced before the stamp existed, written out in full rather than
476    /// computed, so a future field that forgets to exclude itself fails HERE
477    /// and not on a customer's appliance.
478    const V1_BYTES: &str = concat!(
479        r#"{"caps":{"pack_bytes":10737418240},"paid_until_unix_ms":1790812800000,"#,
480        r#""plan":"gunnar/team/sub/2026-09-03","source":"payment:invoice:ocr-42","#,
481        r#""state":"Paid","tenant":"team/sub"}"#
482    );
483
484    #[test]
485    fn v1_of_a_stamped_fact_is_byte_for_byte_the_old_form() {
486        let k = key();
487        assert_eq!(String::from_utf8(fact_message(&bare())).unwrap(), V1_BYTES);
488        let stamped = stamped(&k, 1_789_000_000_000);
489        assert_eq!(
490            String::from_utf8(fact_message(&stamped)).unwrap(),
491            V1_BYTES,
492            "an appliance that has never heard of the stamp computes exactly this"
493        );
494        // …and therefore the V1 signature on a stamped fact is the one such an
495        // appliance checks, and it passes. This is the NEW-fact-meets-OLD-box
496        // direction, and it must never become a refusal.
497        assert!(check(&k.verifying_key(), V1_BYTES.as_bytes(), &stamped.signature).unwrap());
498    }
499
500    #[test]
501    fn the_v2_form_is_the_v1_form_plus_the_issue_time_and_nothing_else() {
502        let stamped = stamped(&key(), 1_789_000_000_000);
503        assert_eq!(
504            String::from_utf8(fact_message_issued(&stamped)).unwrap(),
505            concat!(
506                r#"{"caps":{"pack_bytes":10737418240},"issued_unix_ms":1789000000000,"#,
507                r#""paid_until_unix_ms":1790812800000,"plan":"gunnar/team/sub/2026-09-03","#,
508                r#""source":"payment:invoice:ocr-42","state":"Paid","tenant":"team/sub"}"#
509            )
510        );
511    }
512
513    #[test]
514    fn a_fact_signed_before_the_stamp_existed_still_verifies() {
515        let k = key();
516        verify_fact(&v1(&k), &k.verifying_key()).expect("OLD fact, NEW code: accepted");
517    }
518
519    #[test]
520    fn a_stamped_fact_verifies_both_halves() {
521        let k = key();
522        verify_fact(&stamped(&k, 1_789_000_000_000), &k.verifying_key()).unwrap();
523    }
524
525    /// **The issue time cannot be added, moved or bumped in transit.**
526    ///
527    /// Bumping is the one that would hurt most: a fact stamped far in the future
528    /// moves a product's high-water mark past every fact monetize will ever mint
529    /// for that tenant, so the tenant's plan can never be changed again. That is
530    /// a lasting denial of service, and it is refused here, at the signature.
531    #[test]
532    fn refuse_twin_an_unsigned_or_bumped_issue_time_is_refused_by_its_own_name() {
533        let k = key();
534        let who = || SignatureError::Issued("team/sub".into());
535
536        // Added to a fact that never had one.
537        let mut added = v1(&k);
538        added.issued_unix_ms = Some(u64::MAX);
539        assert_eq!(verify_fact(&added, &k.verifying_key()), Err(who()));
540
541        // Bumped on a genuinely stamped fact: the V1 signature still passes
542        // (it never covered the field), and the V2 one does not.
543        let mut bumped = stamped(&k, 1_789_000_000_000);
544        bumped.issued_unix_ms = Some(u64::MAX);
545        assert!(check(&k.verifying_key(), &fact_message(&bumped), &bumped.signature).unwrap());
546        assert_eq!(verify_fact(&bumped, &k.verifying_key()), Err(who()));
547
548        // Half a stamp, either way round.
549        let mut no_sig = stamped(&k, 1_789_000_000_000);
550        no_sig.issued_signature.clear();
551        assert_eq!(verify_fact(&no_sig, &k.verifying_key()), Err(who()));
552        let mut no_time = stamped(&k, 1_789_000_000_000);
553        no_time.issued_unix_ms = None;
554        assert_eq!(verify_fact(&no_time, &k.verifying_key()), Err(who()));
555
556        // A stamp lifted off another fact for the same tenant.
557        let mut moved = stamped(&k, 1_789_000_000_000);
558        moved.issued_signature = stamped(&k, 1_789_000_000_001).issued_signature;
559        assert_eq!(verify_fact(&moved, &k.verifying_key()), Err(who()));
560    }
561
562    /// A snapshot of stamped facts verifies whole, and one bumped stamp inside
563    /// it rejects the whole snapshot — the envelope vouches for the SET.
564    #[test]
565    fn a_snapshot_carries_stamped_facts_and_one_bad_stamp_rejects_the_set() {
566        let k = key();
567        let good = stamped(&k, 1_789_000_000_000);
568        let facts = vec![good.clone()];
569        let signature = k.sign(&snapshot_message(9, &facts)).to_bytes().to_vec();
570        let snap = Snapshot { issued_unix_ms: 9, facts, signature };
571        verify_snapshot(&snap, &k.verifying_key()).unwrap();
572
573        let mut bumped = good;
574        bumped.issued_unix_ms = Some(u64::MAX);
575        let facts = vec![bumped];
576        let signature = k.sign(&snapshot_message(9, &facts)).to_bytes().to_vec();
577        let snap = Snapshot { issued_unix_ms: 9, facts, signature };
578        assert_eq!(
579            verify_snapshot(&snap, &k.verifying_key()),
580            Err(SignatureError::Issued("team/sub".into()))
581        );
582    }
583}
584
585#[cfg(test)]
586mod estate_growth_tests {
587    use super::*;
588    use ed25519_dalek::{Signer as _, SigningKey};
589
590    fn growth(key: &SigningKey, gib: u64) -> Vec<u8> {
591        let mut g = EstateGrowth { v: 1, signer: GROWTH_SIGNER_SHOP.into(), estate: "acme".into(), main_data_gib_per_member: gib, until_unix_ms: 9_000, reference: "estate/acme/change-2".into(), issued_unix_ms: 7, signature: String::new() };
592        g.signature = base64_encode(&key.sign(&estate_growth_message(&g)).to_bytes());
593        serde_json::to_vec(&g).unwrap()
594    }
595
596    #[test]
597    fn a_shops_growth_go_ahead_verifies_under_its_key_alone() {
598        let shop = SigningKey::from_bytes(&[3; 32]);
599        let bytes = growth(&shop, 25);
600        let g = verify_estate_growth(&bytes, &shop.verifying_key()).expect("the shop's own go-ahead verifies");
601        assert_eq!((g.estate.as_str(), g.main_data_gib_per_member, g.until_unix_ms), ("acme", 25, 9_000));
602        let other = SigningKey::from_bytes(&[4; 32]);
603        assert_eq!(verify_estate_growth(&bytes, &other.verifying_key()), Err(SignatureError::Growth("acme".into())));
604        let raised = String::from_utf8(bytes).unwrap().replace("\"main_data_gib_per_member\":25", "\"main_data_gib_per_member\":250");
605        assert_eq!(verify_estate_growth(raised.as_bytes(), &shop.verifying_key()), Err(SignatureError::Growth("acme".into())), "a raised size is not the shop's");
606    }
607
608    #[test]
609    fn an_estates_call_is_its_keys_and_fresh() {
610        let acme = SigningKey::from_bytes(&[5; 32]);
611        let call = EstateCall { v: 1, call: "grow-set".into(), estate: "acme".into(), members: vec!["main-data-1".into()], gib: 25, at_unix_ms: 1_000_000, carries: String::new() };
612        // A call that carries nothing signs as it always did: `carries` is absent from its JSON.
613        assert!(!String::from_utf8(estate_call_message(&call)).unwrap().contains("carries"));
614        let recharge = EstateCall { call: "recharge".into(), carries: "estate = \"acme\"".into(), ..call.clone() };
615        assert!(String::from_utf8(estate_call_message(&recharge)).unwrap().contains("carries"), "the seed is signed");
616        let sig = acme.sign(&estate_call_message(&call)).to_bytes();
617        assert_eq!(verify_estate_call(&call, &sig, &acme.verifying_key(), 1_000_000 + 60_000), Ok(()));
618        assert!(verify_estate_call(&call, &sig, &acme.verifying_key(), 1_000_000 + CALL_FRESH_MS + 1).is_err(), "a stale call is refused");
619        let more = EstateCall { gib: 250, ..call.clone() };
620        assert!(verify_estate_call(&more, &sig, &acme.verifying_key(), 1_000_000).is_err(), "the signed size is the size");
621        let other = SigningKey::from_bytes(&[6; 32]);
622        assert!(verify_estate_call(&call, &sig, &other.verifying_key(), 1_000_000).is_err());
623    }
624}
625
626/// R5a: the canonical form's keys are written without an expect. Signed bytes
627/// must not move: each key is still the JSON string serde_json writes for it.
628#[cfg(test)]
629mod canonical_key_tests {
630    use super::*;
631
632    #[test]
633    fn a_key_is_written_exactly_as_serde_json_writes_it() {
634        for key in ["plain", "a\"quote", "back\\slash", "ü-ß", "tab\tnew\nline", "\u{1}ctl", "😀"] {
635            let mut out = String::new();
636            canonical_json(&serde_json::json!({ key: 1 }), &mut out);
637            assert_eq!(out, format!("{{{}:1}}", serde_json::to_string(key).unwrap()), "{key:?}");
638        }
639    }
640}