use serde::Deserialize;
pub const MAX_GO_AHEAD_AGE_MS: i64 = 24 * 3600 * 1000;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct GoAhead {
pub v: u32,
pub signer: String,
pub target_sectors: u64,
pub nonce: String,
pub issued_unix_ms: i64,
#[serde(default)]
pub signature: String,
#[serde(default)]
pub alone: String,
}
impl GoAhead {
pub fn parse(bytes: &[u8]) -> Result<GoAhead, String> {
if bytes.is_empty() {
return Err("the go-ahead is empty".into());
}
let go: GoAhead = serde_json::from_slice(bytes).map_err(|e| format!("the go-ahead is not the JSON object this box expects ({e}); see gunnar_server::grow::go_ahead"))?;
if go.v != 1 {
return Err(format!("go-ahead version {} is not 1", go.v));
}
if !matches!(go.signer.as_str(), "operator" | "monetize") {
return Err(format!(
"go-ahead signer {:?} is neither `operator` nor `monetize`",
go.signer
));
}
if go.nonce.trim().is_empty() || go.nonce.len() > 256 || go.nonce.contains(['\n', '\r']) {
return Err("go-ahead nonce must be one non-empty line of at most 256 bytes".into());
}
Ok(go)
}
pub fn operator(target_sectors: u64, nonce: &str, issued_unix_ms: i64) -> Vec<u8> {
serde_json::to_vec(&serde_json::json!({
"v": 1,
"signer": "operator",
"target_sectors": target_sectors,
"nonce": nonce,
"issued_unix_ms": issued_unix_ms,
"signature": "",
}))
.expect("a JSON object of scalars encodes")
}
}
pub trait GoAheadPolicy: Send + Sync + 'static {
fn verify(&self, go: &GoAhead, now_unix_ms: i64) -> Result<(), String>;
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone)]
pub struct OperatorPolicy {
pub max_age_ms: i64,
}
impl Default for OperatorPolicy {
fn default() -> Self {
OperatorPolicy {
max_age_ms: MAX_GO_AHEAD_AGE_MS,
}
}
}
impl GoAheadPolicy for OperatorPolicy {
fn verify(&self, go: &GoAhead, now_unix_ms: i64) -> Result<(), String> {
match go.signer.as_str() {
"operator" => {}
other => {
return Err(format!(
"a `{other}` go-ahead cannot be verified by this box: no policy for that signer is plugged in (the default policy takes `operator` go-aheads on a gunnar-admin call)"
))
}
}
bound_age(go, now_unix_ms, self.max_age_ms)
}
fn name(&self) -> &'static str {
"operator"
}
}
fn bound_age(go: &GoAhead, now_unix_ms: i64, max_age_ms: i64) -> Result<(), String> {
let age = now_unix_ms - go.issued_unix_ms;
if age > max_age_ms {
return Err(format!(
"the go-ahead was issued {} s ago, past the {} s ceiling; mint a fresh one",
age / 1000,
max_age_ms / 1000
));
}
if age < -5 * 60 * 1000 {
return Err(format!(
"the go-ahead is dated {} s in the future; whose clock is wrong?",
-age / 1000
));
}
Ok(())
}
#[cfg(feature = "metered")]
pub struct MonetizePolicy {
key: std::sync::RwLock<Option<monetize_embed::VerifyingKey>>,
operator: OperatorPolicy,
pub max_age_ms: i64,
}
#[cfg(feature = "metered")]
impl std::fmt::Debug for MonetizePolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MonetizePolicy")
.field("max_age_ms", &self.max_age_ms)
.finish()
}
}
#[cfg(feature = "metered")]
impl MonetizePolicy {
pub fn new(monetize_pubkey: &[u8; 32]) -> Result<Self, String> {
let key = monetize_embed::VerifyingKey::from_bytes(monetize_pubkey)
.map_err(|e| format!("the monetize public key this box was started with is not a valid Ed25519 point ({e}); no `monetize` go-ahead could ever verify under it"))?;
Ok(MonetizePolicy {
key: std::sync::RwLock::new(Some(key)),
operator: OperatorPolicy::default(),
max_age_ms: MAX_GO_AHEAD_AGE_MS,
})
}
pub fn awaiting() -> Self {
MonetizePolicy {
key: std::sync::RwLock::new(None),
operator: OperatorPolicy::default(),
max_age_ms: MAX_GO_AHEAD_AGE_MS,
}
}
pub fn set_key(&self, monetize_pubkey: &[u8; 32]) -> Result<(), String> {
let key = monetize_embed::VerifyingKey::from_bytes(monetize_pubkey).map_err(|e| {
format!("the fed monetize public key is not a valid Ed25519 point ({e})")
})?;
*self.key.write().unwrap_or_else(|p| p.into_inner()) = Some(key);
Ok(())
}
}
#[cfg(feature = "metered")]
impl GoAheadPolicy for MonetizePolicy {
fn verify(&self, go: &GoAhead, now_unix_ms: i64) -> Result<(), String> {
match go.signer.as_str() {
"operator" => return self.operator.verify(go, now_unix_ms),
"monetize" => {}
other => {
return Err(format!("go-ahead signer {other:?} is neither `operator` nor `monetize`; this box verifies a `monetize` signature and takes an `operator` go-ahead on a gunnar-admin call, and nothing else"));
}
}
bound_age(go, now_unix_ms, self.max_age_ms)?;
let wire = monetize_embed::signing::GoAhead {
v: go.v,
signer: go.signer.clone(),
target_sectors: go.target_sectors,
nonce: go.nonce.clone(),
issued_unix_ms: go.issued_unix_ms,
signature: go.signature.clone(),
};
let bytes = serde_json::to_vec(&wire).expect("a go-ahead of scalars encodes");
let key = self.key.read().unwrap_or_else(|p| p.into_inner()).clone().ok_or_else(|| {
format!("the `monetize` go-ahead for nonce {:?} cannot be checked: this box has no monetize key yet (the installer feeds it)", go.nonce)
})?;
monetize_embed::signing::verify_go_ahead(&bytes, &key).map(|_| ()).map_err(|e| {
format!(
"the `monetize` go-ahead for nonce {:?} does not verify under the monetize public key this box was started with ({e}); it was not signed by the monetize this appliance was installed against, or it was altered on the way",
go.nonce
)
})
}
fn name(&self) -> &'static str {
"monetize"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_go_ahead_is_parsed_strictly_and_the_operator_policy_bounds_its_age() {
let now = 1_800_000_000_000;
let ok = GoAhead::operator(4096, "n-1", now - 1000);
let go = GoAhead::parse(&ok).expect("parses");
assert_eq!(
(
go.v,
go.signer.as_str(),
go.target_sectors,
go.nonce.as_str()
),
(1, "operator", 4096, "n-1")
);
let p = OperatorPolicy::default();
p.verify(&go, now).expect("fresh operator go-ahead");
assert!(GoAhead::parse(b"").unwrap_err().contains("empty"));
assert!(GoAhead::parse(b"not json")
.unwrap_err()
.contains("not the JSON"));
assert!(GoAhead::parse(
br#"{"v":2,"signer":"operator","target_sectors":1,"nonce":"x","issued_unix_ms":1}"#
)
.unwrap_err()
.contains("version"));
assert!(GoAhead::parse(
br#"{"v":1,"signer":"root","target_sectors":1,"nonce":"x","issued_unix_ms":1}"#
)
.unwrap_err()
.contains("signer"));
assert!(GoAhead::parse(
br#"{"v":1,"signer":"operator","target_sectors":1,"nonce":"","issued_unix_ms":1}"#
)
.unwrap_err()
.contains("nonce"));
assert!(GoAhead::parse(
br#"{"v":1,"signer":"operator","target_sectors":1,"nonce":"a\nb","issued_unix_ms":1}"#
)
.unwrap_err()
.contains("nonce"));
let stale =
GoAhead::parse(&GoAhead::operator(1, "n-2", now - MAX_GO_AHEAD_AGE_MS - 1)).unwrap();
assert!(p.verify(&stale, now).unwrap_err().contains("past the"));
let future = GoAhead::parse(&GoAhead::operator(1, "n-3", now + 10 * 60 * 1000)).unwrap();
assert!(p.verify(&future, now).unwrap_err().contains("future"));
let monetize = GoAhead::parse(br#"{"v":1,"signer":"monetize","target_sectors":1,"nonce":"m","issued_unix_ms":1,"signature":"AAAA"}"#).unwrap();
assert!(p
.verify(&monetize, now)
.unwrap_err()
.contains("no policy for that signer"));
}
}
#[cfg(all(test, feature = "metered"))]
pub(super) mod metered_tests {
use super::*;
use ed25519_dalek::{Signer as _, SigningKey};
use monetize_embed::signing::{base64_encode, go_ahead_message, GO_AHEAD_SIGNER_MONETIZE};
fn key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
pub(crate) fn signed(
k: &SigningKey,
target_sectors: u64,
nonce: &str,
issued_unix_ms: i64,
) -> Vec<u8> {
let mut go = monetize_embed::signing::GoAhead {
v: 1,
signer: GO_AHEAD_SIGNER_MONETIZE.into(),
target_sectors,
nonce: nonce.into(),
issued_unix_ms,
signature: String::new(),
};
go.signature = base64_encode(&k.sign(&go_ahead_message(&go)).to_bytes());
serde_json::to_vec(&go).expect("encodes")
}
#[test]
fn a_monetize_signed_go_ahead_verifies_and_a_bad_signature_is_refused_by_name() {
let now = 1_800_000_000_000;
let monetize = key(7);
let p = MonetizePolicy::new(&monetize.verifying_key().to_bytes()).expect("a real key");
assert_eq!(p.name(), "monetize");
let go = GoAhead::parse(&signed(&monetize, 4096, "m-1", now - 1000)).expect("parses");
assert_eq!((go.signer.as_str(), go.target_sectors), ("monetize", 4096));
p.verify(&go, now)
.expect("monetize's own signature verifies");
let impostor = GoAhead::parse(&signed(&key(9), 4096, "m-2", now - 1000)).unwrap();
let err = p.verify(&impostor, now).unwrap_err();
assert!(
err.contains("does not verify") && err.contains("m-2"),
"{err}"
);
let mut tampered = go.clone();
tampered.target_sectors = 8192;
assert!(p
.verify(&tampered, now)
.unwrap_err()
.contains("does not verify"));
let mut moved = go.clone();
moved.nonce = "m-3".into();
assert!(p
.verify(&moved, now)
.unwrap_err()
.contains("does not verify"));
let mut bare = go.clone();
bare.signature = String::new();
assert!(p
.verify(&bare, now)
.unwrap_err()
.contains("does not verify"));
let mut junk = go.clone();
junk.signature = "!!!not base64!!!".into();
assert!(p
.verify(&junk, now)
.unwrap_err()
.contains("does not verify"));
let stale = GoAhead::parse(&signed(
&monetize,
4096,
"m-4",
now - MAX_GO_AHEAD_AGE_MS - 1,
))
.unwrap();
assert!(p.verify(&stale, now).unwrap_err().contains("past the"));
let ahead = GoAhead::parse(&signed(&monetize, 4096, "m-5", now + 10 * 60 * 1000)).unwrap();
assert!(p.verify(&ahead, now).unwrap_err().contains("future"));
}
#[test]
fn a_wrong_signer_is_refused_by_name_and_neither_the_operator_nor_the_community_door_moved() {
let now = 1_800_000_000_000;
let monetize = key(7);
let p = MonetizePolicy::new(&monetize.verifying_key().to_bytes()).unwrap();
let err = GoAhead::parse(br#"{"v":1,"signer":"root","target_sectors":1,"nonce":"x","issued_unix_ms":1,"signature":"AAAA"}"#).unwrap_err();
assert!(err.contains("signer") && err.contains("root"), "{err}");
let held = GoAhead {
v: 1,
signer: "root".into(),
target_sectors: 1,
nonce: "x".into(),
issued_unix_ms: now,
signature: String::new(),
alone: String::new(),
};
assert!(p
.verify(&held, now)
.unwrap_err()
.contains("neither `operator` nor `monetize`"));
let op = GoAhead::parse(&GoAhead::operator(4096, "op-1", now - 1000)).unwrap();
p.verify(&op, now)
.expect("a metered box still takes its owner's go-ahead");
assert!(p
.verify(
&GoAhead::parse(&GoAhead::operator(1, "op-2", 1)).unwrap(),
now
)
.unwrap_err()
.contains("past the"));
let mon = GoAhead::parse(&signed(&monetize, 4096, "m-9", now - 1000)).unwrap();
let err = OperatorPolicy::default().verify(&mon, now).unwrap_err();
assert!(err.contains("no policy for that signer"), "{err}");
assert!(MonetizePolicy::new(&[0xdd; 32])
.unwrap_err()
.contains("not a valid Ed25519 point"));
}
#[cfg(feature = "metered")]
#[test]
fn a_go_ahead_signed_with_a_key_fed_while_running_is_accepted_by_the_same_policy() {
let now = 1_800_000_000_000;
let monetize = key(7);
let p = MonetizePolicy::awaiting();
let go = GoAhead::parse(&signed(&monetize, 4096, "m-live", now - 1000)).unwrap();
assert!(p
.verify(&go, now)
.unwrap_err()
.contains("no monetize key yet"));
p.verify(
&GoAhead::parse(&GoAhead::operator(4096, "op-live", now - 1000)).unwrap(),
now,
)
.expect("the operator door is there keyless");
p.set_key(&monetize.verifying_key().to_bytes()).unwrap();
p.verify(&go, now)
.expect("the same policy, the fed key, no restart");
let impostor = GoAhead::parse(&signed(&key(9), 4096, "m-other", now - 1000)).unwrap();
assert!(p
.verify(&impostor, now)
.unwrap_err()
.contains("does not verify"));
assert!(p
.set_key(&[0xdd; 32])
.unwrap_err()
.contains("not a valid Ed25519 point"));
}
}