apple_codesign/cli/
certificate_source.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
5use {
6    crate::{
7        cli::get_pkcs12_password,
8        cryptography::{parse_pfx_data, InMemoryPrivateKey, PrivateKey},
9        error::AppleCodesignError,
10        remote_signing::{
11            session_negotiation::{PublicKeyInitiator, SessionInitiatePeer, SharedSecretInitiator},
12            RemoteSignError, UnjoinedSigningClient,
13        },
14        signing_settings::SigningSettings,
15    },
16    base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine},
17    clap::Args,
18    log::{error, info, warn},
19    serde::{Deserialize, Serialize},
20    spki::EncodePublicKey,
21    std::path::PathBuf,
22    x509_certificate::CapturedX509Certificate,
23};
24
25#[cfg(feature = "yubikey")]
26use {
27    crate::{cli::prompt_smartcard_pin, yubikey::YubiKey},
28    std::str::FromStr,
29};
30
31#[cfg(target_os = "macos")]
32use crate::macos::{keychain_find_code_signing_certificates, KeychainDomain};
33
34#[cfg(target_os = "windows")]
35use crate::windows::{windows_store_find_code_signing_certificates, StoreName};
36
37/// Represents a set of keys and certificates.
38#[derive(Default)]
39
40pub struct SigningCertificates {
41    pub keys: Vec<Box<dyn PrivateKey>>,
42    pub certs: Vec<CapturedX509Certificate>,
43}
44
45impl SigningCertificates {
46    pub fn extend(&mut self, other: Self) {
47        self.keys.extend(other.keys);
48        self.certs.extend(other.certs);
49    }
50
51    pub fn is_empty(&self) -> bool {
52        self.keys.is_empty() && self.certs.is_empty()
53    }
54
55    /// Resolve a private key in this collection.
56    ///
57    /// Errors unless the number of keys is exactly one.
58    pub fn private_key(&self) -> Result<&dyn PrivateKey, AppleCodesignError> {
59        self.private_key_optional()?
60            .ok_or_else(|| AppleCodesignError::CliGeneralError("no private key found".into()))
61    }
62
63    /// Resolve an optional private key in this collection.
64    ///
65    /// Errors if there are more than 1 key.
66    pub fn private_key_optional(&self) -> Result<Option<&dyn PrivateKey>, AppleCodesignError> {
67        match self.keys.len() {
68            0 => Ok(None),
69            1 => Ok(Some(self.keys[0].as_ref())),
70            n => Err(AppleCodesignError::CliGeneralError(format!(
71                "at most 1 private keys can be present (found {n})"
72            ))),
73        }
74    }
75
76    /// Loads the instance into a [SigningSettings].
77    pub fn load_into_signing_settings<'settings, 'slf: 'settings>(
78        &'slf self,
79        settings: &'settings mut SigningSettings<'slf>,
80    ) -> Result<(), AppleCodesignError> {
81        let private = self.private_key_optional()?;
82
83        let mut public_certificates = self.certs.clone();
84
85        if let Some(signing_key) = &private {
86            if public_certificates.is_empty() {
87                error!("a PRIVATE KEY requires a corresponding CERTIFICATE to pair with it");
88                return Err(AppleCodesignError::CliBadArgument);
89            }
90
91            let cert = public_certificates.remove(0);
92
93            warn!("registering signing key");
94
95            if !cert.time_constraints_valid(None) {
96                warn!(
97                    "signing certificate expired as of {}; signatures may not be valid",
98                    cert.validity_not_after().to_rfc3339()
99                );
100            }
101
102            settings.set_signing_key(signing_key.as_key_info_signer(), cert);
103            if let Some(certs) = settings.chain_apple_certificates() {
104                for cert in certs {
105                    warn!(
106                        "automatically registered Apple CA certificate: {}",
107                        cert.subject_common_name()
108                            .unwrap_or_else(|| "default".into())
109                    );
110                }
111            }
112        }
113
114        for cert in public_certificates {
115            warn!("registering extra X.509 certificate");
116            settings.chain_certificate(cert);
117        }
118
119        Ok(())
120    }
121}
122
123pub trait KeySource {
124    /// Obtain a bag of private keys and certificates from the instance.
125    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError>;
126
127    /// Whether key source is the lone/exclusive source of keys + certs.
128    fn exclusive(&self) -> bool {
129        false
130    }
131}
132
133#[derive(Args, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
134#[serde(deny_unknown_fields)]
135pub struct SmartcardSigningKey {
136    /// Smartcard slot number of signing certificate to use (9c is common)
137    #[arg(long = "smartcard-slot", value_name = "SLOT")]
138    pub slot: Option<String>,
139
140    /// Smartcard PIN used to unlock certificate
141    ///
142    /// If not provided, you will be prompted for a PIN as necessary.
143    #[arg(long = "smartcard-pin", value_name = "SECRET")]
144    pub pin: Option<String>,
145
146    /// Environment variable holding the smartcard PIN
147    #[arg(long = "smartcard-pin-env", value_name = "STRING")]
148    #[serde(skip)]
149    pub pin_env: Option<String>,
150}
151
152impl KeySource for SmartcardSigningKey {
153    #[cfg(feature = "yubikey")]
154    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
155        if let Some(slot) = &self.slot {
156            let slot_id = ::yubikey::piv::SlotId::from_str(slot)?;
157            let formatted = hex::encode([u8::from(slot_id)]);
158            let mut yk = YubiKey::new()?;
159
160            if let Some(pin) = &self.pin {
161                let pin = pin.clone();
162                yk.set_pin_callback(move || Ok(pin.as_bytes().to_vec()));
163            } else if let Some(pin_var) = &self.pin_env {
164                let pin_var = pin_var.to_owned();
165
166                yk.set_pin_callback(move || {
167                    if let Ok(pin) = std::env::var(&pin_var) {
168                        eprintln!("using PIN from {} environment variable", &pin_var);
169                        Ok(pin.as_bytes().to_vec())
170                    } else {
171                        prompt_smartcard_pin()
172                    }
173                });
174            } else {
175                yk.set_pin_callback(prompt_smartcard_pin);
176            }
177
178            if let Some(signer) = yk.get_certificate_signer(slot_id)? {
179                warn!("using certificate in smartcard slot {}", formatted);
180
181                let cert = signer.certificate().clone();
182
183                Ok(SigningCertificates {
184                    keys: vec![Box::new(signer)],
185                    certs: vec![cert],
186                })
187            } else {
188                Err(AppleCodesignError::SmartcardNoCertificate(formatted))
189            }
190        } else {
191            Ok(Default::default())
192        }
193    }
194
195    #[cfg(not(feature = "yubikey"))]
196    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
197        if self.slot.is_some() {
198            error!("smartcard support not available; ignoring --smartcard-slot");
199        }
200
201        Ok(Default::default())
202    }
203}
204
205#[derive(Args, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
206#[serde(deny_unknown_fields)]
207pub struct MacosKeychainSigningKey {
208    /// (macOS only) Keychain domain to operate on
209    #[arg(long = "keychain-domain", group = "keychain", value_parser = crate::cli::KEYCHAIN_DOMAINS, value_name = "DOMAIN")]
210    #[serde(default)]
211    pub domains: Vec<String>,
212
213    /// (macOS only) SHA-256 fingerprint of certificate in Keychain to use
214    #[arg(
215        long = "keychain-fingerprint",
216        group = "keychain",
217        value_name = "SHA256 FINGERPRINT"
218    )]
219    pub sha256_fingerprint: Option<String>,
220}
221
222impl KeySource for MacosKeychainSigningKey {
223    #[cfg(target_os = "macos")]
224    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
225        // No arguments pertinent to keychains. Don't even speak to the
226        // keychain API since this could only error.
227        if self.domains.is_empty() && self.sha256_fingerprint.is_none() {
228            return Ok(Default::default());
229        }
230
231        // Collect all the keychain domains to search.
232        let domains = if self.domains.is_empty() {
233            vec!["user".to_string()]
234        } else {
235            self.domains.clone()
236        };
237
238        let domains = domains
239            .into_iter()
240            .map(|domain| {
241                KeychainDomain::try_from(domain.as_str())
242                    .expect("clap should have validated domain values")
243            })
244            .collect::<Vec<_>>();
245
246        // Now iterate all the keychains and try to find requested certificates.
247        let mut res = SigningCertificates::default();
248
249        for domain in domains {
250            for cert in keychain_find_code_signing_certificates(domain, None)? {
251                let matches = if let Some(wanted_fingerprint) = &self.sha256_fingerprint {
252                    let got_fingerprint = hex::encode(cert.sha256_fingerprint()?.as_ref());
253
254                    wanted_fingerprint.to_ascii_lowercase() == got_fingerprint.to_ascii_lowercase()
255                } else {
256                    false
257                };
258
259                if matches {
260                    res.certs.push(cert.as_captured_x509_certificate());
261                    res.keys.push(Box::new(cert));
262                }
263            }
264        }
265
266        Ok(res)
267    }
268
269    #[cfg(not(target_os = "macos"))]
270    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
271        if !self.domains.is_empty() || self.sha256_fingerprint.is_some() {
272            error!(
273                "--keychain* arguments only supported on macOS and will be ignored on this platform"
274            );
275        }
276
277        Ok(Default::default())
278    }
279}
280
281#[derive(Args, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
282#[serde(deny_unknown_fields)]
283pub struct WindowsStoreSigningKey {
284    /// (Windows only) Windows Store to operate on
285    #[arg(long = "windows-store-name", value_parser = crate::cli::WINDOWS_STORE_NAMES, value_name = "STORE")]
286    pub stores: Vec<String>,
287
288    /// (Windows only) SHA-1 fingerprint of certificate in Windows Store to use
289    #[arg(
290        long = "windows-store-sha1-fingerprint",
291        value_name = "SHA1 FINGERPRINT"
292    )]
293    pub sha1_fingerprint: Option<String>,
294}
295
296impl KeySource for WindowsStoreSigningKey {
297    #[cfg(target_os = "windows")]
298    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
299        // No arguments pertinent to store. Don't even speak to the
300        // Windows API since this could only error.
301        if self.stores.is_empty() && self.sha1_fingerprint.is_none() {
302            return Ok(Default::default());
303        }
304
305        // Collect all the store names to search.
306        let stores = if self.stores.is_empty() {
307            vec!["user".to_string()]
308        } else {
309            self.stores.clone()
310        };
311
312        let stores = stores
313            .into_iter()
314            .map(|store| {
315                StoreName::try_from(store.as_str())
316                    .expect("clap should have validated store name values")
317            })
318            .collect::<Vec<_>>();
319
320        // Now iterate all the stores and try to find requested certificates.
321        let mut res = SigningCertificates::default();
322
323        for store in stores {
324            for cert in windows_store_find_code_signing_certificates(store)? {
325                let matches = if let Some(wanted_fingerprint) = &self.sha1_fingerprint {
326                    let got_fingerprint = hex::encode(cert.sha1_fingerprint()?.as_ref());
327
328                    wanted_fingerprint.to_ascii_lowercase() == got_fingerprint.to_ascii_lowercase()
329                } else {
330                    false
331                };
332
333                if matches {
334                    res.certs.push(cert.as_captured_x509_certificate());
335                    res.keys.push(Box::new(cert));
336                }
337            }
338        }
339
340        Ok(res)
341    }
342
343    #[cfg(not(target_os = "windows"))]
344    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
345        if !self.stores.is_empty() || self.sha1_fingerprint.is_some() {
346            error!(
347                "--windows-store* arguments only supported on Windows and will be ignored on this platform"
348            );
349        }
350
351        Ok(Default::default())
352    }
353}
354
355#[derive(Args, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
356#[serde(deny_unknown_fields)]
357pub struct P12SigningKey {
358    /// Path to a .p12/PFX file containing a certificate key pair
359    #[arg(long = "p12-file", alias = "pfx-file", value_name = "PATH")]
360    pub path: Option<PathBuf>,
361
362    /// The password to use to open the --p12-file file
363    #[arg(
364        long = "p12-password",
365        alias = "pfx-password",
366        group = "p12-password",
367        value_name = "SECRET"
368    )]
369    pub password: Option<String>,
370
371    // TODO conflicts with p12_password
372    /// Path to file containing password for opening --p12-file file
373    #[arg(
374        long = "p12-password-file",
375        alias = "pfx-password-file",
376        group = "p12-password",
377        value_name = "PATH"
378    )]
379    pub password_path: Option<PathBuf>,
380}
381
382impl KeySource for P12SigningKey {
383    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
384        if let Some(path) = &self.path {
385            let p12_data = std::fs::read(path)?;
386
387            let p12_password =
388                get_pkcs12_password(self.password.clone(), self.password_path.clone())?;
389
390            let (cert, key) = parse_pfx_data(&p12_data, &p12_password)?;
391
392            Ok(SigningCertificates {
393                keys: vec![Box::new(key)],
394                certs: vec![cert],
395            })
396        } else {
397            Ok(Default::default())
398        }
399    }
400}
401
402#[derive(Args, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
403#[serde(deny_unknown_fields)]
404pub struct PemSigningKey {
405    /// Path to file containing PEM encoded certificate/key data
406    #[arg(long = "pem-file", alias = "pem-source", value_name = "PATH")]
407    #[serde(rename = "files")]
408    pub paths: Vec<PathBuf>,
409}
410
411impl KeySource for PemSigningKey {
412    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
413        let mut res = SigningCertificates::default();
414
415        for path in &self.paths {
416            warn!("reading PEM data from {}", path.display());
417            let pem_data = std::fs::read(path)?;
418
419            for pem in pem::parse_many(pem_data).map_err(AppleCodesignError::CertificatePem)? {
420                match pem.tag() {
421                    "CERTIFICATE" => {
422                        info!("adding certificate from {}", path.display());
423                        res.certs
424                            .push(CapturedX509Certificate::from_der(pem.contents())?);
425                    }
426                    "PRIVATE KEY" => {
427                        info!("adding private key from {}", path.display());
428                        res.keys.push(Box::new(InMemoryPrivateKey::from_pkcs8_der(
429                            pem.contents(),
430                        )?));
431                    }
432                    "RSA PRIVATE KEY" => {
433                        info!("adding RSA private key from {}", path.display());
434                        res.keys.push(Box::new(InMemoryPrivateKey::from_pkcs1_der(
435                            pem.contents(),
436                        )?));
437                    }
438                    tag => warn!("(unhandled PEM tag {}; ignoring)", tag),
439                }
440            }
441        }
442
443        Ok(res)
444    }
445}
446
447#[derive(Args, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
448#[serde(deny_unknown_fields)]
449pub struct RemoteSigningKey {
450    /// URL of a remote code signing server
451    #[arg(long = "remote-signing-url", value_name = "URL")]
452    pub url: Option<String>,
453
454    /// Base64 encoded public key data describing the signer
455    #[arg(
456        long = "remote-public-key",
457        group = "remote-initialization",
458        value_name = "BASE64 ENCODED PUBLIC KEY"
459    )]
460    pub public_key: Option<String>,
461
462    /// PEM encoded public key data describing the signer
463    #[arg(
464        long = "remote-public-key-pem-file",
465        group = "remote-initialization",
466        group = "remote-initialization",
467        value_name = "PATH"
468    )]
469    pub public_key_pem_path: Option<PathBuf>,
470
471    /// Shared secret used for remote signing
472    #[arg(
473        long = "remote-shared-secret",
474        group = "remote-initialization",
475        value_name = "SECRET"
476    )]
477    pub shared_secret: Option<String>,
478
479    /// Environment variable holding the shared secret used for remote signing
480    #[arg(
481        long = "remote-shared-secret-env",
482        group = "remote-initialization",
483        value_name = "ENV VAR NAME"
484    )]
485    pub shared_secret_env: Option<String>,
486}
487
488impl KeySource for RemoteSigningKey {
489    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
490        if let Some(initiator) = self.remote_signing_initiator()? {
491            let client = UnjoinedSigningClient::new_initiator(
492                self.url(),
493                initiator,
494                Some(super::print_session_join),
495            )?;
496
497            let mut certs = vec![client.signing_certificate().clone()];
498            certs.extend(client.certificate_chain().iter().cloned());
499
500            Ok(SigningCertificates {
501                keys: vec![Box::new(client)],
502                certs,
503            })
504        } else {
505            Ok(Default::default())
506        }
507    }
508
509    fn exclusive(&self) -> bool {
510        true
511    }
512}
513
514impl RemoteSigningKey {
515    /// Obtain the URL of the relay server.
516    pub fn url(&self) -> String {
517        self.url
518            .clone()
519            .unwrap_or_else(|| crate::remote_signing::DEFAULT_SERVER_URL.to_string())
520    }
521
522    fn remote_signing_initiator(
523        &self,
524    ) -> Result<Option<Box<dyn SessionInitiatePeer>>, RemoteSignError> {
525        let server_url = self.url();
526
527        if let Some(public_key_data) = &self.public_key {
528            let public_key_data = STANDARD_ENGINE.decode(public_key_data)?;
529
530            Ok(Some(Box::new(PublicKeyInitiator::new(
531                public_key_data,
532                Some(server_url),
533            )?)))
534        } else if let Some(path) = &self.public_key_pem_path {
535            let pem_data = std::fs::read(path)?;
536            let doc = pem::parse(pem_data)?;
537
538            let spki_der = match doc.tag() {
539                "PUBLIC KEY" => doc.contents().to_vec(),
540                "CERTIFICATE" => {
541                    let cert = CapturedX509Certificate::from_der(doc.contents())?;
542                    cert.to_public_key_der()?.as_ref().to_vec()
543                }
544                tag => {
545                    error!(
546                        "unknown PEM format: {}; only `PUBLIC KEY` and `CERTIFICATE` are parsed",
547                        tag
548                    );
549                    return Err(RemoteSignError::Crypto("invalid public key data".into()));
550                }
551            };
552
553            Ok(Some(Box::new(PublicKeyInitiator::new(
554                spki_der,
555                Some(server_url),
556            )?)))
557        } else if let Some(env) = &self.shared_secret_env {
558            let secret = std::env::var(env).map_err(|_| {
559                RemoteSignError::ClientState(
560                    "failed reading from shared secret environment variable",
561                )
562            })?;
563
564            Ok(Some(Box::new(SharedSecretInitiator::new(
565                secret.as_bytes().to_vec(),
566            )?)))
567        } else if let Some(value) = &self.shared_secret {
568            Ok(Some(Box::new(SharedSecretInitiator::new(
569                value.as_bytes().to_vec(),
570            )?)))
571        } else {
572            Ok(None)
573        }
574    }
575}
576
577#[derive(Args, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
578#[serde(deny_unknown_fields)]
579pub struct CertificateDerSigningKey {
580    /// Path to file containing DER encoded certificate data
581    #[arg(
582        id = "certificate_der_paths",
583        long = "certificate-der-file",
584        alias = "der-source",
585        alias = "der-file",
586        value_name = "PATH"
587    )]
588    pub paths: Vec<PathBuf>,
589}
590
591impl KeySource for CertificateDerSigningKey {
592    fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
593        let mut res = SigningCertificates::default();
594
595        for path in &self.paths {
596            warn!("reading DER file {}", path.display());
597            let der_data = std::fs::read(path)?;
598
599            res.certs.push(CapturedX509Certificate::from_der(der_data)?);
600        }
601
602        Ok(res)
603    }
604}
605
606#[derive(Args, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
607#[serde(deny_unknown_fields)]
608pub struct CertificateSource {
609    #[command(flatten)]
610    #[serde(default, rename = "smartcard", skip_serializing_if = "Option::is_none")]
611    pub smartcard_key: Option<SmartcardSigningKey>,
612
613    #[command(flatten)]
614    #[serde(
615        default,
616        rename = "macos_keychain",
617        skip_serializing_if = "Option::is_none"
618    )]
619    pub macos_keychain_key: Option<MacosKeychainSigningKey>,
620
621    #[command(flatten)]
622    #[serde(
623        default,
624        rename = "windows_store",
625        skip_serializing_if = "Option::is_none"
626    )]
627    pub windows_store_key: Option<WindowsStoreSigningKey>,
628
629    #[command(flatten)]
630    #[serde(default, rename = "pem", skip_serializing_if = "Option::is_none")]
631    pub pem_path_key: Option<PemSigningKey>,
632
633    #[command(flatten)]
634    #[serde(default, rename = "p12", skip_serializing_if = "Option::is_none")]
635    pub p12_key: Option<P12SigningKey>,
636
637    #[command(flatten)]
638    #[serde(default, rename = "remote", skip_serializing_if = "Option::is_none")]
639    pub remote_signing_key: Option<RemoteSigningKey>,
640
641    #[command(flatten)]
642    #[serde(
643        default,
644        rename = "certificate_der",
645        skip_serializing_if = "Option::is_none"
646    )]
647    pub certificate_der_key: Option<CertificateDerSigningKey>,
648}
649
650impl CertificateSource {
651    /// Obtain a reference to all [KeySource] present.
652    pub fn key_sources(&self, scan_smartcard: bool) -> Vec<&dyn KeySource> {
653        let mut res = vec![];
654
655        if scan_smartcard {
656            if let Some(key) = &self.smartcard_key {
657                res.push(key as &dyn KeySource);
658            }
659        }
660
661        if let Some(key) = &self.macos_keychain_key {
662            res.push(key as &dyn KeySource);
663        }
664
665        if let Some(key) = &self.windows_store_key {
666            res.push(key as &dyn KeySource);
667        }
668
669        if let Some(key) = &self.pem_path_key {
670            res.push(key as &dyn KeySource);
671        }
672
673        if let Some(key) = &self.p12_key {
674            res.push(key as &dyn KeySource);
675        }
676
677        if let Some(key) = &self.remote_signing_key {
678            res.push(key as &dyn KeySource);
679        }
680
681        if let Some(key) = &self.certificate_der_key {
682            res.push(key as &dyn KeySource);
683        }
684
685        res
686    }
687
688    pub fn resolve_certificates(
689        &self,
690        scan_smartcard: bool,
691    ) -> Result<SigningCertificates, AppleCodesignError> {
692        let mut res = SigningCertificates::default();
693
694        for key in self.key_sources(scan_smartcard) {
695            let certs = key.resolve_certificates()?;
696
697            if key.exclusive() && !certs.is_empty() {
698                return Ok(certs);
699            }
700
701            res.extend(certs);
702        }
703
704        Ok(res)
705    }
706}