crates/gunnar-growbench/src/money.rs
//! **The bench's own monetize key**, and facts signed with it.
//!
//! The entitlement cells (T1d, TK) need a box that TRUSTS a key the bench holds:
//! a medium composed `--param monetize_pubkey=<the bench key>` — never
//! monetize's. The bench signs exactly what monetize-server signs
//! (`monetize_embed::signing::fact_message{,_issued}` over monetize's own
//! `EntitlementFact`) and hands it to the box through the one door an operator
//! has, `gunnar control entitlement set`, which signs nothing itself.
//!
//! A seed is 32 bytes kept as 64 hex characters in a file the bench names; it
//! is made once from `/dev/urandom` and reused, so a medium composed against it
//! stays usable across runs.
use std::collections::BTreeMap;
use std::io::Read;
use std::path::Path;
use ed25519_dalek::{Signer, SigningKey};
use monetize_embed::{EntitlementFact, State, TenantId};
/// The seed at `path`, made if absent.
pub fn load_or_make(path: &Path) -> Result<SigningKey, String> {
if !path.exists() {
let mut seed = [0u8; 32];
std::fs::File::open("/dev/urandom")
.and_then(|mut f| f.read_exact(&mut seed))
.map_err(|e| format!("/dev/urandom: {e}"))?;
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
std::fs::write(path, hex(&seed) + "\n").map_err(|e| format!("{}: {e}", path.display()))?;
}
let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
let bytes =
unhex(text.trim()).ok_or_else(|| format!("{} is not 64 hex characters", path.display()))?;
let seed: [u8; 32] = bytes
.try_into()
.map_err(|_| format!("{} is not 32 bytes", path.display()))?;
Ok(SigningKey::from_bytes(&seed))
}
/// The verifying key as the medium's `monetize_pubkey` parameter spells it.
pub fn pub_hex(key: &SigningKey) -> String {
hex(&key.verifying_key().to_bytes())
}
/// A fact monetize would mint, signed V1 and stamped (`issued_unix_ms`), as the
/// JSON `gunnar control entitlement set` reads.
pub fn signed_fact_json(
key: &SigningKey,
namespace: &str,
state: State,
caps: &[(&str, u64)],
issued_unix_ms: u64,
) -> String {
let mut fact = EntitlementFact {
tenant: TenantId(namespace.to_owned()),
plan: format!("growbench/{namespace}"),
state,
paid_until_unix_ms: Some(issued_unix_ms + 30 * 86_400_000),
caps: caps
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect::<BTreeMap<_, _>>(),
source: "payment:growbench:bench-key".into(),
signature: vec![],
issued_unix_ms: Some(issued_unix_ms),
issued_signature: vec![],
};
fact.signature = key
.sign(&monetize_embed::signing::fact_message(&fact))
.to_bytes()
.to_vec();
fact.issued_signature = key
.sign(&monetize_embed::signing::fact_message_issued(&fact))
.to_bytes()
.to_vec();
serde_json::to_string(&fact).expect("a fact serializes")
}
/// The key the command line trusts, if any.
pub fn pubkey_in(cmdline: &str) -> Option<String> {
cmdline
.split_whitespace()
.find_map(|t| t.strip_prefix("korp.env.GUNNAR_MONETIZE_PUBKEY="))
.map(str::to_string)
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn unhex(s: &str) -> Option<Vec<u8>> {
if s.len() % 2 != 0 {
return None;
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
/// A bench fact VERIFIES under the bench key and not under another — the
/// same check `EntitlementSlot` makes — so what the bench pushes is what
/// monetize would have minted, and a wrong-key box refuses it.
#[test]
fn a_bench_fact_verifies_under_its_key_and_only_its_key() {
let key = SigningKey::from_bytes(&[3u8; 32]);
let other = SigningKey::from_bytes(&[4u8; 32]);
let json = signed_fact_json(
&key,
"team",
State::Paid,
&[("pack_bytes", 1 << 30)],
1_789_000_000_000,
);
let fact: EntitlementFact = serde_json::from_str(&json).unwrap();
assert!(monetize_embed::signing::verify_fact(&fact, &key.verifying_key()).is_ok());
assert!(monetize_embed::signing::verify_fact(&fact, &other.verifying_key()).is_err());
}
#[test]
fn the_key_is_read_off_a_command_line_and_its_absence_is_none() {
let a = "a".repeat(64);
let line = format!("console=tty0 korp.env.GUNNAR_MONETIZE_PUBKEY={a} korp.entry=gunnar");
assert_eq!(pubkey_in(&line).as_deref(), Some(a.as_str()));
assert_eq!(pubkey_in("console=tty0 korp.entry=gunnar"), None);
}
#[test]
fn a_seed_is_made_once_and_read_back_the_same() {
let dir = std::env::temp_dir().join(format!("growbench-money-{}", std::process::id()));
let p = dir.join("seed");
let k1 = load_or_make(&p).unwrap();
let k2 = load_or_make(&p).unwrap();
assert_eq!(pub_hex(&k1), pub_hex(&k2));
let _ = std::fs::remove_dir_all(&dir);
}
}