monetize_estate_side/pump/
key.rs1use std::path::Path;
6
7use ed25519_dalek::Signer as _;
8use monetize_embed::fleet::{fleet_call_message, FleetCall};
9
10pub const BOX_KEY_REL: &str = "fleet/box.ed25519";
12
13pub struct BoxKey(ed25519_dalek::SigningKey);
14
15impl BoxKey {
16 pub fn from_seed(seed: [u8; 32]) -> BoxKey {
17 BoxKey(ed25519_dalek::SigningKey::from_bytes(&seed))
18 }
19
20 pub fn load_or_mint(root: &Path) -> std::io::Result<BoxKey> {
24 let path = root.join(BOX_KEY_REL);
25 match std::fs::read(&path) {
26 Ok(bytes) => {
27 let seed: [u8; 32] = bytes.as_slice().try_into().map_err(|_| {
28 std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{} holds {} bytes, not a 32-byte Ed25519 seed", path.display(), bytes.len()))
29 })?;
30 Ok(BoxKey::from_seed(seed))
31 }
32 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
33 let mut seed = [0u8; 32];
34 getrandom::fill(&mut seed).map_err(|e| std::io::Error::other(format!("no randomness for the box key: {e}")))?;
35 let mut tag = [0u8; 8];
36 getrandom::fill(&mut tag).map_err(|e| std::io::Error::other(format!("no randomness: {e}")))?;
37 let fresh = path.with_extension(format!("new-{}", crate::hex(&tag)));
38 {
39 use std::io::Write as _;
40 if let Some(dir) = path.parent() {
41 std::fs::create_dir_all(dir)?;
42 }
43 let mut opts = std::fs::OpenOptions::new();
44 opts.write(true).create_new(true);
45 #[cfg(unix)]
46 {
47 use std::os::unix::fs::OpenOptionsExt;
48 opts.mode(0o600);
49 }
50 let mut f = opts.open(&fresh)?;
51 f.write_all(&seed)?;
52 f.sync_all()?;
53 }
54 let linked = std::fs::hard_link(&fresh, &path);
55 let _ = std::fs::remove_file(&fresh);
56 match linked {
57 Ok(()) => Ok(BoxKey::from_seed(seed)),
58 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => BoxKey::load_or_mint(root),
59 Err(e) => Err(e),
60 }
61 }
62 Err(e) => Err(e),
63 }
64 }
65
66 pub fn public(&self) -> [u8; 32] {
67 self.0.verifying_key().to_bytes()
68 }
69
70 pub fn sign_bytes(&self, message: &[u8]) -> Vec<u8> {
72 self.0.sign(message).to_bytes().to_vec()
73 }
74
75 pub fn sign(&self, c: &FleetCall) -> Vec<u8> {
77 self.0.sign(&fleet_call_message(c)).to_bytes().to_vec()
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn the_box_key_is_minted_once_owner_only_and_racing_loads_agree() {
87 let dir = tempfile::tempdir().unwrap();
88 let keys: Vec<[u8; 32]> = std::thread::scope(|s| {
89 let hs: Vec<_> = (0..8).map(|_| s.spawn(|| BoxKey::load_or_mint(dir.path()).unwrap().public())).collect();
90 hs.into_iter().map(|h| h.join().unwrap()).collect()
91 });
92 let on_disk = BoxKey::load_or_mint(dir.path()).unwrap().public();
93 assert!(keys.iter().all(|k| *k == on_disk), "every first load answers the key on disk");
94 #[cfg(unix)]
95 {
96 use std::os::unix::fs::PermissionsExt;
97 let mode = std::fs::metadata(dir.path().join(BOX_KEY_REL)).unwrap().permissions().mode();
98 assert_eq!(mode & 0o077, 0, "the box key is owner-only");
99 }
100 }
101
102 #[test]
103 fn a_call_verifies_under_monetize_embeds_own_verifier() {
104 let key = BoxKey::from_seed([3u8; 32]);
105 let c = FleetCall { product: "a-product".into(), instance: "acme".into(), role: "main".into(), call: "report".into(), body: "{}".into(), at_unix_ms: 1_000 };
106 let sig = key.sign(&c);
107 let vk = monetize_embed::VerifyingKey::from_bytes(&key.public()).unwrap();
108 monetize_embed::fleet::verify_fleet_call(&c, &sig, &vk, 1_000).expect("monetize verifies the box's call");
109 }
110}