Skip to main content

cargo_codesign/platform/
linux.rs

1use crate::subprocess::{run, SubprocessError};
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, thiserror::Error)]
5pub enum LinuxSignError {
6    #[error("subprocess failed: {0}")]
7    Subprocess(#[from] SubprocessError),
8    #[error("signing failed for {path}: {detail}")]
9    SigningFailed { path: PathBuf, detail: String },
10    #[error("tool not found: {0}")]
11    ToolNotFound(String),
12    #[error("io error: {0}")]
13    Io(#[from] std::io::Error),
14}
15
16pub struct SignOpts<'a> {
17    pub verbose: bool,
18    pub output: Option<&'a Path>,
19}
20
21/// Compute the output path: use custom output if provided, else append `ext` to the archive path.
22fn resolve_output(archive: &Path, ext: &str, custom: Option<&Path>) -> PathBuf {
23    custom.map_or_else(
24        || {
25            let mut p = archive.as_os_str().to_owned();
26            p.push(ext);
27            PathBuf::from(p)
28        },
29        Path::to_path_buf,
30    )
31}
32
33/// Sign an archive using cosign keyless OIDC.
34pub fn sign_cosign(archive: &Path, opts: &SignOpts<'_>) -> Result<PathBuf, LinuxSignError> {
35    let archive_str = archive.to_string_lossy().to_string();
36    let bundle_path = resolve_output(archive, ".bundle", opts.output);
37    let bundle_str = bundle_path.to_string_lossy().to_string();
38
39    let output = run(
40        "cosign",
41        &["sign-blob", "--bundle", &bundle_str, &archive_str, "--yes"],
42        opts.verbose,
43    )?;
44    if !output.success {
45        return Err(LinuxSignError::SigningFailed {
46            path: archive.to_path_buf(),
47            detail: output.stderr,
48        });
49    }
50    Ok(bundle_path)
51}
52
53/// Sign an archive using minisign with a key from an env var.
54pub fn sign_minisign(
55    archive: &Path,
56    key_content: &str,
57    opts: &SignOpts<'_>,
58) -> Result<PathBuf, LinuxSignError> {
59    let archive_str = archive.to_string_lossy().to_string();
60    let sig_path = resolve_output(archive, ".minisig", opts.output);
61    let sig_str = sig_path.to_string_lossy().to_string();
62
63    // Write key to temp file
64    let key_dir = tempfile::TempDir::new()?;
65    let key_path = key_dir.path().join("minisign.key");
66    std::fs::write(&key_path, key_content)?;
67    let key_str = key_path.to_string_lossy().to_string();
68
69    let output = run(
70        "minisign",
71        &["-S", "-s", &key_str, "-m", &archive_str, "-x", &sig_str],
72        opts.verbose,
73    )?;
74
75    // Key file is cleaned up when key_dir drops
76
77    if !output.success {
78        return Err(LinuxSignError::SigningFailed {
79            path: archive.to_path_buf(),
80            detail: output.stderr,
81        });
82    }
83    Ok(sig_path)
84}
85
86/// Sign an archive using GPG detached signature.
87pub fn sign_gpg(archive: &Path, opts: &SignOpts<'_>) -> Result<PathBuf, LinuxSignError> {
88    let archive_str = archive.to_string_lossy().to_string();
89    let sig_path = resolve_output(archive, ".sig", opts.output);
90    let sig_str = sig_path.to_string_lossy().to_string();
91
92    let output = run(
93        "gpg",
94        &["--detach-sign", "--output", &sig_str, &archive_str],
95        opts.verbose,
96    )?;
97    if !output.success {
98        return Err(LinuxSignError::SigningFailed {
99            path: archive.to_path_buf(),
100            detail: output.stderr,
101        });
102    }
103    Ok(sig_path)
104}
105
106/// Verify a cosign bundle.
107pub fn verify_cosign(archive: &Path, bundle: &Path, verbose: bool) -> Result<(), LinuxSignError> {
108    let archive_str = archive.to_string_lossy().to_string();
109    let bundle_str = bundle.to_string_lossy().to_string();
110
111    let output = run(
112        "cosign",
113        &["verify-blob", "--bundle", &bundle_str, &archive_str],
114        verbose,
115    )?;
116    if !output.success {
117        return Err(LinuxSignError::SigningFailed {
118            path: archive.to_path_buf(),
119            detail: output.stderr,
120        });
121    }
122    Ok(())
123}
124
125/// Verify a minisign signature.
126pub fn verify_minisign(
127    archive: &Path,
128    sig_path: &Path,
129    public_key: &str,
130    verbose: bool,
131) -> Result<(), LinuxSignError> {
132    let archive_str = archive.to_string_lossy().to_string();
133    let sig_str = sig_path.to_string_lossy().to_string();
134
135    let output = run(
136        "minisign",
137        &["-V", "-P", public_key, "-m", &archive_str, "-x", &sig_str],
138        verbose,
139    )?;
140    if !output.success {
141        return Err(LinuxSignError::SigningFailed {
142            path: archive.to_path_buf(),
143            detail: output.stderr,
144        });
145    }
146    Ok(())
147}
148
149/// Verify a GPG detached signature.
150pub fn verify_gpg(archive: &Path, sig_path: &Path, verbose: bool) -> Result<(), LinuxSignError> {
151    let archive_str = archive.to_string_lossy().to_string();
152    let sig_str = sig_path.to_string_lossy().to_string();
153
154    let output = run("gpg", &["--verify", &sig_str, &archive_str], verbose)?;
155    if !output.success {
156        return Err(LinuxSignError::SigningFailed {
157            path: archive.to_path_buf(),
158            detail: output.stderr,
159        });
160    }
161    Ok(())
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn default_cosign_output_has_bundle_extension() {
170        let result = resolve_output(Path::new("release.tar.gz"), ".bundle", None);
171        assert_eq!(result, PathBuf::from("release.tar.gz.bundle"));
172    }
173
174    #[test]
175    fn default_minisig_output_has_minisig_extension() {
176        let result = resolve_output(Path::new("release.tar.gz"), ".minisig", None);
177        assert_eq!(result, PathBuf::from("release.tar.gz.minisig"));
178    }
179
180    #[test]
181    fn default_gpg_output_has_sig_extension() {
182        let result = resolve_output(Path::new("release.tar.gz"), ".sig", None);
183        assert_eq!(result, PathBuf::from("release.tar.gz.sig"));
184    }
185
186    #[test]
187    fn custom_output_overrides_default() {
188        let custom = PathBuf::from("custom.sig");
189        let result = resolve_output(Path::new("release.tar.gz"), ".bundle", Some(&custom));
190        assert_eq!(result, custom);
191    }
192}