1use ed25519_dalek::{Signature, Verifier, VerifyingKey};
13use monetize_product::EntitlementFact;
14use serde_json::Value;
15
16#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
19pub struct Snapshot {
20 pub issued_unix_ms: u64,
22 pub facts: Vec<EntitlementFact>,
23 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 #[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 #[error("the actor ticket was refused: {0}")]
48 Ticket(String),
49 #[error("the shop's growth go-ahead for {0} does not verify")]
51 Growth(String),
52 #[error("the call is not signed by {0}'s key")]
54 Call(String),
55}
56
57pub 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 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
89fn 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
103pub fn fact_message(fact: &EntitlementFact) -> Vec<u8> {
112 fact_form(fact, &["signature", "issued_unix_ms", "issued_signature"])
113}
114
115pub fn fact_message_issued(fact: &EntitlementFact) -> Vec<u8> {
119 fact_form(fact, &["signature", "issued_signature"])
120}
121
122pub 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
136pub 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 (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
168pub 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#[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
205pub const GO_AHEAD_SIGNER_MONETIZE: &str = "monetize";
207
208pub 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
218pub 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
235pub 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
251pub 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#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
297pub struct EstateGrowth {
298 pub v: u32,
299 pub signer: String,
300 pub estate: String,
302 pub main_data_gib_per_member: u64,
304 pub until_unix_ms: i64,
306 pub reference: String,
308 pub issued_unix_ms: i64,
309 #[serde(default)]
310 pub signature: String,
311}
312
313pub const GROWTH_SIGNER_SHOP: &str = "shop";
315
316pub 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
325pub 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#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
347pub struct EstateCall {
348 pub v: u32,
349 pub call: String,
351 pub estate: String,
352 #[serde(default)]
354 pub members: Vec<String>,
355 #[serde(default)]
357 pub gib: u64,
358 pub at_unix_ms: i64,
359 #[serde(default, skip_serializing_if = "String::is_empty")]
363 pub carries: String,
364}
365
366pub const CALL_FRESH_MS: i64 = 5 * 60 * 1000;
368
369pub 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
377pub 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#[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 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 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 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 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 #[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 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 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 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 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 #[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 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#[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}