1use std::{
2 collections::HashMap,
3 fs,
4 io::{self, Read, Write},
5 path::{Path, PathBuf},
6 sync::{Arc, RwLock},
7};
8
9use rand::RngCore;
10use serde::{Deserialize, Serialize};
11use sha2::{Digest as Sha2Digest, Sha256};
12
13use crate::attestation::{Ed25519Signer, Signer};
14
15pub type KeyId = String;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct KeyInfo {
22 pub id: KeyId,
23 pub algorithm: String, pub is_default: bool,
25 pub created_at: String, pub fingerprint: String,
28 pub public_key: Vec<u8>, #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub valid_until: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub successor_key_id: Option<KeyId>,
41}
42
43#[derive(Debug, Clone)]
45pub struct RotationResult {
46 pub predecessor: KeyInfo,
48 pub successor: KeyInfo,
50 pub grace_period_until: String,
54}
55
56#[derive(Debug)]
58pub enum KeyError {
59 Io(io::Error),
60 Json(serde_json::Error),
61 Crypto(String),
62 NotFound(KeyId),
63 EmptyKeyId,
64 NoDefaultKey,
65}
66
67impl std::fmt::Display for KeyError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::Io(e) => write!(f, "keys io: {}", e),
71 Self::Json(e) => write!(f, "keys json: {}", e),
72 Self::Crypto(e) => write!(f, "keys crypto: {}", e),
73 Self::NotFound(k) => write!(f, "key not found: {}", k),
74 Self::EmptyKeyId => write!(f, "key id must not be empty"),
75 Self::NoDefaultKey => write!(f, "no default key — run treeship init"),
76 }
77 }
78}
79
80impl std::error::Error for KeyError {}
81impl From<io::Error> for KeyError { fn from(e: io::Error) -> Self { Self::Io(e) } }
82impl From<serde_json::Error> for KeyError { fn from(e: serde_json::Error) -> Self { Self::Json(e) } }
83
84#[derive(Serialize, Deserialize, Clone)]
88struct EncryptedEntry {
89 id: KeyId,
90 algorithm: String,
91 created_at: String,
92 public_key: Vec<u8>,
93 enc_priv_key: Vec<u8>,
95 nonce: Vec<u8>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
101 valid_until: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
105 successor_key_id: Option<KeyId>,
106}
107
108#[derive(Serialize, Deserialize, Default)]
110struct Manifest {
111 default_key_id: Option<KeyId>,
112 key_ids: Vec<KeyId>,
113}
114
115pub struct Store {
125 dir: PathBuf,
126 machine_key: [u8; 32],
127 cache: Arc<RwLock<HashMap<KeyId, EncryptedEntry>>>,
129}
130
131impl Store {
132 pub fn open(dir: impl AsRef<Path>) -> Result<Self, KeyError> {
134 let dir = dir.as_ref().to_path_buf();
135 fs::create_dir_all(&dir)?;
136
137 let machine_key = derive_machine_key(&dir)?;
138
139 Ok(Self {
140 dir,
141 machine_key,
142 cache: Arc::new(RwLock::new(HashMap::new())),
143 })
144 }
145
146 pub fn generate(&self, set_default: bool) -> Result<KeyInfo, KeyError> {
150 let key_id = new_key_id();
151
152 let signer = Ed25519Signer::generate(&key_id)
153 .map_err(|e| KeyError::Crypto(e.to_string()))?;
154
155 let secret = signer.secret_bytes();
156 let pub_key = signer.public_key_bytes();
157
158 let (enc, nonce) = aes_gcm_encrypt(&self.machine_key, &secret)
159 .map_err(|e| KeyError::Crypto(e))?;
160
161 let entry = EncryptedEntry {
162 id: key_id.clone(),
163 algorithm: "ed25519".into(),
164 created_at: crate::statements::unix_to_rfc3339(unix_now()),
165 public_key: pub_key.clone(),
166 enc_priv_key: enc,
167 nonce,
168 valid_until: None,
169 successor_key_id: None,
170 };
171
172 self.write_entry(&entry)?;
173
174 let mut manifest = self.read_manifest()?;
176 manifest.key_ids.push(key_id.clone());
177 if set_default || manifest.default_key_id.is_none() {
178 manifest.default_key_id = Some(key_id.clone());
179 }
180 self.write_manifest(&manifest)?;
181
182 self.cache.write().unwrap().insert(key_id.clone(), entry);
184
185 Ok(KeyInfo {
186 id: key_id.clone(),
187 algorithm: "ed25519".into(),
188 is_default: manifest.default_key_id.as_deref() == Some(key_id.as_str()),
189 created_at: crate::statements::unix_to_rfc3339(unix_now()),
190 fingerprint: fingerprint(&pub_key),
191 public_key: pub_key,
192 valid_until: None,
193 successor_key_id: None,
194 })
195 }
196
197 pub fn rotate(
222 &self,
223 predecessor_id: Option<&str>,
224 grace_period: std::time::Duration,
225 set_default: bool,
226 ) -> Result<RotationResult, KeyError> {
227 let pred_id = match predecessor_id {
229 Some(id) => id.to_string(),
230 None => self.default_key_id()?,
231 };
232
233 let pred_entry_existing = self.load_entry(&pred_id)?;
237 if let Some(existing) = &pred_entry_existing.successor_key_id {
238 return Err(KeyError::Crypto(format!(
239 "key {pred_id} has already been rotated to {existing}; \
240 rotate the chain head instead"
241 )));
242 }
243
244 let succ_id = new_key_id();
249 let signer = Ed25519Signer::generate(&succ_id)
250 .map_err(|e| KeyError::Crypto(e.to_string()))?;
251 let succ_secret = signer.secret_bytes();
252 let succ_pub_key = signer.public_key_bytes();
253 let (succ_enc, succ_nonce) = aes_gcm_encrypt(&self.machine_key, &succ_secret)
254 .map_err(KeyError::Crypto)?;
255
256 let succ_created = crate::statements::unix_to_rfc3339(unix_now());
257 let succ_entry = EncryptedEntry {
258 id: succ_id.clone(),
259 algorithm: "ed25519".into(),
260 created_at: succ_created.clone(),
261 public_key: succ_pub_key.clone(),
262 enc_priv_key: succ_enc,
263 nonce: succ_nonce,
264 valid_until: None,
265 successor_key_id: None,
266 };
267
268 let valid_until = crate::statements::unix_to_rfc3339(
270 unix_now() + grace_period.as_secs(),
271 );
272 let mut pred_entry = pred_entry_existing;
273 pred_entry.valid_until = Some(valid_until.clone());
274 pred_entry.successor_key_id = Some(succ_id.clone());
275
276 self.write_entry(&succ_entry)?;
291 self.write_entry(&pred_entry)?;
292
293 {
303 let mut cache = self.cache.write().unwrap();
304 cache.insert(pred_entry.id.clone(), pred_entry.clone());
305 cache.insert(succ_id.clone(), succ_entry.clone());
306 }
307
308 let mut manifest = self.read_manifest()?;
310 manifest.key_ids.push(succ_id.clone());
311 if set_default {
312 manifest.default_key_id = Some(succ_id.clone());
313 }
314 self.write_manifest(&manifest)?;
315
316 let default_id = manifest.default_key_id.clone();
317 let predecessor = KeyInfo {
318 id: pred_entry.id.clone(),
319 algorithm: pred_entry.algorithm.clone(),
320 is_default: default_id.as_deref() == Some(pred_entry.id.as_str()),
321 created_at: pred_entry.created_at.clone(),
322 fingerprint: fingerprint(&pred_entry.public_key),
323 public_key: pred_entry.public_key.clone(),
324 valid_until: pred_entry.valid_until.clone(),
325 successor_key_id: pred_entry.successor_key_id.clone(),
326 };
327 let successor = KeyInfo {
328 id: succ_id.clone(),
329 algorithm: "ed25519".into(),
330 is_default: default_id.as_deref() == Some(succ_id.as_str()),
331 created_at: succ_created,
332 fingerprint: fingerprint(&succ_pub_key),
333 public_key: succ_pub_key,
334 valid_until: None,
335 successor_key_id: None,
336 };
337
338 Ok(RotationResult {
339 predecessor,
340 successor,
341 grace_period_until: valid_until,
342 })
343 }
344
345 pub fn successor_chain(&self, id: &str) -> Result<Vec<KeyId>, KeyError> {
349 let mut chain = Vec::new();
350 let mut cursor = id.to_string();
351 let max_steps = self.read_manifest()?.key_ids.len() + 1;
355 for _ in 0..max_steps {
356 chain.push(cursor.clone());
357 let entry = self.load_entry(&cursor)?;
358 match entry.successor_key_id {
359 Some(next) => cursor = next,
360 None => return Ok(chain),
361 }
362 }
363 Err(KeyError::Crypto(format!(
364 "rotation chain starting at {id} exceeds keystore size; suspected loop"
365 )))
366 }
367
368 pub fn valid_keys_at(&self, at_unix_secs: u64) -> Result<Vec<KeyInfo>, KeyError> {
373 let cutoff_rfc = crate::statements::unix_to_rfc3339(at_unix_secs);
374 Ok(self.list()?
375 .into_iter()
376 .filter(|k| match &k.valid_until {
377 None => true,
378 Some(until) => until.as_str() > cutoff_rfc.as_str(),
379 })
380 .collect())
381 }
382
383 pub fn default_signer(&self) -> Result<Box<dyn Signer>, KeyError> {
385 let manifest = self.read_manifest()?;
386 let id = manifest.default_key_id.ok_or(KeyError::NoDefaultKey)?;
387 self.signer(&id)
388 }
389
390 pub fn signer(&self, id: &str) -> Result<Box<dyn Signer>, KeyError> {
392 let entry = self.load_entry(id)?;
393
394 let secret = aes_gcm_decrypt(&self.machine_key, &entry.enc_priv_key, &entry.nonce)
395 .map_err(|e| self.enrich_crypto_error(e))?;
396
397 let secret_arr: [u8; 32] = secret.try_into()
398 .map_err(|_| KeyError::Crypto("decrypted key is wrong length".into()))?;
399
400 let signer = Ed25519Signer::from_bytes(&entry.id, &secret_arr)
401 .map_err(|e| KeyError::Crypto(e.to_string()))?;
402
403 Ok(Box::new(signer))
404 }
405
406 fn enrich_crypto_error(&self, raw: String) -> KeyError {
418 if !raw.contains("MAC verification failed") {
421 return KeyError::Crypto(raw);
422 }
423
424 let legacy_seed_dot = self.dir.join(".machineseed");
425 let legacy_seed = self.dir.join("machine_seed");
426 let has_legacy_seed = legacy_seed_dot.exists() || legacy_seed.exists();
427
428 let diagnosis = if has_legacy_seed {
429 "your keystore was created by an older Treeship version whose \
430 machine-key derivation has since changed. The ciphertext is \
431 intact but cannot be decrypted under the current derivation."
432 } else {
433 "the keystore cannot be decrypted. Usual causes: the key file \
434 was copied from a different machine, the hostname or username \
435 changed, or the file was corrupted."
436 };
437
438 let ts_dir = std::env::var("HOME")
441 .map(|h| format!("{h}/.treeship"))
442 .unwrap_or_else(|_| "~/.treeship".into());
443
444 let msg = format!(
449 "{raw}\n\n \
450 Diagnosis: {diagnosis}\n\n \
451 Recovery (nondestructive -- the old keystore is moved aside, \
452 not deleted; any sealed .treeship packages you produced remain \
453 verifiable since their receipts embed the old public key):\n\n \
454 mv {ts_dir} {ts_dir}.bak.$(date +%s)\n \
455 treeship init\n"
456 );
457
458 KeyError::Crypto(msg)
459 }
460
461 pub fn default_key_id(&self) -> Result<KeyId, KeyError> {
463 self.read_manifest()?
464 .default_key_id
465 .ok_or(KeyError::NoDefaultKey)
466 }
467
468 pub fn list(&self) -> Result<Vec<KeyInfo>, KeyError> {
470 let manifest = self.read_manifest()?;
471 let default = manifest.default_key_id.as_deref().unwrap_or("");
472
473 manifest.key_ids.iter().map(|id| {
474 let entry = self.load_entry(id)?;
475 Ok(KeyInfo {
476 id: entry.id.clone(),
477 algorithm: entry.algorithm.clone(),
478 is_default: entry.id == default,
479 created_at: entry.created_at.clone(),
480 fingerprint: fingerprint(&entry.public_key),
481 public_key: entry.public_key.clone(),
482 valid_until: entry.valid_until.clone(),
483 successor_key_id: entry.successor_key_id.clone(),
484 })
485 }).collect()
486 }
487
488 pub fn set_default(&self, id: &str) -> Result<(), KeyError> {
490 self.load_entry(id)?;
492 let mut manifest = self.read_manifest()?;
493 manifest.default_key_id = Some(id.to_string());
494 self.write_manifest(&manifest)
495 }
496
497 pub fn public_key(&self, id: &str) -> Result<Vec<u8>, KeyError> {
499 Ok(self.load_entry(id)?.public_key)
500 }
501
502 fn load_entry(&self, id: &str) -> Result<EncryptedEntry, KeyError> {
505 if let Ok(cache) = self.cache.read() {
507 if let Some(entry) = cache.get(id) {
508 return Ok(entry.clone());
509 }
510 }
511 self.read_entry(id)
512 }
513
514 fn entry_path(&self, id: &str) -> PathBuf {
515 self.dir.join(format!("{}.json", id))
516 }
517
518 fn write_entry(&self, entry: &EncryptedEntry) -> Result<(), KeyError> {
519 let path = self.entry_path(&entry.id);
520 let json = serde_json::to_vec_pretty(entry)?;
521 write_file_600(&path, &json)?;
522 Ok(())
523 }
524
525 fn read_entry(&self, id: &str) -> Result<EncryptedEntry, KeyError> {
526 let path = self.entry_path(id);
527 if !path.exists() {
528 return Err(KeyError::NotFound(id.to_string()));
529 }
530 let bytes = fs::read(&path)?;
531 let entry: EncryptedEntry = serde_json::from_slice(&bytes)?;
532 Ok(entry)
533 }
534
535 fn manifest_path(&self) -> PathBuf {
536 self.dir.join("manifest.json")
537 }
538
539 fn read_manifest(&self) -> Result<Manifest, KeyError> {
540 let path = self.manifest_path();
541 if !path.exists() {
542 return Ok(Manifest::default());
543 }
544 let bytes = fs::read(&path)?;
545 Ok(serde_json::from_slice(&bytes)?)
546 }
547
548 fn write_manifest(&self, m: &Manifest) -> Result<(), KeyError> {
549 let json = serde_json::to_vec_pretty(m)?;
550 write_file_600(&self.manifest_path(), &json)?;
551 Ok(())
552 }
553}
554
555pub fn aes_gcm_encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
560 use sha2::Sha256;
571
572 let mut nonce = [0u8; 12];
573 rand::thread_rng().fill_bytes(&mut nonce);
574
575 let mut enc_key_input = key.to_vec();
577 enc_key_input.extend_from_slice(&nonce);
578 enc_key_input.extend_from_slice(b"enc");
579 let enc_key = Sha256::digest(&enc_key_input);
580
581 let mut mac_key_input = key.to_vec();
582 mac_key_input.extend_from_slice(&nonce);
583 mac_key_input.extend_from_slice(b"mac");
584 let mac_key = Sha256::digest(&mac_key_input);
585
586 let ciphertext: Vec<u8> = plaintext.iter().enumerate().map(|(i, &b)| {
588 let mut block_input = enc_key.to_vec();
589 block_input.extend_from_slice(&(i as u64).to_le_bytes());
590 let block = Sha256::digest(&block_input);
591 b ^ block[i % 32]
592 }).collect();
593
594 let mut mac_input = mac_key.to_vec();
596 mac_input.extend_from_slice(&nonce);
597 mac_input.extend_from_slice(&ciphertext);
598 let mac = Sha256::digest(&mac_input);
599
600 let mut out = Vec::with_capacity(12 + 32 + ciphertext.len());
602 out.extend_from_slice(&nonce);
603 out.extend_from_slice(&mac);
604 out.extend_from_slice(&ciphertext);
605
606 Ok((out, nonce.to_vec()))
607}
608
609pub fn aes_gcm_decrypt(key: &[u8; 32], enc_data: &[u8], _nonce_unused: &[u8]) -> Result<Vec<u8>, String> {
610 if enc_data.len() < 44 {
611 return Err("ciphertext too short".into());
612 }
613 use sha2::Sha256;
614
615 let nonce = &enc_data[..12];
616 let stored_mac = &enc_data[12..44];
617 let ciphertext = &enc_data[44..];
618
619 let nonce_arr: [u8; 12] = nonce.try_into().unwrap();
620
621 let mut enc_key_input = key.to_vec();
622 enc_key_input.extend_from_slice(&nonce_arr);
623 enc_key_input.extend_from_slice(b"enc");
624 let enc_key = Sha256::digest(&enc_key_input);
625
626 let mut mac_key_input = key.to_vec();
627 mac_key_input.extend_from_slice(&nonce_arr);
628 mac_key_input.extend_from_slice(b"mac");
629 let mac_key = Sha256::digest(&mac_key_input);
630
631 let mut mac_input = mac_key.to_vec();
633 mac_input.extend_from_slice(&nonce_arr);
634 mac_input.extend_from_slice(ciphertext);
635 let computed_mac = Sha256::digest(&mac_input);
636
637 let mac_ok = stored_mac.iter().zip(computed_mac.iter())
639 .fold(0u8, |acc, (a, b)| acc | (a ^ b)) == 0;
640
641 if !mac_ok {
642 return Err("MAC verification failed — key file may be corrupt or wrong machine".into());
643 }
644
645 let plaintext: Vec<u8> = ciphertext.iter().enumerate().map(|(i, &b)| {
646 let mut block_input = enc_key.to_vec();
647 block_input.extend_from_slice(&(i as u64).to_le_bytes());
648 let block = Sha256::digest(&block_input);
649 b ^ block[i % 32]
650 }).collect();
651
652 Ok(plaintext)
653}
654
655pub fn derive_machine_key(store_dir: &Path) -> Result<[u8; 32], KeyError> {
658 if let Ok(id) = fs::read_to_string("/etc/machine-id") {
660 let trimmed = id.trim();
661 if !trimmed.is_empty() {
662 let mut h = Sha256::new();
663 h.update(trimmed.as_bytes());
664 h.update(store_dir.to_string_lossy().as_bytes());
665 return Ok(h.finalize().into());
666 }
667 }
668
669 #[cfg(target_os = "macos")]
681 {
682 let hostname = std::process::Command::new("hostname")
683 .output()
684 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
685 .unwrap_or_default();
686 let username = std::env::var("USER").unwrap_or_default();
687 if !hostname.is_empty() && !username.is_empty() {
688 let mut h = Sha256::new();
689 h.update(b"treeship-machine-key:");
690 h.update(hostname.as_bytes());
691 h.update(b":");
692 h.update(username.as_bytes());
693 h.update(b":");
694 h.update(store_dir.to_string_lossy().as_bytes());
695 return Ok(h.finalize().into());
696 }
697 }
698
699 let local_seed_path = store_dir.parent().map(|p| p.join("machine_seed"));
719 let home = std::env::var("HOME")
720 .map(std::path::PathBuf::from)
721 .map_err(|_| KeyError::Crypto("HOME not set".to_string()))?;
722 let global_seed_path = home.join(".treeship").join("machine_seed");
723
724 let seed = if let Some(local) = local_seed_path.as_ref().filter(|p| p.exists()) {
725 fs::read_to_string(local).map_err(KeyError::Io)?
726 } else if global_seed_path.exists() {
727 fs::read_to_string(&global_seed_path).map_err(KeyError::Io)?
731 } else {
732 let mut bytes = [0u8; 32];
733 rand::thread_rng().fill_bytes(&mut bytes);
734 let seed_hex = hex_encode(&bytes);
735
736 let target = match local_seed_path.as_ref() {
740 Some(p) => {
741 let _ = fs::create_dir_all(p.parent().unwrap_or(Path::new(".")));
742 p.clone()
743 }
744 None => {
745 let _ = fs::create_dir_all(global_seed_path.parent().unwrap_or(Path::new(".")));
746 global_seed_path.clone()
747 }
748 };
749 fs::write(&target, &seed_hex).map_err(KeyError::Io)?;
750 #[cfg(unix)]
751 {
752 use std::os::unix::fs::PermissionsExt;
753 let _ = fs::set_permissions(&target, fs::Permissions::from_mode(0o600));
754 }
755 seed_hex
756 };
757
758 let mut h = Sha256::new();
759 h.update(b"treeship-machine-key-fallback:");
760 h.update(seed.trim().as_bytes());
761 h.update(b":");
762 h.update(store_dir.to_string_lossy().as_bytes());
763 Ok(h.finalize().into())
764}
765
766pub fn derive_machine_key_stable(store_dir: &Path) -> Result<[u8; 32], KeyError> {
770 if let Ok(id) = fs::read_to_string("/etc/machine-id") {
772 let trimmed = id.trim();
773 if !trimmed.is_empty() {
774 let mut h = Sha256::new();
775 h.update(b"treeship-machine-key-v2:");
776 h.update(trimmed.as_bytes());
777 h.update(b":");
778 h.update(store_dir.to_string_lossy().as_bytes());
779 return Ok(h.finalize().into());
780 }
781 }
782
783 #[cfg(target_os = "macos")]
786 {
787 if let Ok(output) = std::process::Command::new("ioreg")
788 .args(["-rd1", "-c", "IOPlatformExpertDevice"])
789 .output()
790 {
791 let stdout = String::from_utf8_lossy(&output.stdout);
792 for line in stdout.lines() {
793 if line.contains("IOPlatformSerialNumber") {
794 if let Some(serial) = line.split('"').nth(3) {
795 if !serial.is_empty() {
796 let mut h = Sha256::new();
797 h.update(b"treeship-machine-key-v2:");
798 h.update(serial.as_bytes());
799 h.update(b":");
800 h.update(store_dir.to_string_lossy().as_bytes());
801 return Ok(h.finalize().into());
802 }
803 }
804 }
805 }
806 }
807 }
808
809 let home = std::env::var("HOME")
812 .map(std::path::PathBuf::from)
813 .map_err(|_| KeyError::Crypto("HOME not set".to_string()))?;
814 let seed_dir = home.join(".treeship").join(".internal");
815 let _ = fs::create_dir_all(&seed_dir);
816 #[cfg(unix)]
817 {
818 use std::os::unix::fs::PermissionsExt;
819 let _ = fs::set_permissions(&seed_dir, fs::Permissions::from_mode(0o700));
820 }
821
822 let seed_path = seed_dir.join("machine_seed_v2");
823 let seed = if seed_path.exists() {
824 fs::read_to_string(&seed_path).map_err(KeyError::Io)?
825 } else {
826 let mut bytes = [0u8; 32];
827 rand::thread_rng().fill_bytes(&mut bytes);
828 let seed_hex = hex_encode(&bytes);
829 fs::write(&seed_path, &seed_hex).map_err(KeyError::Io)?;
830 #[cfg(unix)]
831 {
832 use std::os::unix::fs::PermissionsExt;
833 let _ = fs::set_permissions(&seed_path, fs::Permissions::from_mode(0o600));
834 }
835 seed_hex
836 };
837
838 let mut h = Sha256::new();
839 h.update(b"treeship-machine-key-v2-fallback:");
840 h.update(seed.trim().as_bytes());
841 h.update(b":");
842 h.update(store_dir.to_string_lossy().as_bytes());
843 Ok(h.finalize().into())
844}
845
846fn new_key_id() -> KeyId {
849 let mut b = [0u8; 8];
850 rand::thread_rng().fill_bytes(&mut b);
851 format!("key_{}", hex_encode(&b))
852}
853
854fn fingerprint(pub_key: &[u8]) -> String {
855 let h = Sha256::digest(pub_key);
856 hex_encode(&h[..8])
857}
858
859fn hex_encode(b: &[u8]) -> String {
860 b.iter().fold(String::new(), |mut s, byte| {
861 s.push_str(&format!("{:02x}", byte));
862 s
863 })
864}
865
866fn write_file_600(path: &Path, data: &[u8]) -> Result<(), KeyError> {
867 let mut f = fs::OpenOptions::new()
868 .write(true)
869 .create(true)
870 .truncate(true)
871 .open(path)?;
872 f.write_all(data)?;
873 #[cfg(unix)]
875 {
876 use std::os::unix::fs::PermissionsExt;
877 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
878 }
879 Ok(())
880}
881
882fn unix_now() -> u64 {
883 use std::time::{SystemTime, UNIX_EPOCH};
884 SystemTime::now()
885 .duration_since(UNIX_EPOCH)
886 .unwrap_or_default()
887 .as_secs()
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893
894 fn temp_dir_path() -> PathBuf {
895 let mut p = std::env::temp_dir();
896 p.push(format!("treeship-test-{}", {
897 let mut b = [0u8; 4];
898 rand::thread_rng().fill_bytes(&mut b);
899 hex_encode(&b)
900 }));
901 p
902 }
903
904 fn make_store() -> (Store, PathBuf) {
905 let dir = temp_dir_path();
906 let store = Store::open(&dir).unwrap();
907 (store, dir)
908 }
909
910 fn cleanup(dir: PathBuf) {
911 let _ = fs::remove_dir_all(dir);
912 }
913
914 #[test]
915 fn generate_key() {
916 let (store, dir) = make_store();
917 let info = store.generate(true).unwrap();
918 assert!(info.id.starts_with("key_"));
919 assert_eq!(info.algorithm, "ed25519");
920 assert!(!info.fingerprint.is_empty());
921 assert_eq!(info.public_key.len(), 32);
922 cleanup(dir);
923 }
924
925 #[test]
926 fn default_signer_works() {
927 let (store, dir) = make_store();
928 store.generate(true).unwrap();
929 let signer = store.default_signer().unwrap();
930 assert!(!signer.key_id().is_empty());
931 let pae = crate::attestation::pae("text/plain", b"test");
932 let sig = signer.sign(&pae).unwrap();
933 assert_eq!(sig.len(), 64);
934 cleanup(dir);
935 }
936
937 #[test]
938 fn encrypt_decrypt_roundtrip() {
939 let key = [42u8; 32];
940 let plaintext = b"super secret private key material here!";
941 let (enc, nonce) = aes_gcm_encrypt(&key, plaintext).unwrap();
942 let dec = aes_gcm_decrypt(&key, &enc, &nonce).unwrap();
943 assert_eq!(dec, plaintext);
944 }
945
946 #[test]
947 fn decrypt_wrong_key_fails() {
948 let key = [42u8; 32];
949 let wrong = [99u8; 32];
950 let (enc, nonce) = aes_gcm_encrypt(&key, b"secret").unwrap();
951 assert!(aes_gcm_decrypt(&wrong, &enc, &nonce).is_err());
952 }
953
954 #[test]
955 fn persist_and_reload() {
956 let (store, dir) = make_store();
957 let info = store.generate(true).unwrap();
958
959 let store2 = Store::open(&dir).unwrap();
961 let signer = store2.signer(&info.id).unwrap();
962 assert_eq!(signer.key_id(), info.id);
963
964 let verifier = {
967 use crate::attestation::Verifier;
968 use ed25519_dalek::VerifyingKey;
969 let pk_bytes: [u8; 32] = info.public_key.try_into().unwrap();
970 let vk = VerifyingKey::from_bytes(&pk_bytes).unwrap();
971 let mut v = Verifier::new(std::collections::HashMap::new());
972 v.add_key(info.id.clone(), vk);
973 v
974 };
975
976 use crate::attestation::sign;
977 use crate::statements::ActionStatement;
978 let stmt = ActionStatement::new("agent://test", "tool.call");
979 let pt = crate::statements::payload_type("action");
980 let signed = sign(&pt, &stmt, signer.as_ref()).unwrap();
981 verifier.verify(&signed.envelope).unwrap();
982
983 cleanup(dir);
984 }
985
986 #[test]
987 fn list_keys() {
988 let (store, dir) = make_store();
989 store.generate(true).unwrap();
990 store.generate(false).unwrap();
991
992 let keys = store.list().unwrap();
993 assert_eq!(keys.len(), 2);
994 assert_eq!(keys.iter().filter(|k| k.is_default).count(), 1);
995 cleanup(dir);
996 }
997
998 #[test]
999 fn no_default_key_errors() {
1000 let (store, dir) = make_store();
1001 assert!(store.default_signer().is_err());
1002 cleanup(dir);
1003 }
1004
1005 #[test]
1006 fn rotate_mints_successor_and_links_predecessor() {
1007 let (store, dir) = make_store();
1008 let pred = store.generate(true).unwrap();
1009 assert!(pred.valid_until.is_none(), "fresh key has no expiry");
1010 assert!(pred.successor_key_id.is_none(), "fresh key has no successor");
1011
1012 let result = store
1013 .rotate(None, std::time::Duration::from_secs(3600), true)
1014 .unwrap();
1015
1016 assert_eq!(result.predecessor.id, pred.id);
1018 assert!(result.predecessor.valid_until.is_some(),
1019 "predecessor must get valid_until after rotation");
1020 assert_eq!(result.predecessor.successor_key_id.as_deref(),
1021 Some(result.successor.id.as_str()),
1022 "predecessor must link forward to successor");
1023 assert!(!result.predecessor.is_default,
1024 "after rotation with set_default=true, predecessor is no longer default");
1025
1026 assert_ne!(result.successor.id, pred.id);
1028 assert!(result.successor.valid_until.is_none(), "successor has no expiry yet");
1029 assert!(result.successor.successor_key_id.is_none(), "successor is chain head");
1030 assert!(result.successor.is_default, "successor is the new default");
1031
1032 let listed = store.list().unwrap();
1034 assert_eq!(listed.len(), 2);
1035 let pred_listed = listed.iter().find(|k| k.id == pred.id).unwrap();
1036 assert!(pred_listed.valid_until.is_some());
1037 assert_eq!(pred_listed.successor_key_id.as_deref(),
1038 Some(result.successor.id.as_str()));
1039
1040 cleanup(dir);
1041 }
1042
1043 #[test]
1044 fn rotate_with_set_default_false_keeps_predecessor_active() {
1045 let (store, dir) = make_store();
1046 let pred = store.generate(true).unwrap();
1047
1048 let result = store
1049 .rotate(None, std::time::Duration::from_secs(3600), false)
1050 .unwrap();
1051
1052 assert!(result.predecessor.is_default);
1054 assert!(!result.successor.is_default);
1055 assert_eq!(store.default_key_id().unwrap(), pred.id);
1056
1057 cleanup(dir);
1058 }
1059
1060 #[test]
1061 fn rotate_predecessor_signing_still_works_during_grace_window() {
1062 let (store, dir) = make_store();
1063 let pred = store.generate(true).unwrap();
1064 let _ = store
1065 .rotate(None, std::time::Duration::from_secs(3600), true)
1066 .unwrap();
1067
1068 let signer = store.signer(&pred.id).unwrap();
1072 let pae = crate::attestation::pae("text/plain", b"grace-window-payload");
1073 let sig = signer.sign(&pae).unwrap();
1074 assert_eq!(sig.len(), 64);
1075
1076 cleanup(dir);
1077 }
1078
1079 #[test]
1080 fn rotate_refuses_to_rotate_already_rotated_key() {
1081 let (store, dir) = make_store();
1082 store.generate(true).unwrap();
1083 let r1 = store
1084 .rotate(None, std::time::Duration::from_secs(60), true)
1085 .unwrap();
1086
1087 let err = store
1090 .rotate(Some(&r1.predecessor.id),
1091 std::time::Duration::from_secs(60),
1092 true)
1093 .unwrap_err();
1094 match err {
1095 KeyError::Crypto(msg) => assert!(
1096 msg.contains("already been rotated"),
1097 "error must explain why: {msg}"
1098 ),
1099 other => panic!("expected Crypto error, got {other:?}"),
1100 }
1101 cleanup(dir);
1102 }
1103
1104 #[test]
1105 fn successor_chain_walks_forward() {
1106 let (store, dir) = make_store();
1107 let k0 = store.generate(true).unwrap();
1108 let r1 = store
1109 .rotate(None, std::time::Duration::from_secs(60), true)
1110 .unwrap();
1111 let r2 = store
1112 .rotate(None, std::time::Duration::from_secs(60), true)
1113 .unwrap();
1114
1115 let chain = store.successor_chain(&k0.id).unwrap();
1116 assert_eq!(chain, vec![k0.id.clone(), r1.successor.id.clone(), r2.successor.id.clone()],
1117 "chain must be ordered head -> tail");
1118
1119 let mid = store.successor_chain(&r1.successor.id).unwrap();
1121 assert_eq!(mid, vec![r1.successor.id.clone(), r2.successor.id.clone()]);
1122
1123 let tail = store.successor_chain(&r2.successor.id).unwrap();
1125 assert_eq!(tail, vec![r2.successor.id.clone()]);
1126
1127 cleanup(dir);
1128 }
1129
1130 #[test]
1131 fn valid_keys_at_filters_by_grace_window() {
1132 let (store, dir) = make_store();
1133 let _ = store.generate(true).unwrap();
1134 let result = store
1135 .rotate(None, std::time::Duration::from_secs(3600), true)
1136 .unwrap();
1137
1138 let now = unix_now();
1141 let valid_now = store.valid_keys_at(now).unwrap();
1142 assert_eq!(valid_now.len(), 2, "both predecessor (in grace) and successor should be valid");
1143
1144 let after_grace = unix_now() + 7200;
1146 let valid_after = store.valid_keys_at(after_grace).unwrap();
1147 assert_eq!(valid_after.len(), 1,
1148 "after grace window only successor remains valid");
1149 assert_eq!(valid_after[0].id, result.successor.id);
1150
1151 cleanup(dir);
1152 }
1153
1154 #[test]
1176 fn rotate_cache_reflects_stamped_predecessor_for_retry_safety() {
1177 let (store, dir) = make_store();
1178 let pred = store.generate(true).unwrap();
1179 let _ = store
1180 .rotate(None, std::time::Duration::from_secs(60), true)
1181 .unwrap();
1182
1183 let err = store
1188 .rotate(Some(&pred.id),
1189 std::time::Duration::from_secs(60),
1190 true)
1191 .unwrap_err();
1192 match err {
1193 KeyError::Crypto(msg) => assert!(
1194 msg.contains("already been rotated"),
1195 "cache should reflect stamped predecessor; got: {msg}"
1196 ),
1197 other => panic!("expected Crypto error, got {other:?}"),
1198 }
1199
1200 cleanup(dir);
1201 }
1202
1203 #[test]
1204 fn rotated_predecessor_pointing_at_missing_successor_surfaces_clear_error() {
1205 let (store, dir) = make_store();
1206 store.generate(true).unwrap();
1207 let result = store
1208 .rotate(None, std::time::Duration::from_secs(60), true)
1209 .unwrap();
1210
1211 let succ_path = store.entry_path(&result.successor.id);
1215 fs::remove_file(&succ_path).unwrap();
1216
1217 let store2 = Store::open(&dir).unwrap();
1222 let err = store2.successor_chain(&result.predecessor.id).unwrap_err();
1223 match err {
1224 KeyError::NotFound(id) => assert_eq!(id, result.successor.id),
1225 other => panic!("expected NotFound error, got {other:?}"),
1226 }
1227
1228 cleanup(dir);
1229 }
1230
1231 #[test]
1235 fn legacy_entry_without_lifecycle_fields_loads() {
1236 let (store, dir) = make_store();
1237 let info = store.generate(true).unwrap();
1238
1239 let path = store.entry_path(&info.id);
1242 let raw = fs::read(&path).unwrap();
1243 let mut json: serde_json::Value = serde_json::from_slice(&raw).unwrap();
1244 let obj = json.as_object_mut().unwrap();
1245 obj.remove("valid_until");
1246 obj.remove("successor_key_id");
1247 fs::write(&path, serde_json::to_vec_pretty(&json).unwrap()).unwrap();
1248
1249 let store2 = Store::open(&dir).unwrap();
1252 let listed = store2.list().unwrap();
1253 assert_eq!(listed.len(), 1);
1254 assert!(listed[0].valid_until.is_none(),
1255 "missing valid_until must default to None on legacy entry");
1256 assert!(listed[0].successor_key_id.is_none(),
1257 "missing successor_key_id must default to None on legacy entry");
1258 let signer = store2.default_signer().unwrap();
1259 assert_eq!(signer.key_id(), info.id);
1260
1261 cleanup(dir);
1262 }
1263}