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
21fn 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
33pub 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
53pub 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 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 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
86pub 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
106pub 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
125pub 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
149pub 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}