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;
36pub const UPGRADE_RUNNING: &str = "is already running here";
40
41pub mod kind {
45 pub const RECHARGE_FROM_TWIN: &str = "recharge_from_twin";
48 pub const FENCE_PRIMARY: &str = "fence_primary";
50 pub const ADOPT_TWIN: &str = "adopt_twin";
52 pub const REVIEW_TWIN: &str = "review_twin";
54 pub const RESTATE_ENTITLEMENTS: &str = "restate_entitlements";
60 pub use monetize_product::pull::order::GROW_GO_AHEAD;
68 pub use monetize_product::pull::order::SIGN_IN_AGAIN;
76 pub use monetize_product::pull::order::REFEED;
82
83 pub const GUARDIAN: [&str; 5] = [RECHARGE_FROM_TWIN, FENCE_PRIMARY, ADOPT_TWIN, REVIEW_TWIN, RESTATE_ENTITLEMENTS];
86}
87
88pub use monetize_product::pull::{Fetch, FetchAsk, ProductOrder};
91
92pub mod ask {
93 pub use monetize_product::pull::ask::GROW_SET;
97 pub use monetize_product::pull::ask::FETCH;
100 pub const UPGRADE: &str = "upgrade";
104 pub const RECHARGE: &str = "recharge";
108 pub const MATERIAL: &str = "material";
111 pub const UPDATE_TAKE: &str = "update_take";
114 pub const UPDATE_REPORT: &str = "update_report";
117 pub const FRONT: [&str; 5] = [UPGRADE, RECHARGE, MATERIAL, UPDATE_TAKE, UPDATE_REPORT];
120
121 pub fn is_front(kind: &str) -> bool {
123 FRONT.contains(&kind)
124 }
125}
126
127pub mod call {
129 pub const REPORT: &str = "report";
130 pub const ORDERS: &str = "orders";
131 pub const ASK: &str = "ask";
132}
133
134#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
136pub struct SignedOrder {
137 pub seq: u64,
139 pub product: String,
140 pub instance: String,
141 pub role: String,
144 pub kind: String,
145 pub body: String,
147 pub issued_unix_ms: u64,
148 pub not_after_unix_ms: u64,
149 #[serde(default)]
151 pub signature: Vec<u8>,
152}
153
154#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
156pub enum OrderRefusal {
157 #[error("the order is not signed by monetize")]
158 Signature,
159 #[error("order {seq} is not newer than {last}")]
160 Replayed { seq: u64, last: u64 },
161 #[error("order {0} has expired")]
162 Expired(u64),
163 #[error("order {0} is for {1}, not this box")]
164 NotForMe(u64, String),
165 #[error("order {0} is dated more than five minutes ahead of this box's clock")]
166 Ahead(u64),
167}
168
169fn field(out: &mut Vec<u8>, bytes: &[u8]) {
170 out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
171 out.extend_from_slice(bytes);
172}
173
174pub fn order_message(o: &SignedOrder) -> Vec<u8> {
177 let mut out = ORDER_DOMAIN.to_vec();
178 out.extend_from_slice(&o.seq.to_be_bytes());
179 out.extend_from_slice(&o.issued_unix_ms.to_be_bytes());
180 out.extend_from_slice(&o.not_after_unix_ms.to_be_bytes());
181 for f in [&o.product, &o.instance, &o.role, &o.kind, &o.body] {
182 field(&mut out, f.as_bytes());
183 }
184 out
185}
186
187pub fn verify_order(o: &SignedOrder, key: &VerifyingKey, me: (&str, &str, &str), now_unix_ms: u64, last_seq: u64) -> Result<(), OrderRefusal> {
193 let (product, instance, role) = me;
194 if !check(key, &order_message(o), &o.signature) {
195 return Err(OrderRefusal::Signature);
196 }
197 if o.product != product || o.instance != instance || o.role != role {
198 return Err(OrderRefusal::NotForMe(o.seq, o.role.clone()));
199 }
200 if o.seq <= last_seq {
201 return Err(OrderRefusal::Replayed { seq: o.seq, last: last_seq });
202 }
203 if o.not_after_unix_ms != 0 && now_unix_ms > o.not_after_unix_ms {
204 return Err(OrderRefusal::Expired(o.seq));
205 }
206 if o.issued_unix_ms > now_unix_ms.saturating_add(FUTURE_SKEW_MS) {
207 return Err(OrderRefusal::Ahead(o.seq));
208 }
209 Ok(())
210}
211
212#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
214pub struct FleetCall {
215 pub product: String,
216 pub instance: String,
217 pub role: String,
218 pub call: String,
220 pub body: String,
222 pub at_unix_ms: u64,
223}
224
225#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
227pub enum CallRefusal {
228 #[error("the call is not signed by the box's key")]
229 Signature,
230 #[error("the call's time is too far from monetize's clock")]
231 Stale,
232}
233
234pub fn fleet_call_message(c: &FleetCall) -> Vec<u8> {
237 let mut out = CALL_DOMAIN.to_vec();
238 out.extend_from_slice(&c.at_unix_ms.to_be_bytes());
239 for f in [&c.product, &c.instance, &c.role, &c.call, &c.body] {
240 field(&mut out, f.as_bytes());
241 }
242 out
243}
244
245pub fn verify_fleet_call(c: &FleetCall, signature: &[u8], key: &VerifyingKey, now_unix_ms: u64) -> Result<(), CallRefusal> {
247 if !check(key, &fleet_call_message(c), signature) {
248 return Err(CallRefusal::Signature);
249 }
250 if c.at_unix_ms.abs_diff(now_unix_ms) > CALL_FRESH_MS {
251 return Err(CallRefusal::Stale);
252 }
253 Ok(())
254}
255
256fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> bool {
258 Signature::from_slice(sig).map(|s| key.verify_strict(msg, &s).is_ok()).unwrap_or(false)
259}
260
261#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
263#[serde(default)]
264pub struct OrderAck {
265 pub seq: u64,
266 pub ok: bool,
267 pub detail: String,
268 pub receipt_json: String,
270}
271
272#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
274#[serde(default)]
275pub struct Report {
276 pub build: String,
277 pub health: std::collections::BTreeMap<String, String>,
281 pub acked: Vec<OrderAck>,
282 pub twin_review: Option<String>,
285 pub product: Option<String>,
289 pub measured_unix_ms: u64,
290}
291
292#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
294#[serde(default)]
295pub struct OrdersAfter {
296 pub after: u64,
297 pub wait_ms: u64,
298}
299
300#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
302#[serde(default)]
303pub struct GrowGoAhead {
304 pub go_ahead: String,
306 pub data_gib: u64,
308}
309
310#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
312#[serde(default)]
313pub struct SignInAgain {
314 pub account: String,
316 pub to: String,
318 pub host: String,
320 pub ttl_secs: u64,
322}
323
324pub mod refeed {
330 pub const SMTP_URL: &str = "secret.smtp_url";
331 pub const SMTP_FROM: &str = "secret.smtp_from";
332 pub const SMTP_CA: &str = "secret.smtp_ca";
333 pub const PUBLIC_REPOSITORIES: &str = "public_repositories";
334 pub const PUBLIC_SITE: &str = "console_public_site";
337 pub const KNOWN: [&str; 5] = [SMTP_URL, SMTP_FROM, SMTP_CA, PUBLIC_REPOSITORIES, PUBLIC_SITE];
339 pub fn is_secret(key: &str) -> bool {
341 key.starts_with("secret.")
342 }
343}
344
345#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
348#[serde(default)]
349pub struct Refeed {
350 pub keys: std::collections::BTreeMap<String, String>,
351}
352
353impl std::fmt::Debug for Refeed {
354 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355 f.debug_struct("Refeed").field("keys", &self.keys.keys().collect::<Vec<_>>()).finish()
356 }
357}
358
359#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
362#[serde(default)]
363pub struct GrowSetAsk {
364 pub members: Vec<String>,
365 pub gib: u64,
366}
367
368#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
374#[serde(default)]
375pub struct FrontAsk {
376 #[serde(skip_serializing_if = "String::is_empty")]
377 pub role: String,
378 #[serde(skip_serializing_if = "std::ops::Not::not")]
379 pub done: bool,
380 #[serde(skip_serializing_if = "String::is_empty")]
381 pub seed: String,
382 #[serde(skip_serializing_if = "String::is_empty")]
383 pub carries: String,
384}
385
386#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
388#[serde(default)]
389pub struct Ask {
390 pub kind: String,
391 pub body: serde_json::Value,
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use ed25519_dalek::{Signer as _, SigningKey};
398
399 #[test]
402 fn the_core_kinds_here_are_monetize_products_one_list() {
403 use monetize_product::pull;
404 assert_eq!(kind::GROW_GO_AHEAD, pull::order::GROW_GO_AHEAD);
405 assert_eq!(kind::SIGN_IN_AGAIN, pull::order::SIGN_IN_AGAIN);
406 assert_eq!(kind::REFEED, pull::order::REFEED);
407 assert_eq!(ask::GROW_SET, pull::ask::GROW_SET);
408 for k in kind::GUARDIAN {
409 assert!(!pull::is_core(k), "{k} is a guardian kind, not a core one");
410 }
411 assert_eq!(kind::GUARDIAN, [kind::RECHARGE_FROM_TWIN, kind::FENCE_PRIMARY, kind::ADOPT_TWIN, kind::REVIEW_TWIN, kind::RESTATE_ENTITLEMENTS]);
412 }
413
414 #[test]
418 fn the_fronts_asks_are_the_five_soldestate_calls_and_their_body_round_trips() {
419 assert_eq!(ask::FRONT, ["upgrade", "recharge", "material", "update_take", "update_report"]);
420 for k in ask::FRONT {
421 assert!(ask::is_front(k));
422 }
423 for k in [ask::GROW_SET, kind::RECHARGE_FROM_TWIN, kind::REVIEW_TWIN, "new_twin", "new_server", ""] {
424 assert!(!ask::is_front(k), "{k}");
425 }
426 for k in ask::FRONT {
428 assert!(!monetize_product::pull::is_core(k), "{k}");
429 }
430 let a = FrontAsk { role: "main".into(), seed: "SEED".into(), ..Default::default() };
431 let text = serde_json::to_string(&Ask { kind: ask::RECHARGE.into(), body: serde_json::to_value(&a).unwrap() }).unwrap();
432 assert_eq!(text, r#"{"kind":"recharge","body":{"role":"main","seed":"SEED"}}"#);
433 let back: Ask = serde_json::from_str(&text).unwrap();
434 assert_eq!(serde_json::from_value::<FrontAsk>(back.body).unwrap(), a);
435 assert_eq!(serde_json::from_value::<FrontAsk>(serde_json::json!({})).unwrap(), FrontAsk::default(), "an empty body is the update take's");
436 }
437
438 #[test]
441 fn a_front_waits_on_the_shops_whole_keep_boot() {
442 assert_eq!(KEEP_BOOT_WITHIN, std::time::Duration::from_secs(2 * 3600));
443 assert_eq!(KEEP_BOOT_WITHIN, monetize_product::KEEP_BOOT_WITHIN);
444 }
445
446 const GOLDEN_MESSAGE_HEX: &str = "6e6f726469736b2d6f726465722d7631000000000000000003000001a3185c5000000001a3186a0ba00000000667756e6e61720000000461636d65000000047477696e0000001272656368617267655f66726f6d5f7477696e000000517b226163636570745f6c6f7374223a66616c73652c2265706f6368223a352c226d61696e223a2231302e362e302e31303a3530303531222c227477696e223a2231302e362e302e31323a3530303531227d";
447 const GOLDEN_SIGNATURE_HEX: &str = "d4a3233158d9a3fbfa42f9aa80fd2ca420666ec2fe9f7fbf1a61abab533c09f6dab60c67367bc771a1d819d1e6bed28c59d6d56187a4748ba9b4b9988d13d50a";
448
449 fn key() -> SigningKey {
450 SigningKey::from_bytes(&[7u8; 32])
451 }
452
453 fn order() -> SignedOrder {
454 SignedOrder {
455 seq: 3,
456 product: "gunnar".into(),
457 instance: "acme".into(),
458 role: "twin".into(),
459 kind: kind::RECHARGE_FROM_TWIN.into(),
460 body: r#"{"accept_lost":false,"epoch":5,"main":"10.6.0.10:50051","twin":"10.6.0.12:50051"}"#.into(),
461 issued_unix_ms: 1_800_000_000_000,
462 not_after_unix_ms: 1_800_000_900_000,
463 signature: Vec::new(),
464 }
465 }
466
467 fn signed() -> SignedOrder {
468 let mut o = order();
469 o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
470 o
471 }
472
473 #[test]
475 fn the_order_message_is_the_agreed_binary_layout() {
476 let m = order_message(&order());
477 assert!(m.starts_with(ORDER_DOMAIN));
478 let mut want = ORDER_DOMAIN.to_vec();
479 want.extend_from_slice(&3u64.to_be_bytes());
480 want.extend_from_slice(&1_800_000_000_000u64.to_be_bytes());
481 want.extend_from_slice(&1_800_000_900_000u64.to_be_bytes());
482 for f in ["gunnar", "acme", "twin", kind::RECHARGE_FROM_TWIN, &order().body] {
483 want.extend_from_slice(&(f.len() as u32).to_be_bytes());
484 want.extend_from_slice(f.as_bytes());
485 }
486 assert_eq!(m, want);
487 let sig = key().sign(&m).to_bytes();
488 let hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();
489 assert_eq!(hex, GOLDEN_SIGNATURE_HEX, "the golden vector moved: tell gunnar_repair::order");
490 let mhex: String = m.iter().map(|b| format!("{b:02x}")).collect();
491 assert_eq!(mhex, GOLDEN_MESSAGE_HEX);
492 }
493
494 #[test]
495 fn a_box_runs_only_a_fresh_order_signed_for_it() {
496 let pk = key().verifying_key();
497 let me = ("gunnar", "acme", "twin");
498 let now = 1_800_000_100_000;
499 assert_eq!(verify_order(&signed(), &pk, me, now, 2), Ok(()));
500 assert_eq!(verify_order(&signed(), &pk, me, now, 3), Err(OrderRefusal::Replayed { seq: 3, last: 3 }));
501 assert_eq!(verify_order(&signed(), &pk, me, 1_800_000_900_001, 0), Err(OrderRefusal::Expired(3)));
502 assert_eq!(verify_order(&signed(), &pk, ("gunnar", "acme", "main"), now, 0), Err(OrderRefusal::NotForMe(3, "twin".into())));
503 let mut forged = signed();
504 forged.role = "main".into();
505 assert_eq!(verify_order(&forged, &pk, ("gunnar", "acme", "main"), now, 0), Err(OrderRefusal::Signature));
506 let other = SigningKey::from_bytes(&[9u8; 32]).verifying_key();
507 assert_eq!(verify_order(&signed(), &other, me, now, 0), Err(OrderRefusal::Signature));
508 }
509
510 #[test]
515 fn a_weak_key_with_the_all_zero_signature_signs_no_order() {
516 let (weak, zero) = weak_key_and_zero_signature();
517 let mut o = order();
518 o.signature = zero;
519 let now = 1_800_000_100_000;
520 assert_eq!(verify_order(&o, &weak, ("gunnar", "acme", "twin"), now, 0), Err(OrderRefusal::Signature), "a weak key verifies nothing");
521 assert_eq!(verify_order(&signed(), &key().verifying_key(), ("gunnar", "acme", "twin"), now, 0), Ok(()));
522 }
523
524 #[test]
526 fn a_weak_box_key_with_the_all_zero_signature_signs_no_call() {
527 let (weak, zero) = weak_key_and_zero_signature();
528 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 };
529 assert_eq!(verify_fleet_call(&c, &zero, &weak, 1_800_000_000_000), Err(CallRefusal::Signature));
530 }
531
532 fn weak_key_and_zero_signature() -> (VerifyingKey, Vec<u8>) {
535 let mut identity = [0u8; 32];
536 identity[0] = 1;
537 let weak = VerifyingKey::from_bytes(&identity).expect("the identity point decompresses");
538 let mut zero = vec![0u8; 64];
539 zero[0] = 1;
540 (weak, zero)
541 }
542
543 #[test]
546 fn a_tampered_order_is_not_monetizes() {
547 let pk = key().verifying_key();
548 let me = ("gunnar", "acme", "twin");
549 let now = 1_800_000_100_000;
550 type Tamper = Box<dyn Fn(&mut SignedOrder)>;
551 let tampered: Vec<(&str, Tamper)> = vec![
552 ("seq", Box::new(|o| o.seq += 1)),
553 ("kind", Box::new(|o| o.kind = kind::FENCE_PRIMARY.into())),
554 ("body", Box::new(|o| o.body = "{}".into())),
555 ("issued", Box::new(|o| o.issued_unix_ms -= 1)),
556 ("not_after", Box::new(|o| o.not_after_unix_ms += 1)),
557 ("bit", Box::new(|o| o.signature[5] ^= 1)),
558 ("short", Box::new(|o| o.signature.truncate(63))),
559 ("none", Box::new(|o| o.signature.clear())),
560 ];
561 for (what, f) in tampered {
562 let mut o = signed();
563 f(&mut o);
564 assert_eq!(verify_order(&o, &pk, me, now, 0), Err(OrderRefusal::Signature), "{what}");
565 }
566 }
567
568 #[test]
571 fn a_forged_order_for_another_box_is_forged_not_misaddressed() {
572 let mut o = signed();
573 o.role = "main".into();
574 assert_eq!(verify_order(&o, &key().verifying_key(), ("gunnar", "acme", "twin"), 1_800_000_100_000, 0), Err(OrderRefusal::Signature));
575 }
576
577 #[test]
579 fn an_order_with_no_window_does_not_expire() {
580 let mut o = order();
581 o.not_after_unix_ms = 0;
582 o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
583 assert_eq!(verify_order(&o, &key().verifying_key(), ("gunnar", "acme", "twin"), 1_900_000_000_000, 0), Ok(()));
584 }
585
586 #[test]
589 fn an_order_dated_ahead_of_the_box_clock_is_refused() {
590 let pk = key().verifying_key();
591 let me = ("gunnar", "acme", "twin");
592 let issued = order().issued_unix_ms;
593 assert_eq!(verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS, 0), Ok(()), "five minutes of skew");
594 assert_eq!(verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS - 1, 0), Err(OrderRefusal::Ahead(3)));
595 }
596
597 #[test]
598 fn a_call_is_signed_by_the_box_and_fresh() {
599 let box_key = SigningKey::from_bytes(&[3u8; 32]);
600 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 };
601 let m = fleet_call_message(&c);
602 assert!(m.starts_with(CALL_DOMAIN));
603 let sig = box_key.sign(&m).to_bytes();
604 let pk = box_key.verifying_key();
605 assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_060_000), Ok(()));
606 assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_000_000 + CALL_FRESH_MS + 1), Err(CallRefusal::Stale));
607 let mut moved = c.clone();
608 moved.role = "twin".into();
609 assert_eq!(verify_fleet_call(&moved, &sig, &pk, 1_800_000_000_000), Err(CallRefusal::Signature));
610 assert_eq!(verify_fleet_call(&c, &sig[..10], &pk, 1_800_000_000_000), Err(CallRefusal::Signature));
611 }
612}