1use std::path::Path;
4use std::process::{Command, Stdio};
5
6use anyhow::{Context, Result, bail};
7use nixfleet_proto::FleetResolved;
8use tempfile::NamedTempFile;
9
10use crate::canonicalize_resolved;
11
12pub(crate) fn sign(cmd: &str, canonical: &[u8]) -> Result<Vec<u8>> {
13 let input = NamedTempFile::new().context("create tempfile for canonical bytes")?;
14 let output = NamedTempFile::new().context("create tempfile for signature")?;
15
16 std::fs::write(input.path(), canonical).context("write canonical bytes to tempfile")?;
17 std::fs::write(output.path(), b"").ok();
19
20 tracing::info!("sign hook");
21 let status = Command::new("sh")
22 .arg("-c")
23 .arg(cmd)
24 .env("NIXFLEET_INPUT", input.path())
25 .env("NIXFLEET_OUTPUT", output.path())
26 .stdin(Stdio::null())
27 .status()
28 .with_context(|| format!("invoke sign hook ({cmd:?})"))?;
29 if !status.success() {
30 bail!(
31 "sign hook exited {} ({:?})",
32 status.code().unwrap_or(-1),
33 cmd,
34 );
35 }
36
37 let sig = std::fs::read(output.path()).context("read signature output")?;
38 if sig.is_empty() {
39 bail!("sign hook produced 0-byte signature - refusing to publish");
40 }
41 Ok(sig)
42}
43
44pub(crate) fn smoke_verify(canonical: &[u8], signature: &[u8]) -> Result<()> {
47 let parsed: FleetResolved = serde_json::from_slice(canonical)
48 .context("smoke verify: canonical bytes don't parse as FleetResolved")?;
49 let recanonical =
50 canonicalize_resolved(&parsed).context("smoke verify: re-canonicalize failed")?;
51 if recanonical.as_bytes() != canonical {
52 bail!("smoke verify: canonicalization is not byte-stable round-trip");
53 }
54 if signature.is_empty() {
55 bail!("smoke verify: empty signature");
56 }
57 tracing::info!(sig_len = signature.len(), "smoke verify ok");
58 Ok(())
59}
60
61pub(crate) fn write_release(
62 release_dir: &Path,
63 artifact_name: &str,
64 canonical: &[u8],
65 signature: &[u8],
66) -> Result<()> {
67 std::fs::create_dir_all(release_dir)
68 .with_context(|| format!("create release dir {}", release_dir.display()))?;
69 let artifact_path = release_dir.join(artifact_name);
70 let signature_path = release_dir.join(format!("{artifact_name}.sig"));
71 atomic_write(&artifact_path, canonical)?;
72 atomic_write(&signature_path, signature)?;
73 Ok(())
74}
75
76fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
77 let dir = path.parent().unwrap_or_else(|| Path::new("."));
78 let mut tmp = tempfile::NamedTempFile::new_in(dir)
79 .with_context(|| format!("tempfile in {}", dir.display()))?;
80 use std::io::Write;
81 tmp.write_all(bytes).context("write release tempfile")?;
82 tmp.persist(path)
83 .with_context(|| format!("rename tempfile to {}", path.display()))?;
84 Ok(())
85}