Skip to main content

andysu_cli/
keys.rs

1//! `andysu keygen` and `andysu cert`.
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use andysu_core::keys::{PublicKey, SecretKey};
7use andysu_core::sig::Cert;
8
9use crate::{random, write, R};
10
11fn write_private(p: &Path, pem: &str) -> R<()> {
12    if p.exists() {
13        return Err(format!("{} exists; refusing to overwrite a private key", p.display()));
14    }
15    write(p, pem.as_bytes())?;
16    #[cfg(unix)]
17    {
18        use std::os::unix::fs::PermissionsExt;
19        fs::set_permissions(p, fs::Permissions::from_mode(0o600)).map_err(|e| e.to_string())?;
20    }
21    Ok(())
22}
23
24pub struct Generated {
25    pub private: PathBuf,
26    pub public: PathBuf,
27    pub id: String,
28}
29
30/// Root key pair: `<name>-root.key` (keep offline) and `<name>-public.pem` (compiled into hosts).
31pub fn keygen(dir: &Path, name: &str) -> R<Generated> {
32    let sk = SecretKey::from_seed(&random::<32>());
33    let private = dir.join(format!("{name}-root.key"));
34    let public = dir.join(format!("{name}-public.pem"));
35    if public.exists() {
36        return Err(format!("{} exists", public.display()));
37    }
38    write_private(&private, &sk.to_pem())?;
39    write(&public, sk.public().to_pem().as_bytes())?;
40    Ok(Generated { private, public, id: sk.public().id() })
41}
42
43pub fn load_secret(p: &Path) -> R<SecretKey> {
44    let s = fs::read_to_string(p).map_err(|e| format!("read {}: {e}", p.display()))?;
45    SecretKey::from_pem(&s).map_err(|e| e.to_string())
46}
47
48pub fn load_public(p: &Path) -> R<PublicKey> {
49    let s = fs::read_to_string(p).map_err(|e| format!("read {}: {e}", p.display()))?;
50    PublicKey::parse_any(&s).map_err(|e| e.to_string())
51}
52
53/// A short-lived release key certified by the root: `<name>-release.key` + `<name>-release.cert.json`.
54pub fn release_key(dir: &Path, name: &str, root: &SecretKey, not_before: i64, days: i64) -> R<(PathBuf, PathBuf)> {
55    let sk = SecretKey::from_seed(&random::<32>());
56    let kp = dir.join(format!("{name}-release.key"));
57    let cp = dir.join(format!("{name}-release.cert.json"));
58    write_private(&kp, &sk.to_pem())?;
59    let cert = Cert::issue(root, &sk.public(), not_before, not_before + days * 86400);
60    write(&cp, serde_json::to_string_pretty(&cert).unwrap().as_bytes())?;
61    Ok((kp, cp))
62}
63
64pub fn load_cert(p: &Path) -> R<Cert> {
65    serde_json::from_slice(&crate::read(p)?).map_err(|e| format!("{}: {e}", p.display()))
66}
67
68/// What a detached file signature covers: a domain-separated digest, so that a file signature can
69/// never be taken for a manifest signature made with the same key.
70fn file_message(path: &Path) -> R<Vec<u8>> {
71    let (sha, size) = andysu_core::hash::sha256_file(path).map_err(|e| format!("{}: {e}", path.display()))?;
72    Ok(format!("andysu-file-v1\0{sha}\0{size}").into_bytes())
73}
74
75/// `andysu sign-file`: a detached signature envelope (`<file>.sig`) over the file's SHA-256.
76pub fn sign_file(path: &Path, key: &SecretKey, cert: Option<Cert>) -> R<PathBuf> {
77    let env = andysu_core::sig::sign(&file_message(path)?, key, cert).map_err(|e| e.to_string())?;
78    let out = PathBuf::from(format!("{}.sig", path.display()));
79    write(&out, &env)?;
80    Ok(out)
81}
82
83/// `andysu verify-file`: checks `<file>.sig` (or `sig`) against the given roots.
84pub fn verify_file(path: &Path, sig: Option<&Path>, roots: &[PublicKey], now: i64) -> R<andysu_core::sig::Verified> {
85    let sig = sig.map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from(format!("{}.sig", path.display())));
86    let env = crate::read(&sig)?;
87    andysu_core::sig::verify(&file_message(path)?, &env, roots, now).map_err(|e| format!("{}: {e}", path.display()))
88}