tugger_windows_codesign/
signing.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/*! Functionality for signing binaries on Windows. */
6
7use {
8    crate::SystemStore,
9    anyhow::{anyhow, Result},
10    std::{
11        io::Read,
12        path::{Path, PathBuf},
13    },
14};
15
16/// Represents a code signing certificate backed by a file.
17///
18/// Often a `.pfx` file.
19#[derive(Clone, Debug)]
20pub struct FileBasedCodeSigningCertificate {
21    /// Path to the certificate file.
22    path: PathBuf,
23    /// Password used to unlock the certificate.
24    password: Option<String>,
25}
26
27impl FileBasedCodeSigningCertificate {
28    /// Construct an instance from a path.
29    ///
30    /// No validation is done that the path exists.
31    pub fn new(path: impl AsRef<Path>) -> Self {
32        Self {
33            path: path.as_ref().to_path_buf(),
34            password: None,
35        }
36    }
37
38    pub fn path(&self) -> &Path {
39        &self.path
40    }
41
42    pub fn password(&self) -> &Option<String> {
43        &self.password
44    }
45
46    pub fn set_password(&mut self, password: impl ToString) {
47        self.password = Some(password.to_string());
48    }
49}
50
51/// Represents a code signing certificate used to sign binaries on Windows.
52///
53/// This only represents the location of the certificate. It is possible
54/// for instances to refer to entities that don't exist.
55#[derive(Clone, Debug)]
56pub enum CodeSigningCertificate {
57    /// Select the best available signing certificate.
58    Auto,
59
60    /// An x509 certificate backed by a filesystem file.
61    File(FileBasedCodeSigningCertificate),
62
63    /// An x509 certificate specified by its subject name or substring thereof.
64    SubjectName(SystemStore, String),
65
66    /// A certificate specified by its store and SHA-1 thumbprint.
67    ///
68    /// This is the most reliable way to specify a certificate in the Windows
69    /// certificate store because thumbprints should be unique.
70    Sha1Thumbprint(SystemStore, String),
71}
72
73impl From<FileBasedCodeSigningCertificate> for CodeSigningCertificate {
74    fn from(v: FileBasedCodeSigningCertificate) -> Self {
75        Self::File(v)
76    }
77}
78
79/// Create parameters for a self-signed x509 certificate suitable for code signing on Windows.
80///
81/// The self-signed certificate mimics what the powershell
82/// `New-SelfSignedCertificate -DnsName <subject_name> -Type CodeSigning -KeyAlgorithm ECDSA_nistP256`
83/// would do.
84pub fn create_self_signed_code_signing_certificate_params(
85    subject_name: &str,
86) -> rcgen::CertificateParams {
87    let mut params = rcgen::CertificateParams::new(vec![]);
88    params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
89    params.key_identifier_method = rcgen::KeyIdMethod::Sha256;
90    params.distinguished_name = rcgen::DistinguishedName::new();
91    params
92        .subject_alt_names
93        .push(rcgen::SanType::DnsName(subject_name.to_string()));
94    params
95        .distinguished_name
96        .push(rcgen::DnType::CommonName, subject_name);
97    params
98        .extended_key_usages
99        .push(rcgen::ExtendedKeyUsagePurpose::CodeSigning);
100    params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
101    // The default is thousands of years in the future. Let's use something more reasonable.
102    params.not_after = time::OffsetDateTime::now_utc()
103        .checked_add(time::Duration::days(365))
104        .unwrap();
105
106    // KeyUsage(KeyUsage { flags: 1 })
107    let mut key_usage =
108        rcgen::CustomExtension::from_oid_content(&[2, 5, 29, 15], vec![3, 2, 7, 128]);
109    key_usage.set_criticality(true);
110    params.custom_extensions.push(key_usage);
111
112    params
113}
114
115pub fn create_self_signed_code_signing_certificate(
116    subject_name: &str,
117) -> std::result::Result<rcgen::Certificate, rcgen::RcgenError> {
118    let params = create_self_signed_code_signing_certificate_params(subject_name);
119
120    rcgen::Certificate::from_params(params)
121}
122
123/// Serialize a certificate to a PKCS #12 `.pfx` file.
124///
125/// This file format is what is used by `signtool` and other Microsoft tools.
126pub fn certificate_to_pfx(
127    cert: &rcgen::Certificate,
128    password: &str,
129    name: &str,
130) -> Result<Vec<u8>> {
131    let cert_der = cert.serialize_der()?;
132    let key_der = cert.serialize_private_key_der();
133
134    let pfx = p12::PFX::new(&cert_der, &key_der, None, password, name)
135        .ok_or_else(|| anyhow!("unable to convert to pfx"))?;
136
137    let buffer = yasna::construct_der(|writer| {
138        pfx.write(writer);
139    });
140
141    Ok(buffer)
142}
143
144/// MSI file magic.
145const CFB_MAGIC_NUMBER: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
146
147/// Whether the bytes passed in look like a file header for a format that is signable.
148///
149/// The passed buffer must be at least 16 bytes long.
150///
151/// This could yield false positives.
152#[allow(clippy::if_same_then_else)]
153pub fn is_signable_binary_header(data: &[u8]) -> bool {
154    if data.len() < 16 {
155        false
156    // DOS header.
157    } else if data[0] == 0x4d && data[1] == 0x5a {
158        true
159    } else {
160        data[0..CFB_MAGIC_NUMBER.len()] == CFB_MAGIC_NUMBER
161    }
162}
163
164/// Determine whether a given filesystem path is signable.
165///
166/// This effectively answers whether the given path is a PE or MSI.
167pub fn is_file_signable(path: impl AsRef<Path>) -> Result<bool> {
168    let path = path.as_ref();
169
170    if path.metadata()?.len() < 16 {
171        return Ok(false);
172    }
173
174    let mut fh = std::fs::File::open(path)?;
175    let mut buffer: [u8; 16] = [0; 16];
176    fh.read_exact(&mut buffer)?;
177
178    Ok(is_signable_binary_header(&buffer))
179}
180
181#[cfg(test)]
182mod tests {
183
184    use {super::*, anyhow::Result, der_parser::oid, x509_parser::prelude::*};
185
186    // PEM encoded key pair generated via Powershell.
187    const POWERSHELL_CERTIFICATE_PUBLIC_PEM: &str = "-----BEGIN CERTIFICATE-----\n\
188        MIIBnzCCAUagAwIBAgIQSE/jLE4ZZYtHZ1e/Uh5IKTAKBggqhkjOPQQDAjAeMRww\n\
189        GgYDVQQDDBN0ZXN0aW5nQGV4YW1wbGUuY29tMB4XDTIwMTEyNjIxMjIyOFoXDTIx\n\
190        MTEyNjIxNDIyOFowHjEcMBoGA1UEAwwTdGVzdGluZ0BleGFtcGxlLmNvbTBZMBMG\n\
191        ByqGSM49AgEGCCqGSM49AwEHA0IABG50cCwrBbSYIHjakucfkFQwBxyELaqq36a5\n\
192        l33+zC5ugnh/zDNp/txhOEHoWb7KxgeeLsDU5fnE5o7LWMweHF6jZjBkMA4GA1Ud\n\
193        DwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAeBgNVHREEFzAVghN0ZXN0\n\
194        aW5nQGV4YW1wbGUuY29tMB0GA1UdDgQWBBQTIsJVQaqqlRroqvxjrQxdaPWF2zAK\n\
195        BggqhkjOPQQDAgNHADBEAiBW6XrjErz6HAyJk/lhyhAfpYiQBKc+74dBaBFRccbd\n\
196        HgIgWCs4HPGhR1KmUEvjOLZLxsph/SZ1omQt8QQQYsUn1m4=\n\
197        -----END CERTIFICATE-----\n";
198
199    const POWERSHELL_CERTIFICATE_PRIVATE_PEM: &str = "-----BEGIN PRIVATE KEY-----\n\
200        MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9mPzM4rZBqtjLuWZ\n\
201        rWiPM5PgwTcsYMm6ojX9OAz1AIehRANCAARudHAsKwW0mCB42pLnH5BUMAcchC2q\n\
202        qt+muZd9/swuboJ4f8wzaf7cYThB6Fm+ysYHni7A1OX5xOaOy1jMHhxe\n\
203        -----END PRIVATE KEY-----\n";
204
205    fn find_extension<'a>(
206        cert: &'a X509Certificate,
207        oid: &x509_parser::der_parser::oid::Oid,
208    ) -> Option<&'a X509Extension<'a>> {
209        cert.extensions().iter().find(|ext| &ext.oid == oid)
210    }
211
212    #[test]
213    fn test_create_self_signed_certificate() -> Result<()> {
214        let powershell_pem = x509_parser::pem::Pem::read(std::io::Cursor::new(
215            POWERSHELL_CERTIFICATE_PUBLIC_PEM.as_bytes(),
216        ))?
217        .0;
218        let powershell = powershell_pem.parse_x509()?;
219
220        // Just in case we need to use this in the future.
221        rcgen::KeyPair::from_pem(POWERSHELL_CERTIFICATE_PRIVATE_PEM)?;
222
223        let cert = create_self_signed_code_signing_certificate("testing@example.com")?;
224        let cert_der = cert.serialize_der()?;
225
226        let generated = x509_parser::parse_x509_certificate(&cert_der)?.1;
227
228        assert_eq!(generated.subject(), powershell.subject(), "subject matches");
229        assert_eq!(
230            generated.signature_algorithm, powershell.signature_algorithm,
231            "signature algorithm matches"
232        );
233
234        let subject_key_identifier_oid = oid!(2.5.29 .14);
235        let basic_constraints_oid = oid!(2.5.29 .19);
236        let subject_alternative_name_oid = oid!(2.5.29 .17);
237        let extended_usage_oid = oid!(2.5.29 .37);
238        let key_usage_oid = oid!(2.5.29 .15);
239
240        assert!(find_extension(&generated, &subject_key_identifier_oid).is_some());
241        assert!(find_extension(&powershell, &subject_key_identifier_oid).is_some());
242        assert_ne!(
243            find_extension(&generated, &subject_key_identifier_oid),
244            find_extension(&powershell, &subject_key_identifier_oid),
245            "subject key identifier extension differ"
246        );
247
248        assert!(find_extension(&generated, &basic_constraints_oid).is_some());
249        assert!(find_extension(&powershell, &basic_constraints_oid).is_none());
250
251        assert!(find_extension(&generated, &subject_alternative_name_oid).is_some());
252        assert_eq!(
253            find_extension(&generated, &subject_alternative_name_oid),
254            find_extension(&powershell, &subject_alternative_name_oid),
255            "subject alternative name extension equal"
256        );
257
258        assert!(find_extension(&generated, &extended_usage_oid).is_some());
259        assert_eq!(
260            find_extension(&generated, &extended_usage_oid),
261            find_extension(&powershell, &extended_usage_oid),
262            "extended usage extension identical"
263        );
264
265        assert!(find_extension(&generated, &key_usage_oid).is_some());
266        assert_eq!(
267            find_extension(&generated, &key_usage_oid),
268            find_extension(&powershell, &key_usage_oid),
269            "key usage extension identical"
270        );
271
272        // Subject Key Identifier differs due to different key pairs in use.
273        // Ours also emits a basic constraints extension.
274        let mut generated_filtered = generated
275            .extensions()
276            .iter()
277            .filter(|ext| ext.oid != subject_key_identifier_oid && ext.oid != basic_constraints_oid)
278            .collect::<Vec<_>>();
279        generated_filtered.sort_by(|a, b| a.value.cmp(b.value));
280        let mut powershell_filtered = powershell
281            .extensions()
282            .iter()
283            .filter(|ext| ext.oid != subject_key_identifier_oid)
284            .collect::<Vec<_>>();
285        powershell_filtered.sort_by(|a, b| a.value.cmp(b.value));
286
287        assert_eq!(generated_filtered, powershell_filtered, "extensions match");
288
289        Ok(())
290    }
291
292    #[test]
293    fn test_serialize_pfx() -> Result<()> {
294        let cert = create_self_signed_code_signing_certificate("someone@example.com")?;
295        certificate_to_pfx(&cert, "password", "name")?;
296
297        Ok(())
298    }
299
300    #[test]
301    fn test_is_signable() -> Result<()> {
302        let exe = std::env::current_exe()?;
303
304        let is_signable = is_file_signable(exe)?;
305
306        if cfg!(target_family = "windows") {
307            assert!(is_signable);
308        } else {
309            assert!(!is_signable);
310        }
311
312        Ok(())
313    }
314}