use anyhow::{bail, Context, Result};
use chacha20poly1305::aead::{Aead, KeyInit};
use chacha20poly1305::{XChaCha20Poly1305, XNonce};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use std::path::Path;
const SIG_MAGIC: &str = "CAVSBSG1";
const ENC_MAGIC: &[u8; 8] = b"CAVSENC1";
fn read_hex_key(path: &Path, expected_len: usize) -> Result<Vec<u8>> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("cannot read key file {}", path.display()))?;
let hex = raw.trim();
if hex.len() != expected_len * 2 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
bail!(
"{} does not look like a {expected_len}-byte hex key",
path.display()
);
}
Ok((0..expected_len)
.map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap())
.collect())
}
fn artifact_digest(path: &Path) -> Result<[u8; 32]> {
let mut hasher = cavs_hash::Hasher::new();
let mut file =
std::fs::File::open(path).with_context(|| format!("cannot open {}", path.display()))?;
let mut buf = vec![0u8; 1 << 20];
loop {
use std::io::Read;
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(hasher.finalize())
}
pub fn sign(artifact: &Path, key: &Path, out: Option<&Path>) -> Result<()> {
let secret = read_hex_key(key, 32)?;
let signing = SigningKey::from_bytes(&secret.try_into().unwrap());
let digest = artifact_digest(artifact)?;
let signature = signing.sign(&digest);
let sig_hex: String = signature
.to_bytes()
.iter()
.map(|b| format!("{b:02x}"))
.collect();
let default_out = artifact.with_extension(format!(
"{}.sig",
artifact
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default()
));
let out = out.unwrap_or(&default_out);
std::fs::write(out, format!("{SIG_MAGIC}\n{sig_hex}\n"))?;
println!("signed : {} → {}", artifact.display(), out.display());
println!(
"pubkey : verify with `cavs build verify {} --pub <key.pub>`",
artifact.display()
);
Ok(())
}
pub fn verify(artifact: &Path, pubkey: &Path, sig: Option<&Path>) -> Result<()> {
let public = read_hex_key(pubkey, 32)?;
let verifying = VerifyingKey::from_bytes(&public.clone().try_into().unwrap())
.map_err(|e| anyhow::anyhow!("CAVS-E-SIGNATURE-INVALID: bad public key: {e}"))?;
let default_sig = artifact.with_extension(format!(
"{}.sig",
artifact
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default()
));
let sig_path = sig.unwrap_or(&default_sig);
let raw = std::fs::read_to_string(sig_path)
.with_context(|| format!("cannot read signature {}", sig_path.display()))?;
let mut lines = raw.lines();
if lines.next() != Some(SIG_MAGIC) {
bail!(
"CAVS-E-SIGNATURE-INVALID: {} is not a CAVS build signature",
sig_path.display()
);
}
let sig_hex = lines.next().unwrap_or_default().trim();
if sig_hex.len() != 128 {
bail!("CAVS-E-SIGNATURE-INVALID: truncated signature");
}
let sig_bytes: Vec<u8> = (0..64)
.map(|i| u8::from_str_radix(&sig_hex[i * 2..i * 2 + 2], 16))
.collect::<Result<_, _>>()
.map_err(|_| anyhow::anyhow!("CAVS-E-SIGNATURE-INVALID: non-hex signature"))?;
let signature = Signature::from_bytes(&sig_bytes.try_into().unwrap());
let digest = artifact_digest(artifact)?;
match verifying.verify(&digest, &signature) {
Ok(()) => {
println!(
"verify : OK — {} matches its signature",
artifact.display()
);
Ok(())
}
Err(_) => bail!(
"CAVS-E-SIGNATURE-INVALID: {} does NOT match {} (artifact or signature tampered)",
artifact.display(),
sig_path.display()
),
}
}
pub fn generate_content_key(out: &Path) -> Result<()> {
use rand_core::{OsRng, RngCore};
let mut key = [0u8; 32];
OsRng.fill_bytes(&mut key);
let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
std::fs::write(out, format!("{hex}\n"))?;
println!(
"content key : {} (keep private; anyone with it can decrypt)",
out.display()
);
Ok(())
}
pub fn encrypt(artifact: &Path, key: &Path, out: &Path) -> Result<()> {
use rand_core::{OsRng, RngCore};
let secret = read_hex_key(key, 32)?;
let cipher = XChaCha20Poly1305::new(secret.as_slice().into());
let mut nonce = [0u8; 24];
OsRng.fill_bytes(&mut nonce);
let plaintext = std::fs::read(artifact)?;
let ciphertext = cipher
.encrypt(XNonce::from_slice(&nonce), plaintext.as_slice())
.map_err(|_| anyhow::anyhow!("encryption failed"))?;
let mut body = Vec::with_capacity(8 + 24 + ciphertext.len());
body.extend_from_slice(ENC_MAGIC);
body.extend_from_slice(&nonce);
body.extend_from_slice(&ciphertext);
std::fs::write(out, body)?;
println!(
"encrypt : {} → {} (XChaCha20-Poly1305; local at-rest protection, not DRM)",
artifact.display(),
out.display()
);
Ok(())
}
pub fn decrypt(artifact: &Path, key: &Path, out: &Path) -> Result<()> {
let secret = read_hex_key(key, 32)?;
let cipher = XChaCha20Poly1305::new(secret.as_slice().into());
let body = std::fs::read(artifact)?;
if body.len() < 8 + 24 || &body[..8] != ENC_MAGIC {
bail!("{} is not a CAVS-encrypted artifact", artifact.display());
}
let nonce = XNonce::from_slice(&body[8..32]);
let plaintext = cipher
.decrypt(nonce, &body[32..])
.map_err(|_| anyhow::anyhow!("decryption failed: wrong key or tampered data"))?;
std::fs::write(out, plaintext)?;
println!("decrypt : {} → {}", artifact.display(), out.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn keypair(dir: &Path) -> (std::path::PathBuf, std::path::PathBuf) {
use rand_core::OsRng;
let key = SigningKey::generate(&mut OsRng);
let secret: String = key.to_bytes().iter().map(|b| format!("{b:02x}")).collect();
let public: String = key
.verifying_key()
.to_bytes()
.iter()
.map(|b| format!("{b:02x}"))
.collect();
let sk = dir.join("cavs.key");
let pk = dir.join("cavs.key.pub");
std::fs::write(&sk, secret).unwrap();
std::fs::write(&pk, public).unwrap();
(sk, pk)
}
#[test]
fn sign_verify_and_tamper_detection() {
let dir = tempfile::tempdir().unwrap();
let (sk, pk) = keypair(dir.path());
let artifact = dir.path().join("build.cavs");
std::fs::write(&artifact, b"release payload").unwrap();
let sig = dir.path().join("build.cavs.sig");
sign(&artifact, &sk, Some(&sig)).unwrap();
verify(&artifact, &pk, Some(&sig)).unwrap();
std::fs::write(&artifact, b"release payloaX").unwrap();
let err = verify(&artifact, &pk, Some(&sig)).unwrap_err().to_string();
assert!(err.contains("CAVS-E-SIGNATURE-INVALID"), "{err}");
}
#[test]
fn encrypt_round_trip_and_wrong_key() {
let dir = tempfile::tempdir().unwrap();
let k1 = dir.path().join("a.key");
let k2 = dir.path().join("b.key");
generate_content_key(&k1).unwrap();
generate_content_key(&k2).unwrap();
let artifact = dir.path().join("build.cavs");
std::fs::write(&artifact, vec![7u8; 100_000]).unwrap();
let enc = dir.path().join("build.enc");
let dec = dir.path().join("build.dec");
encrypt(&artifact, &k1, &enc).unwrap();
assert_ne!(std::fs::read(&enc).unwrap()[32..132], vec![7u8; 100][..]);
decrypt(&enc, &k1, &dec).unwrap();
assert_eq!(std::fs::read(&dec).unwrap(), vec![7u8; 100_000]);
assert!(decrypt(&enc, &k2, &dec).is_err());
}
}