tugger_windows_codesign/
signtool.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! Interface to `signtool.exe`. */
6
7use {
8    crate::signing::CodeSigningCertificate,
9    anyhow::{anyhow, Context, Result},
10    log::warn,
11    std::{
12        io::{BufRead, BufReader},
13        path::{Path, PathBuf},
14    },
15};
16
17#[cfg(target_family = "windows")]
18use tugger_windows::find_windows_sdk_current_arch_bin_path;
19
20/// Describes a timestamp server to use during signing.
21#[derive(Clone, Debug)]
22pub enum TimestampServer {
23    /// Simple timestamp server.
24    ///
25    /// Corresponds to `/t` flag to signtool.
26    Simple(String),
27
28    /// RFC 3161 timestamp server.
29    ///
30    /// First item is the URL, second is signing algorithm. Corresponds to signtool
31    /// flags `/tr` and `/td`.
32    Rfc3161(String, String),
33}
34
35#[cfg(target_family = "windows")]
36pub fn find_signtool() -> Result<PathBuf> {
37    let bin_path = find_windows_sdk_current_arch_bin_path(None).context("finding Windows SDK")?;
38
39    let p = bin_path.join("signtool.exe");
40
41    if p.exists() {
42        Ok(p)
43    } else {
44        Err(anyhow!(
45            "unable to locate signtool.exe in Windows SDK at {}",
46            bin_path.display()
47        ))
48    }
49}
50
51#[cfg(target_family = "unix")]
52pub fn find_signtool() -> Result<PathBuf> {
53    Err(anyhow!("finding signtool.exe only supported on Windows"))
54}
55
56/// Represents an invocation of `signtool.exe sign` to sign some files.
57#[derive(Clone, Debug)]
58pub struct SigntoolSign {
59    certificate: CodeSigningCertificate,
60    verbose: bool,
61    debug: bool,
62    description: Option<String>,
63    file_digest_algorithm: String,
64    timestamp_server: Option<TimestampServer>,
65    extra_args: Vec<String>,
66    sign_files: Vec<PathBuf>,
67}
68
69impl SigntoolSign {
70    /// Construct a new instance using a specified signing certificate.
71    pub fn new(certificate: CodeSigningCertificate) -> Self {
72        Self {
73            certificate,
74            verbose: false,
75            debug: false,
76            description: None,
77            file_digest_algorithm: "SHA256".to_string(),
78            timestamp_server: None,
79            extra_args: vec![],
80            sign_files: vec![],
81        }
82    }
83
84    /// Clone this instance, but not the list of files to sign.
85    #[must_use]
86    pub fn clone_settings(&self) -> Self {
87        Self {
88            certificate: self.certificate.clone(),
89            verbose: self.verbose,
90            debug: self.debug,
91            description: self.description.clone(),
92            file_digest_algorithm: self.file_digest_algorithm.clone(),
93            timestamp_server: self.timestamp_server.clone(),
94            extra_args: self.extra_args.clone(),
95            sign_files: vec![],
96        }
97    }
98
99    /// Run signtool in verbose mode.
100    ///
101    /// Activates the `/v` flag.
102    pub fn verbose(&mut self) -> &mut Self {
103        self.verbose = true;
104        self
105    }
106
107    /// Run signtool in debug mode.
108    ///
109    /// Activates the `/debug` flag.
110    pub fn debug(&mut self) -> &mut Self {
111        self.debug = true;
112        self
113    }
114
115    /// Set the description of the content to be signed.
116    ///
117    /// This is passed into the `/d` argument.
118    pub fn description(&mut self, description: impl ToString) -> &mut Self {
119        self.description = Some(description.to_string());
120        self
121    }
122
123    /// Set the file digest algorithm to use.
124    ///
125    /// This is passed into the `/fd` argument.
126    pub fn file_digest_algorithm(&mut self, algorithm: impl ToString) -> &mut Self {
127        self.file_digest_algorithm = algorithm.to_string();
128        self
129    }
130
131    /// Set the timestamp server to use when signing.
132    pub fn timestamp_server(&mut self, server: TimestampServer) -> &mut Self {
133        self.timestamp_server = Some(server);
134        self
135    }
136
137    /// Set extra arguments to pass to signtool.
138    ///
139    /// Ideally this would not be used. Consider adding a separate API for use cases
140    /// that require this.
141    pub fn extra_args(&mut self, extra_args: impl Iterator<Item = impl ToString>) -> &mut Self {
142        self.extra_args = extra_args.map(|x| x.to_string()).collect::<_>();
143        self
144    }
145
146    /// Mark a file path as to be signed.
147    pub fn sign_file(&mut self, path: impl AsRef<Path>) -> &mut Self {
148        self.sign_files.push(path.as_ref().to_path_buf());
149        self
150    }
151
152    /// Run `signtool sign` with requested options.
153    pub fn run(&self) -> Result<()> {
154        let signtool = find_signtool().context("locating signtool.exe")?;
155
156        let mut args = vec!["sign".to_string()];
157
158        if self.verbose {
159            args.push("/v".to_string());
160        }
161
162        if self.debug {
163            args.push("/debug".to_string());
164        }
165
166        match &self.certificate {
167            CodeSigningCertificate::Auto => {
168                args.push("/a".to_string());
169            }
170            CodeSigningCertificate::File(file) => {
171                args.push("/f".to_string());
172                args.push(file.path().display().to_string());
173                if let Some(password) = file.password() {
174                    args.push("/p".to_string());
175                    args.push(password.to_string());
176                }
177            }
178            CodeSigningCertificate::SubjectName(store, sn) => {
179                args.push("/s".to_string());
180                args.push(store.as_ref().to_string());
181                args.push("/n".to_string());
182                args.push(sn.to_string());
183            }
184            CodeSigningCertificate::Sha1Thumbprint(store, sha1) => {
185                args.push("/s".to_string());
186                args.push(store.as_ref().to_string());
187                args.push("/sha1".to_string());
188                args.push(sha1.to_string());
189            }
190        }
191
192        if let Some(description) = &self.description {
193            args.push("/d".to_string());
194            args.push(description.to_string());
195        }
196
197        args.push("/fd".to_string());
198        args.push(self.file_digest_algorithm.clone());
199
200        if let Some(server) = &self.timestamp_server {
201            match server {
202                TimestampServer::Simple(url) => {
203                    args.push("/t".to_string());
204                    args.push(url.to_string());
205                }
206                TimestampServer::Rfc3161(url, algorithm) => {
207                    args.push("/tr".to_string());
208                    args.push(url.to_string());
209                    args.push("/td".to_string());
210                    args.push(algorithm.to_string());
211                }
212            }
213        }
214
215        args.extend(self.extra_args.iter().cloned());
216
217        args.extend(self.sign_files.iter().map(|p| p.display().to_string()));
218
219        let command = duct::cmd(signtool, args)
220            .stderr_to_stdout()
221            .reader()
222            .context("running signtool")?;
223        {
224            let reader = BufReader::new(&command);
225            for line in reader.lines() {
226                warn!("{}", line?);
227            }
228        }
229
230        let output = command
231            .try_wait()?
232            .ok_or_else(|| anyhow!("unable to wait on command"))?;
233        if output.status.success() {
234            Ok(())
235        } else {
236            Err(anyhow!("error running signtool"))
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use {
244        super::*,
245        crate::{
246            create_self_signed_code_signing_certificate,
247            signing::{certificate_to_pfx, FileBasedCodeSigningCertificate},
248        },
249        tugger_common::testutil::*,
250    };
251
252    #[test]
253    fn test_find_signtool() -> Result<()> {
254        let res = find_signtool();
255
256        // Rust development environments on Windows should have the Windows SDK available.
257        if cfg!(target_family = "windows") {
258            res?;
259        } else {
260            assert!(res.is_err());
261        }
262
263        Ok(())
264    }
265
266    #[test]
267    fn test_sign_executable() -> Result<()> {
268        if cfg!(target_family = "unix") {
269            eprintln!("skipping test because only works on Windows");
270            return Ok(());
271        }
272
273        let temp_path = DEFAULT_TEMP_DIR.path().join("test_sign_executable");
274        std::fs::create_dir(&temp_path)?;
275
276        let cert = create_self_signed_code_signing_certificate("tugger@example.com")?;
277        let pfx_data = certificate_to_pfx(&cert, "some_password", "cert_name")?;
278
279        let key_path = temp_path.join("signing.pfx");
280        std::fs::write(&key_path, pfx_data)?;
281
282        // We sign the current test executable because why not.
283        let sign_path = temp_path.join("test.exe");
284        let current_exe = std::env::current_exe()?;
285        std::fs::copy(current_exe, &sign_path)?;
286
287        let mut c = FileBasedCodeSigningCertificate::new(&key_path);
288        c.set_password("some_password");
289
290        SigntoolSign::new(c.into())
291            .verbose()
292            .debug()
293            .description("tugger test executable")
294            .file_digest_algorithm("sha256")
295            .sign_file(&sign_path)
296            .run()?;
297
298        Ok(())
299    }
300}