1use ed25519_dalek::{Signature, VerifyingKey};
18use serde::{Deserialize, Serialize};
19
20pub const ORDER_DOMAIN: &[u8] = b"nordisk-order-v1\0";
22pub const CALL_DOMAIN: &[u8] = b"nordisk-fleet-call-v1\0";
24pub const CALL_FRESH_MS: u64 = 5 * 60 * 1000;
26pub const FUTURE_SKEW_MS: u64 = 5 * 60 * 1000;
29pub const LONG_POLL_MAX_MS: u64 = 25_000;
31pub use monetize_product::KEEP_BOOT_WITHIN;
36
37pub mod kind {
41 pub const RECHARGE_FROM_TWIN: &str = "recharge_from_twin";
44 pub const FENCE_PRIMARY: &str = "fence_primary";
46 pub const ADOPT_TWIN: &str = "adopt_twin";
48 pub const REVIEW_TWIN: &str = "review_twin";
50 pub const RESTATE_ENTITLEMENTS: &str = "restate_entitlements";
56 pub use monetize_product::pull::order::GROW_GO_AHEAD;
64 pub use monetize_product::pull::order::SIGN_IN_AGAIN;
72
73 pub const GUARDIAN: [&str; 5] = [RECHARGE_FROM_TWIN, FENCE_PRIMARY, ADOPT_TWIN, REVIEW_TWIN, RESTATE_ENTITLEMENTS];
76}
77
78pub mod ask {
80 pub use monetize_product::pull::ask::GROW_SET;
84 pub const UPGRADE: &str = "upgrade";
88 pub const RECHARGE: &str = "recharge";
92 pub const MATERIAL: &str = "material";
95 pub const UPDATE_TAKE: &str = "update_take";
98 pub const UPDATE_REPORT: &str = "update_report";
101 pub const FRONT: [&str; 5] = [UPGRADE, RECHARGE, MATERIAL, UPDATE_TAKE, UPDATE_REPORT];
104
105 pub fn is_front(kind: &str) -> bool {
107 FRONT.contains(&kind)
108 }
109}
110
111pub mod call {
113 pub const REPORT: &str = "report";
114 pub const ORDERS: &str = "orders";
115 pub const ASK: &str = "ask";
116}
117
118#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
120pub struct SignedOrder {
121 pub seq: u64,
123 pub product: String,
124 pub instance: String,
125 pub role: String,
128 pub kind: String,
129 pub body: String,
131 pub issued_unix_ms: u64,
132 pub not_after_unix_ms: u64,
133 #[serde(default)]
135 pub signature: Vec<u8>,
136}
137
138#[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
158pub 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
171pub 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#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
198pub struct FleetCall {
199 pub product: String,
200 pub instance: String,
201 pub role: String,
202 pub call: String,
204 pub body: String,
206 pub at_unix_ms: u64,
207}
208
209#[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
218pub 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
229pub 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
240fn 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#[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 pub receipt_json: String,
254}
255
256#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
258#[serde(default)]
259pub struct Report {
260 pub build: String,
261 pub health: std::collections::BTreeMap<String, String>,
265 pub acked: Vec<OrderAck>,
266 pub twin_review: Option<String>,
269 pub product: Option<String>,
273 pub measured_unix_ms: u64,
274}
275
276#[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#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
286#[serde(default)]
287pub struct GrowGoAhead {
288 pub go_ahead: String,
290 pub data_gib: u64,
292}
293
294#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
296#[serde(default)]
297pub struct SignInAgain {
298 pub account: String,
300 pub to: String,
302 pub host: String,
304 pub ttl_secs: u64,
306}
307
308#[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#[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#[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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}