use ed25519_dalek::{Signature, VerifyingKey};
use serde::{Deserialize, Serialize};
pub const ORDER_DOMAIN: &[u8] = b"nordisk-order-v1\0";
pub const CALL_DOMAIN: &[u8] = b"nordisk-fleet-call-v1\0";
pub const CALL_FRESH_MS: u64 = 5 * 60 * 1000;
pub const FUTURE_SKEW_MS: u64 = 5 * 60 * 1000;
pub const LONG_POLL_MAX_MS: u64 = 25_000;
pub use monetize_product::KEEP_BOOT_WITHIN;
pub const UPGRADE_RUNNING: &str = "is already running here";
pub mod kind {
pub const RECHARGE_FROM_TWIN: &str = "recharge_from_twin";
pub const FENCE_PRIMARY: &str = "fence_primary";
pub const ADOPT_TWIN: &str = "adopt_twin";
pub const REVIEW_TWIN: &str = "review_twin";
pub const RESTATE_ENTITLEMENTS: &str = "restate_entitlements";
pub use monetize_product::pull::order::GROW_GO_AHEAD;
pub use monetize_product::pull::order::SIGN_IN_AGAIN;
pub use monetize_product::pull::order::REFEED;
pub const GUARDIAN: [&str; 5] = [RECHARGE_FROM_TWIN, FENCE_PRIMARY, ADOPT_TWIN, REVIEW_TWIN, RESTATE_ENTITLEMENTS];
}
pub use monetize_product::pull::{Fetch, FetchAsk, ProductOrder};
pub mod ask {
pub use monetize_product::pull::ask::GROW_SET;
pub use monetize_product::pull::ask::FETCH;
pub const UPGRADE: &str = "upgrade";
pub const RECHARGE: &str = "recharge";
pub const MATERIAL: &str = "material";
pub const UPDATE_TAKE: &str = "update_take";
pub const UPDATE_REPORT: &str = "update_report";
pub const FRONT: [&str; 5] = [UPGRADE, RECHARGE, MATERIAL, UPDATE_TAKE, UPDATE_REPORT];
pub fn is_front(kind: &str) -> bool {
FRONT.contains(&kind)
}
}
pub mod call {
pub const REPORT: &str = "report";
pub const ORDERS: &str = "orders";
pub const ASK: &str = "ask";
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct SignedOrder {
pub seq: u64,
pub product: String,
pub instance: String,
pub role: String,
pub kind: String,
pub body: String,
pub issued_unix_ms: u64,
pub not_after_unix_ms: u64,
#[serde(default)]
pub signature: Vec<u8>,
}
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
pub enum OrderRefusal {
#[error("the order is not signed by monetize")]
Signature,
#[error("order {seq} is not newer than {last}")]
Replayed { seq: u64, last: u64 },
#[error("order {0} has expired")]
Expired(u64),
#[error("order {0} is for {1}, not this box")]
NotForMe(u64, String),
#[error("order {0} is dated more than five minutes ahead of this box's clock")]
Ahead(u64),
}
fn field(out: &mut Vec<u8>, bytes: &[u8]) {
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
out.extend_from_slice(bytes);
}
pub fn order_message(o: &SignedOrder) -> Vec<u8> {
let mut out = ORDER_DOMAIN.to_vec();
out.extend_from_slice(&o.seq.to_be_bytes());
out.extend_from_slice(&o.issued_unix_ms.to_be_bytes());
out.extend_from_slice(&o.not_after_unix_ms.to_be_bytes());
for f in [&o.product, &o.instance, &o.role, &o.kind, &o.body] {
field(&mut out, f.as_bytes());
}
out
}
pub fn verify_order(o: &SignedOrder, key: &VerifyingKey, me: (&str, &str, &str), now_unix_ms: u64, last_seq: u64) -> Result<(), OrderRefusal> {
let (product, instance, role) = me;
if !check(key, &order_message(o), &o.signature) {
return Err(OrderRefusal::Signature);
}
if o.product != product || o.instance != instance || o.role != role {
return Err(OrderRefusal::NotForMe(o.seq, o.role.clone()));
}
if o.seq <= last_seq {
return Err(OrderRefusal::Replayed { seq: o.seq, last: last_seq });
}
if o.not_after_unix_ms != 0 && now_unix_ms > o.not_after_unix_ms {
return Err(OrderRefusal::Expired(o.seq));
}
if o.issued_unix_ms > now_unix_ms.saturating_add(FUTURE_SKEW_MS) {
return Err(OrderRefusal::Ahead(o.seq));
}
Ok(())
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct FleetCall {
pub product: String,
pub instance: String,
pub role: String,
pub call: String,
pub body: String,
pub at_unix_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
pub enum CallRefusal {
#[error("the call is not signed by the box's key")]
Signature,
#[error("the call's time is too far from monetize's clock")]
Stale,
}
pub fn fleet_call_message(c: &FleetCall) -> Vec<u8> {
let mut out = CALL_DOMAIN.to_vec();
out.extend_from_slice(&c.at_unix_ms.to_be_bytes());
for f in [&c.product, &c.instance, &c.role, &c.call, &c.body] {
field(&mut out, f.as_bytes());
}
out
}
pub fn verify_fleet_call(c: &FleetCall, signature: &[u8], key: &VerifyingKey, now_unix_ms: u64) -> Result<(), CallRefusal> {
if !check(key, &fleet_call_message(c), signature) {
return Err(CallRefusal::Signature);
}
if c.at_unix_ms.abs_diff(now_unix_ms) > CALL_FRESH_MS {
return Err(CallRefusal::Stale);
}
Ok(())
}
fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> bool {
Signature::from_slice(sig).map(|s| key.verify_strict(msg, &s).is_ok()).unwrap_or(false)
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct OrderAck {
pub seq: u64,
pub ok: bool,
pub detail: String,
pub receipt_json: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Report {
pub build: String,
pub health: std::collections::BTreeMap<String, String>,
pub acked: Vec<OrderAck>,
pub twin_review: Option<String>,
pub product: Option<String>,
pub measured_unix_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct OrdersAfter {
pub after: u64,
pub wait_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct GrowGoAhead {
pub go_ahead: String,
pub data_gib: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct SignInAgain {
pub account: String,
pub to: String,
pub host: String,
pub ttl_secs: u64,
}
pub mod refeed {
pub const SMTP_URL: &str = "secret.smtp_url";
pub const SMTP_FROM: &str = "secret.smtp_from";
pub const SMTP_CA: &str = "secret.smtp_ca";
pub const PUBLIC_REPOSITORIES: &str = "public_repositories";
pub const PUBLIC_SITE: &str = "console_public_site";
pub const KNOWN: [&str; 5] = [SMTP_URL, SMTP_FROM, SMTP_CA, PUBLIC_REPOSITORIES, PUBLIC_SITE];
pub fn is_secret(key: &str) -> bool {
key.starts_with("secret.")
}
}
#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Refeed {
pub keys: std::collections::BTreeMap<String, String>,
}
impl std::fmt::Debug for Refeed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Refeed").field("keys", &self.keys.keys().collect::<Vec<_>>()).finish()
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct GrowSetAsk {
pub members: Vec<String>,
pub gib: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct FrontAsk {
#[serde(skip_serializing_if = "String::is_empty")]
pub role: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub done: bool,
#[serde(skip_serializing_if = "String::is_empty")]
pub seed: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub carries: String,
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Ask {
pub kind: String,
pub body: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer as _, SigningKey};
#[test]
fn the_core_kinds_here_are_monetize_products_one_list() {
use monetize_product::pull;
assert_eq!(kind::GROW_GO_AHEAD, pull::order::GROW_GO_AHEAD);
assert_eq!(kind::SIGN_IN_AGAIN, pull::order::SIGN_IN_AGAIN);
assert_eq!(kind::REFEED, pull::order::REFEED);
assert_eq!(ask::GROW_SET, pull::ask::GROW_SET);
for k in kind::GUARDIAN {
assert!(!pull::is_core(k), "{k} is a guardian kind, not a core one");
}
assert_eq!(kind::GUARDIAN, [kind::RECHARGE_FROM_TWIN, kind::FENCE_PRIMARY, kind::ADOPT_TWIN, kind::REVIEW_TWIN, kind::RESTATE_ENTITLEMENTS]);
}
#[test]
fn the_fronts_asks_are_the_five_soldestate_calls_and_their_body_round_trips() {
assert_eq!(ask::FRONT, ["upgrade", "recharge", "material", "update_take", "update_report"]);
for k in ask::FRONT {
assert!(ask::is_front(k));
}
for k in [ask::GROW_SET, kind::RECHARGE_FROM_TWIN, kind::REVIEW_TWIN, "new_twin", "new_server", ""] {
assert!(!ask::is_front(k), "{k}");
}
for k in ask::FRONT {
assert!(!monetize_product::pull::is_core(k), "{k}");
}
let a = FrontAsk { role: "main".into(), seed: "SEED".into(), ..Default::default() };
let text = serde_json::to_string(&Ask { kind: ask::RECHARGE.into(), body: serde_json::to_value(&a).unwrap() }).unwrap();
assert_eq!(text, r#"{"kind":"recharge","body":{"role":"main","seed":"SEED"}}"#);
let back: Ask = serde_json::from_str(&text).unwrap();
assert_eq!(serde_json::from_value::<FrontAsk>(back.body).unwrap(), a);
assert_eq!(serde_json::from_value::<FrontAsk>(serde_json::json!({})).unwrap(), FrontAsk::default(), "an empty body is the update take's");
}
#[test]
fn a_front_waits_on_the_shops_whole_keep_boot() {
assert_eq!(KEEP_BOOT_WITHIN, std::time::Duration::from_secs(2 * 3600));
assert_eq!(KEEP_BOOT_WITHIN, monetize_product::KEEP_BOOT_WITHIN);
}
const GOLDEN_MESSAGE_HEX: &str = "6e6f726469736b2d6f726465722d7631000000000000000003000001a3185c5000000001a3186a0ba00000000667756e6e61720000000461636d65000000047477696e0000001272656368617267655f66726f6d5f7477696e000000517b226163636570745f6c6f7374223a66616c73652c2265706f6368223a352c226d61696e223a2231302e362e302e31303a3530303531222c227477696e223a2231302e362e302e31323a3530303531227d";
const GOLDEN_SIGNATURE_HEX: &str = "d4a3233158d9a3fbfa42f9aa80fd2ca420666ec2fe9f7fbf1a61abab533c09f6dab60c67367bc771a1d819d1e6bed28c59d6d56187a4748ba9b4b9988d13d50a";
fn key() -> SigningKey {
SigningKey::from_bytes(&[7u8; 32])
}
fn order() -> SignedOrder {
SignedOrder {
seq: 3,
product: "gunnar".into(),
instance: "acme".into(),
role: "twin".into(),
kind: kind::RECHARGE_FROM_TWIN.into(),
body: r#"{"accept_lost":false,"epoch":5,"main":"10.6.0.10:50051","twin":"10.6.0.12:50051"}"#.into(),
issued_unix_ms: 1_800_000_000_000,
not_after_unix_ms: 1_800_000_900_000,
signature: Vec::new(),
}
}
fn signed() -> SignedOrder {
let mut o = order();
o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
o
}
#[test]
fn the_order_message_is_the_agreed_binary_layout() {
let m = order_message(&order());
assert!(m.starts_with(ORDER_DOMAIN));
let mut want = ORDER_DOMAIN.to_vec();
want.extend_from_slice(&3u64.to_be_bytes());
want.extend_from_slice(&1_800_000_000_000u64.to_be_bytes());
want.extend_from_slice(&1_800_000_900_000u64.to_be_bytes());
for f in ["gunnar", "acme", "twin", kind::RECHARGE_FROM_TWIN, &order().body] {
want.extend_from_slice(&(f.len() as u32).to_be_bytes());
want.extend_from_slice(f.as_bytes());
}
assert_eq!(m, want);
let sig = key().sign(&m).to_bytes();
let hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(hex, GOLDEN_SIGNATURE_HEX, "the golden vector moved: tell gunnar_repair::order");
let mhex: String = m.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(mhex, GOLDEN_MESSAGE_HEX);
}
#[test]
fn a_box_runs_only_a_fresh_order_signed_for_it() {
let pk = key().verifying_key();
let me = ("gunnar", "acme", "twin");
let now = 1_800_000_100_000;
assert_eq!(verify_order(&signed(), &pk, me, now, 2), Ok(()));
assert_eq!(verify_order(&signed(), &pk, me, now, 3), Err(OrderRefusal::Replayed { seq: 3, last: 3 }));
assert_eq!(verify_order(&signed(), &pk, me, 1_800_000_900_001, 0), Err(OrderRefusal::Expired(3)));
assert_eq!(verify_order(&signed(), &pk, ("gunnar", "acme", "main"), now, 0), Err(OrderRefusal::NotForMe(3, "twin".into())));
let mut forged = signed();
forged.role = "main".into();
assert_eq!(verify_order(&forged, &pk, ("gunnar", "acme", "main"), now, 0), Err(OrderRefusal::Signature));
let other = SigningKey::from_bytes(&[9u8; 32]).verifying_key();
assert_eq!(verify_order(&signed(), &other, me, now, 0), Err(OrderRefusal::Signature));
}
#[test]
fn a_weak_key_with_the_all_zero_signature_signs_no_order() {
let (weak, zero) = weak_key_and_zero_signature();
let mut o = order();
o.signature = zero;
let now = 1_800_000_100_000;
assert_eq!(verify_order(&o, &weak, ("gunnar", "acme", "twin"), now, 0), Err(OrderRefusal::Signature), "a weak key verifies nothing");
assert_eq!(verify_order(&signed(), &key().verifying_key(), ("gunnar", "acme", "twin"), now, 0), Ok(()));
}
#[test]
fn a_weak_box_key_with_the_all_zero_signature_signs_no_call() {
let (weak, zero) = weak_key_and_zero_signature();
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 };
assert_eq!(verify_fleet_call(&c, &zero, &weak, 1_800_000_000_000), Err(CallRefusal::Signature));
}
fn weak_key_and_zero_signature() -> (VerifyingKey, Vec<u8>) {
let mut identity = [0u8; 32];
identity[0] = 1;
let weak = VerifyingKey::from_bytes(&identity).expect("the identity point decompresses");
let mut zero = vec![0u8; 64];
zero[0] = 1;
(weak, zero)
}
#[test]
fn a_tampered_order_is_not_monetizes() {
let pk = key().verifying_key();
let me = ("gunnar", "acme", "twin");
let now = 1_800_000_100_000;
type Tamper = Box<dyn Fn(&mut SignedOrder)>;
let tampered: Vec<(&str, Tamper)> = vec![
("seq", Box::new(|o| o.seq += 1)),
("kind", Box::new(|o| o.kind = kind::FENCE_PRIMARY.into())),
("body", Box::new(|o| o.body = "{}".into())),
("issued", Box::new(|o| o.issued_unix_ms -= 1)),
("not_after", Box::new(|o| o.not_after_unix_ms += 1)),
("bit", Box::new(|o| o.signature[5] ^= 1)),
("short", Box::new(|o| o.signature.truncate(63))),
("none", Box::new(|o| o.signature.clear())),
];
for (what, f) in tampered {
let mut o = signed();
f(&mut o);
assert_eq!(verify_order(&o, &pk, me, now, 0), Err(OrderRefusal::Signature), "{what}");
}
}
#[test]
fn a_forged_order_for_another_box_is_forged_not_misaddressed() {
let mut o = signed();
o.role = "main".into();
assert_eq!(verify_order(&o, &key().verifying_key(), ("gunnar", "acme", "twin"), 1_800_000_100_000, 0), Err(OrderRefusal::Signature));
}
#[test]
fn an_order_with_no_window_does_not_expire() {
let mut o = order();
o.not_after_unix_ms = 0;
o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
assert_eq!(verify_order(&o, &key().verifying_key(), ("gunnar", "acme", "twin"), 1_900_000_000_000, 0), Ok(()));
}
#[test]
fn an_order_dated_ahead_of_the_box_clock_is_refused() {
let pk = key().verifying_key();
let me = ("gunnar", "acme", "twin");
let issued = order().issued_unix_ms;
assert_eq!(verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS, 0), Ok(()), "five minutes of skew");
assert_eq!(verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS - 1, 0), Err(OrderRefusal::Ahead(3)));
}
#[test]
fn a_call_is_signed_by_the_box_and_fresh() {
let box_key = SigningKey::from_bytes(&[3u8; 32]);
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 };
let m = fleet_call_message(&c);
assert!(m.starts_with(CALL_DOMAIN));
let sig = box_key.sign(&m).to_bytes();
let pk = box_key.verifying_key();
assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_060_000), Ok(()));
assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_000_000 + CALL_FRESH_MS + 1), Err(CallRefusal::Stale));
let mut moved = c.clone();
moved.role = "twin".into();
assert_eq!(verify_fleet_call(&moved, &sig, &pk, 1_800_000_000_000), Err(CallRefusal::Signature));
assert_eq!(verify_fleet_call(&c, &sig[..10], &pk, 1_800_000_000_000), Err(CallRefusal::Signature));
}
}