tugger_windows_codesign/
signing.rs1use {
8 crate::SystemStore,
9 anyhow::{anyhow, Result},
10 std::{
11 io::Read,
12 path::{Path, PathBuf},
13 },
14};
15
16#[derive(Clone, Debug)]
20pub struct FileBasedCodeSigningCertificate {
21 path: PathBuf,
23 password: Option<String>,
25}
26
27impl FileBasedCodeSigningCertificate {
28 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#[derive(Clone, Debug)]
56pub enum CodeSigningCertificate {
57 Auto,
59
60 File(FileBasedCodeSigningCertificate),
62
63 SubjectName(SystemStore, String),
65
66 Sha1Thumbprint(SystemStore, String),
71}
72
73impl From<FileBasedCodeSigningCertificate> for CodeSigningCertificate {
74 fn from(v: FileBasedCodeSigningCertificate) -> Self {
75 Self::File(v)
76 }
77}
78
79pub 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 params.not_after = time::OffsetDateTime::now_utc()
103 .checked_add(time::Duration::days(365))
104 .unwrap();
105
106 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
123pub 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
144const CFB_MAGIC_NUMBER: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
146
147#[allow(clippy::if_same_then_else)]
153pub fn is_signable_binary_header(data: &[u8]) -> bool {
154 if data.len() < 16 {
155 false
156 } 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
164pub 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 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 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 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}