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}
50
51pub fn canonical_json(value: &Value, out: &mut String) {
53 match value {
54 Value::Object(map) => {
55 let mut keys: Vec<&String> = map.keys().collect();
56 keys.sort();
57 out.push('{');
58 for (i, k) in keys.iter().enumerate() {
59 if i > 0 {
60 out.push(',');
61 }
62 out.push_str(&serde_json::to_string(k).expect("string"));
63 out.push(':');
64 canonical_json(&map[*k], out);
65 }
66 out.push('}');
67 }
68 Value::Array(items) => {
69 out.push('[');
70 for (i, v) in items.iter().enumerate() {
71 if i > 0 {
72 out.push(',');
73 }
74 canonical_json(v, out);
75 }
76 out.push(']');
77 }
78 other => out.push_str(&other.to_string()),
79 }
80}
81
82fn fact_form(fact: &EntitlementFact, drop: &[&str]) -> Vec<u8> {
86 let mut v = serde_json::to_value(fact).expect("fact serializes");
87 let object = v.as_object_mut().expect("fact is an object");
88 for key in drop {
89 object.remove(*key);
90 }
91 let mut s = String::new();
92 canonical_json(&v, &mut s);
93 s.into_bytes()
94}
95
96pub fn fact_message(fact: &EntitlementFact) -> Vec<u8> {
105 fact_form(fact, &["signature", "issued_unix_ms", "issued_signature"])
106}
107
108pub fn fact_message_issued(fact: &EntitlementFact) -> Vec<u8> {
112 fact_form(fact, &["signature", "issued_signature"])
113}
114
115pub fn snapshot_message(issued_unix_ms: u64, facts: &[EntitlementFact]) -> Vec<u8> {
118 let v = serde_json::json!({ "issued_unix_ms": issued_unix_ms, "facts": facts });
119 let mut s = String::new();
120 canonical_json(&v, &mut s);
121 s.into_bytes()
122}
123
124pub(crate) fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> Result<bool, SignatureError> {
125 let sig = Signature::from_slice(sig).map_err(|_| SignatureError::Malformed)?;
126 Ok(key.verify(msg, &sig).is_ok())
127}
128
129pub fn verify_fact(fact: &EntitlementFact, key: &VerifyingKey) -> Result<(), SignatureError> {
144 if !check(key, &fact_message(fact), &fact.signature)? {
145 return Err(SignatureError::Fact(fact.tenant.0.clone()));
146 }
147 match (fact.issued_unix_ms, fact.issued_signature.is_empty()) {
148 (None, true) => Ok(()),
150 (Some(_), false) => {
151 if check(key, &fact_message_issued(fact), &fact.issued_signature)? {
152 Ok(())
153 } else {
154 Err(SignatureError::Issued(fact.tenant.0.clone()))
155 }
156 }
157 (None, false) | (Some(_), true) => Err(SignatureError::Issued(fact.tenant.0.clone())),
158 }
159}
160
161pub fn verify_snapshot(snap: &Snapshot, key: &VerifyingKey) -> Result<(), SignatureError> {
163 if !check(key, &snapshot_message(snap.issued_unix_ms, &snap.facts), &snap.signature)? {
164 return Err(SignatureError::Envelope);
165 }
166 snap.facts.iter().try_for_each(|f| verify_fact(f, key))
167}
168
169#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
188pub struct GoAhead {
189 pub v: u32,
190 pub signer: String,
191 pub target_sectors: u64,
192 pub nonce: String,
193 pub issued_unix_ms: i64,
194 #[serde(default)]
195 pub signature: String,
196}
197
198pub const GO_AHEAD_SIGNER_MONETIZE: &str = "monetize";
200
201pub fn go_ahead_message(go: &GoAhead) -> Vec<u8> {
204 let mut v = serde_json::to_value(go).expect("go-ahead serializes");
205 v.as_object_mut().expect("go-ahead is an object").remove("signature");
206 let mut s = String::new();
207 canonical_json(&v, &mut s);
208 s.into_bytes()
209}
210
211pub fn verify_go_ahead(bytes: &[u8], key: &VerifyingKey) -> Result<GoAhead, SignatureError> {
214 let go: GoAhead = serde_json::from_slice(bytes).map_err(|_| SignatureError::Malformed)?;
215 if go.v != 1 || go.signer != GO_AHEAD_SIGNER_MONETIZE || go.nonce.trim().is_empty() {
216 return Err(SignatureError::Malformed);
217 }
218 let sig = base64_decode(&go.signature).ok_or(SignatureError::Malformed)?;
219 if check(key, &go_ahead_message(&go), &sig)? {
220 Ok(go)
221 } else {
222 Err(SignatureError::GoAhead(go.nonce.clone()))
223 }
224}
225
226const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
227
228pub fn base64_encode(bytes: &[u8]) -> String {
232 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
233 for chunk in bytes.chunks(3) {
234 let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
235 let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
236 out.push(B64[(n >> 18) as usize & 63] as char);
237 out.push(B64[(n >> 12) as usize & 63] as char);
238 out.push(if chunk.len() > 1 { B64[(n >> 6) as usize & 63] as char } else { '=' });
239 out.push(if chunk.len() > 2 { B64[n as usize & 63] as char } else { '=' });
240 }
241 out
242}
243
244pub fn base64_decode(text: &str) -> Option<Vec<u8>> {
246 let text = text.trim();
247 if text.len() % 4 != 0 {
248 return None;
249 }
250 let val = |c: u8| B64.iter().position(|b| *b == c).map(|p| p as u32);
251 let mut out = Vec::with_capacity(text.len() / 4 * 3);
252 for chunk in text.as_bytes().chunks(4) {
253 let pad = chunk.iter().rev().take_while(|c| **c == b'=').count();
254 if pad > 2 || chunk[..4 - pad].iter().any(|c| *c == b'=') {
255 return None;
256 }
257 let mut n = 0u32;
258 for (i, c) in chunk.iter().enumerate() {
259 let v = if i >= 4 - pad { 0 } else { val(*c)? };
260 n = (n << 6) | v;
261 }
262 out.push((n >> 16) as u8);
263 if pad < 2 {
264 out.push((n >> 8) as u8);
265 }
266 if pad < 1 {
267 out.push(n as u8);
268 }
269 }
270 Some(out)
271}
272
273#[cfg(test)]
274mod go_ahead_tests {
275 use super::*;
276
277 #[test]
278 fn base64_round_trips_every_padding_shape_and_refuses_junk() {
279 for n in 0..10 {
280 let bytes: Vec<u8> = (0..n).map(|i| (i * 37 + 11) as u8).collect();
281 let enc = base64_encode(&bytes);
282 assert_eq!(enc.len() % 4, 0);
283 assert_eq!(base64_decode(&enc).unwrap(), bytes, "{enc}");
284 }
285 assert_eq!(base64_encode(b"Man"), "TWFu");
286 assert_eq!(base64_encode(b"Ma"), "TWE=");
287 assert_eq!(base64_encode(b"M"), "TQ==");
288 assert_eq!(base64_decode("TQ="), None);
289 assert_eq!(base64_decode("T@=="), None);
290 assert_eq!(base64_decode("TQ=x"), None);
291 }
292
293 #[test]
294 fn the_go_ahead_message_is_canonical_and_excludes_the_signature() {
295 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() };
296 let msg = String::from_utf8(go_ahead_message(&go)).unwrap();
297 assert_eq!(msg, r#"{"issued_unix_ms":1800000000000,"nonce":"n-1","signer":"monetize","target_sectors":134217728,"v":1}"#);
298 }
299}
300
301#[cfg(test)]
318mod stamp_tests {
319 use super::*;
320 use ed25519_dalek::{Signer as _, SigningKey};
321 use monetize_product::{State, TenantId};
322
323 fn key() -> SigningKey {
324 SigningKey::from_bytes(&[3u8; 32])
325 }
326
327 fn bare() -> EntitlementFact {
328 EntitlementFact {
329 tenant: TenantId("team/sub".into()),
330 plan: "gunnar/team/sub/2026-09-03".into(),
331 state: State::Paid,
332 paid_until_unix_ms: Some(1_790_812_800_000),
333 caps: [("pack_bytes".to_string(), 10u64 << 30)].into(),
334 source: "payment:invoice:ocr-42".into(),
335 signature: vec![],
336 issued_unix_ms: None,
337 issued_signature: vec![],
338 }
339 }
340
341 fn v1(k: &SigningKey) -> EntitlementFact {
343 let mut f = bare();
344 f.signature = k.sign(&fact_message(&f)).to_bytes().to_vec();
345 f
346 }
347
348 fn stamped(k: &SigningKey, issued: u64) -> EntitlementFact {
350 let mut f = bare();
351 f.issued_unix_ms = Some(issued);
352 f.signature = k.sign(&fact_message(&f)).to_bytes().to_vec();
353 f.issued_signature = k.sign(&fact_message_issued(&f)).to_bytes().to_vec();
354 f
355 }
356
357 const V1_BYTES: &str = concat!(
362 r#"{"caps":{"pack_bytes":10737418240},"paid_until_unix_ms":1790812800000,"#,
363 r#""plan":"gunnar/team/sub/2026-09-03","source":"payment:invoice:ocr-42","#,
364 r#""state":"Paid","tenant":"team/sub"}"#
365 );
366
367 #[test]
368 fn v1_of_a_stamped_fact_is_byte_for_byte_the_old_form() {
369 let k = key();
370 assert_eq!(String::from_utf8(fact_message(&bare())).unwrap(), V1_BYTES);
371 let stamped = stamped(&k, 1_789_000_000_000);
372 assert_eq!(
373 String::from_utf8(fact_message(&stamped)).unwrap(),
374 V1_BYTES,
375 "an appliance that has never heard of the stamp computes exactly this"
376 );
377 assert!(check(&k.verifying_key(), V1_BYTES.as_bytes(), &stamped.signature).unwrap());
381 }
382
383 #[test]
384 fn the_v2_form_is_the_v1_form_plus_the_issue_time_and_nothing_else() {
385 let stamped = stamped(&key(), 1_789_000_000_000);
386 assert_eq!(
387 String::from_utf8(fact_message_issued(&stamped)).unwrap(),
388 concat!(
389 r#"{"caps":{"pack_bytes":10737418240},"issued_unix_ms":1789000000000,"#,
390 r#""paid_until_unix_ms":1790812800000,"plan":"gunnar/team/sub/2026-09-03","#,
391 r#""source":"payment:invoice:ocr-42","state":"Paid","tenant":"team/sub"}"#
392 )
393 );
394 }
395
396 #[test]
397 fn a_fact_signed_before_the_stamp_existed_still_verifies() {
398 let k = key();
399 verify_fact(&v1(&k), &k.verifying_key()).expect("OLD fact, NEW code: accepted");
400 }
401
402 #[test]
403 fn a_stamped_fact_verifies_both_halves() {
404 let k = key();
405 verify_fact(&stamped(&k, 1_789_000_000_000), &k.verifying_key()).unwrap();
406 }
407
408 #[test]
415 fn refuse_twin_an_unsigned_or_bumped_issue_time_is_refused_by_its_own_name() {
416 let k = key();
417 let who = || SignatureError::Issued("team/sub".into());
418
419 let mut added = v1(&k);
421 added.issued_unix_ms = Some(u64::MAX);
422 assert_eq!(verify_fact(&added, &k.verifying_key()), Err(who()));
423
424 let mut bumped = stamped(&k, 1_789_000_000_000);
427 bumped.issued_unix_ms = Some(u64::MAX);
428 assert!(check(&k.verifying_key(), &fact_message(&bumped), &bumped.signature).unwrap());
429 assert_eq!(verify_fact(&bumped, &k.verifying_key()), Err(who()));
430
431 let mut no_sig = stamped(&k, 1_789_000_000_000);
433 no_sig.issued_signature.clear();
434 assert_eq!(verify_fact(&no_sig, &k.verifying_key()), Err(who()));
435 let mut no_time = stamped(&k, 1_789_000_000_000);
436 no_time.issued_unix_ms = None;
437 assert_eq!(verify_fact(&no_time, &k.verifying_key()), Err(who()));
438
439 let mut moved = stamped(&k, 1_789_000_000_000);
441 moved.issued_signature = stamped(&k, 1_789_000_000_001).issued_signature;
442 assert_eq!(verify_fact(&moved, &k.verifying_key()), Err(who()));
443 }
444
445 #[test]
448 fn a_snapshot_carries_stamped_facts_and_one_bad_stamp_rejects_the_set() {
449 let k = key();
450 let good = stamped(&k, 1_789_000_000_000);
451 let facts = vec![good.clone()];
452 let signature = k.sign(&snapshot_message(9, &facts)).to_bytes().to_vec();
453 let snap = Snapshot { issued_unix_ms: 9, facts, signature };
454 verify_snapshot(&snap, &k.verifying_key()).unwrap();
455
456 let mut bumped = good;
457 bumped.issued_unix_ms = Some(u64::MAX);
458 let facts = vec![bumped];
459 let signature = k.sign(&snapshot_message(9, &facts)).to_bytes().to_vec();
460 let snap = Snapshot { issued_unix_ms: 9, facts, signature };
461 assert_eq!(
462 verify_snapshot(&snap, &k.verifying_key()),
463 Err(SignatureError::Issued("team/sub".into()))
464 );
465 }
466}