apple_codesign/cli/
mod.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
5pub mod certificate_source;
6pub mod config;
7pub mod debug_commands;
8pub mod extract_commands;
9
10use {
11    crate::{
12        certificate::{
13            create_self_signed_code_signing_certificate, AppleCertificate, CertificateProfile,
14        },
15        cli::{
16            certificate_source::CertificateSource,
17            config::{Config, ConfigBuilder},
18        },
19        code_directory::CodeSignatureFlags,
20        code_requirement::CodeRequirements,
21        cryptography::DigestType,
22        environment_constraints::EncodedEnvironmentConstraints,
23        error::AppleCodesignError,
24        macho::MachFile,
25        reader::SignatureReader,
26        remote_signing::{
27            session_negotiation::{create_session_joiner, SessionJoinState},
28            RemoteSignError, UnjoinedSigningClient,
29        },
30        signing::UnifiedSigner,
31        signing_settings::{SettingsScope, SigningSettings},
32    },
33    base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine},
34    clap::{ArgAction, Args, Parser, Subcommand},
35    difference::{Changeset, Difference},
36    log::{error, warn, LevelFilter},
37    serde::{Deserialize, Serialize},
38    spki::EncodePublicKey,
39    std::{
40        collections::BTreeMap,
41        path::{Path, PathBuf},
42        str::FromStr,
43    },
44    x509_certificate::{CapturedX509Certificate, EcdsaCurve, KeyAlgorithm, X509CertificateBuilder},
45};
46
47#[cfg(feature = "notarize")]
48use crate::notarization::Notarizer;
49
50#[cfg(feature = "yubikey")]
51use {
52    crate::yubikey::YubiKey,
53    yubikey::{PinPolicy, TouchPolicy},
54};
55
56#[cfg(target_os = "macos")]
57use crate::macos::{
58    keychain_find_code_signing_certificates, macos_keychain_find_certificate_chain, KeychainDomain,
59};
60
61#[cfg(target_os = "windows")]
62use crate::windows::{
63    windows_store_find_certificate_chain, windows_store_find_code_signing_certificates, StoreName,
64};
65
66pub const KEYCHAIN_DOMAINS: [&str; 4] = ["user", "system", "common", "dynamic"];
67pub const WINDOWS_STORE_NAMES: [&str; 3] = ["user", "machine", "service"];
68
69const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01";
70
71/// Holds state to pass to CLI commands.
72pub struct Context {
73    pub config: Config,
74}
75
76pub trait CliCommand {
77    /// Obtain the current command arguments normalized to a [Config] instance.
78    fn as_config(&self) -> Result<Option<Config>, AppleCodesignError> {
79        Ok(None)
80    }
81
82    /// Runs the command.
83    fn run(&self, context: &Context) -> Result<(), AppleCodesignError>;
84}
85
86#[allow(unused)]
87pub fn prompt_smartcard_pin() -> Result<Vec<u8>, AppleCodesignError> {
88    let pin = dialoguer::Password::new()
89        .with_prompt("Please enter device PIN")
90        .interact()?;
91
92    Ok(pin.as_bytes().to_vec())
93}
94
95pub fn get_pkcs12_password(
96    password: Option<impl ToString>,
97    password_file: Option<impl AsRef<Path>>,
98) -> Result<String, AppleCodesignError> {
99    if let Some(password) = password {
100        Ok(password.to_string())
101    } else if let Some(path) = password_file {
102        Ok(std::fs::read_to_string(path.as_ref())?
103            .lines()
104            .next()
105            .ok_or_else(|| {
106                AppleCodesignError::CliGeneralError("password file appears to be empty".into())
107            })?
108            .to_string())
109    } else {
110        Ok(dialoguer::Password::new()
111            .with_prompt("Please enter password for p12 file")
112            .interact()?)
113    }
114}
115
116#[cfg(feature = "notarize")]
117#[derive(Args)]
118struct NotaryApi {
119    /// Path to a JSON file containing the API Key
120    #[arg(
121        long = "api-key-file",
122        alias = "api-key-path",
123        group = "source",
124        value_name = "PATH"
125    )]
126    api_key_path: Option<PathBuf>,
127
128    /// App Store Connect Issuer ID (likely a UUID)
129    #[arg(long, requires = "api_key")]
130    api_issuer: Option<String>,
131
132    #[arg(long, requires = "api_issuer")]
133    /// App Store Connect API Key ID
134    api_key: Option<String>,
135}
136
137#[cfg(feature = "notarize")]
138impl NotaryApi {
139    /// Resolve a notarizer from arguments.
140    fn notarizer(&self) -> Result<Notarizer, AppleCodesignError> {
141        if let Some(api_key_path) = &self.api_key_path {
142            Notarizer::from_api_key(api_key_path)
143        } else if let (Some(issuer), Some(key)) = (&self.api_issuer, &self.api_key) {
144            Notarizer::from_api_key_id(issuer, key)
145        } else {
146            Err(AppleCodesignError::NotarizeNoAuthCredentials)
147        }
148    }
149}
150
151#[derive(Args)]
152struct YubikeyPolicy {
153    /// Smartcard touch policy to protect key access
154    #[arg(long, value_parser = ["default", "always", "never", "cached"], default_value = "default")]
155    touch_policy: String,
156
157    /// Smartcard pin prompt policy to protect key access
158    #[arg(long, value_parser = ["default", "never", "once", "always"], default_value = "default")]
159    pin_policy: String,
160}
161
162#[cfg(feature = "yubikey")]
163fn str_to_touch_policy(s: &str) -> Result<TouchPolicy, AppleCodesignError> {
164    match s {
165        "default" => Ok(TouchPolicy::Default),
166        "never" => Ok(TouchPolicy::Never),
167        "always" => Ok(TouchPolicy::Always),
168        "cached" => Ok(TouchPolicy::Cached),
169        _ => Err(AppleCodesignError::CliBadArgument),
170    }
171}
172
173#[cfg(feature = "yubikey")]
174fn str_to_pin_policy(s: &str) -> Result<PinPolicy, AppleCodesignError> {
175    match s {
176        "default" => Ok(PinPolicy::Default),
177        "never" => Ok(PinPolicy::Never),
178        "once" => Ok(PinPolicy::Once),
179        "always" => Ok(PinPolicy::Always),
180        _ => Err(AppleCodesignError::CliBadArgument),
181    }
182}
183
184fn print_certificate_info(cert: &CapturedX509Certificate) -> Result<(), AppleCodesignError> {
185    println!(
186        "Subject CN:                  {}",
187        cert.subject_common_name()
188            .unwrap_or_else(|| "<missing>".to_string())
189    );
190    println!(
191        "Issuer CN:                   {}",
192        cert.issuer_common_name()
193            .unwrap_or_else(|| "<missing>".to_string())
194    );
195    println!("Subject is Issuer?:          {}", cert.subject_is_issuer());
196    println!(
197        "Team ID:                     {}",
198        cert.apple_team_id()
199            .unwrap_or_else(|| "<missing>".to_string())
200    );
201    println!(
202        "SHA-1 fingerprint:           {}",
203        hex::encode(cert.sha1_fingerprint()?)
204    );
205    println!(
206        "SHA-256 fingerprint:         {}",
207        hex::encode(cert.sha256_fingerprint()?)
208    );
209    println!(
210        "Not Valid Before:            {}",
211        cert.validity_not_before().to_rfc3339()
212    );
213    println!(
214        "Not Valid After:             {}",
215        cert.validity_not_after().to_rfc3339()
216    );
217    if let Some(alg) = cert.key_algorithm() {
218        println!("Key Algorithm:               {alg}");
219    }
220    if let Some(alg) = cert.signature_algorithm() {
221        println!("Signature Algorithm:         {alg}");
222    }
223    println!(
224        "Public Key Data:             {}",
225        STANDARD_ENGINE.encode(
226            cert.to_public_key_der()
227                .map_err(|e| AppleCodesignError::X509Parse(format!(
228                    "error constructing SPKI: {e}"
229                )))?
230        )
231    );
232    println!(
233        "Signed by Apple?:            {}",
234        cert.chains_to_apple_root_ca()
235    );
236    if cert.chains_to_apple_root_ca() {
237        println!("Apple Issuing Chain:");
238        for signer in cert.apple_issuing_chain() {
239            println!(
240                "  - {}",
241                signer
242                    .subject_common_name()
243                    .unwrap_or_else(|| "<unknown>".to_string())
244            );
245        }
246    }
247
248    println!(
249        "Guessed Certificate Profile: {}",
250        if let Some(profile) = cert.apple_guess_profile() {
251            format!("{profile:?}")
252        } else {
253            "none".to_string()
254        }
255    );
256    println!("Is Apple Root CA?:           {}", cert.is_apple_root_ca());
257    println!(
258        "Is Apple Intermediate CA?:   {}",
259        cert.is_apple_intermediate_ca()
260    );
261
262    if !cert.apple_ca_extensions().is_empty() {
263        println!("Apple CA Extensions:");
264        for ext in cert.apple_ca_extensions() {
265            println!("  - {} ({:?})", ext.as_oid(), ext);
266        }
267    }
268
269    println!("Apple Extended Key Usage Purpose Extensions:");
270    for purpose in cert.apple_extended_key_usage_purposes() {
271        println!("  - {} ({:?})", purpose.as_oid(), purpose);
272    }
273    println!("Apple Code Signing Extensions:");
274    for ext in cert.apple_code_signing_extensions() {
275        println!("  - {} ({:?})", ext.as_oid(), ext);
276    }
277    print!(
278        "\n{}",
279        cert.to_public_key_pem(Default::default())
280            .map_err(|e| AppleCodesignError::X509Parse(format!("error constructing SPKI: {e}")))?
281    );
282    print!("\n{}", cert.encode_pem());
283
284    Ok(())
285}
286
287pub fn print_session_join(sjs_base64: &str, sjs_pem: &str) -> Result<(), RemoteSignError> {
288    error!("");
289    error!("Run the following command to join this signing session:");
290    error!("");
291    error!("    rcodesign remote-sign {}", sjs_base64);
292    error!("");
293    error!("Or if this output is too long, paste the following output:");
294    error!("");
295    for line in sjs_pem.lines() {
296        error!("{}", line);
297    }
298    error!("");
299    error!("Into an interactive editor using:");
300    error!("");
301    error!("    rcodesign remote-sign --editor");
302    error!("");
303    error!("Or into a new file whose path you define with:");
304    error!("");
305    error!("    rcodesign remote-sign --sjs-path /path/to/file/you/just/saved");
306    error!("");
307    error!("(waiting for remote signer to join)");
308
309    Ok(())
310}
311
312#[derive(Parser)]
313struct AnalyzeCertificate {
314    #[command(flatten)]
315    certificate: CertificateSource,
316}
317
318impl CliCommand for AnalyzeCertificate {
319    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
320        let certs = self.certificate.resolve_certificates(true)?.certs;
321
322        for (i, cert) in certs.into_iter().enumerate() {
323            println!("# Certificate {i}");
324            println!();
325            print_certificate_info(&cert)?;
326            println!();
327        }
328
329        Ok(())
330    }
331}
332
333#[derive(Parser)]
334struct ComputeCodeHashes {
335    /// Path to Mach-O binary to examine.
336    path: PathBuf,
337
338    /// Hashing algorithm to use.
339    #[arg(long, default_value_t = DigestType::Sha256)]
340    hash: DigestType,
341
342    /// Chunk size to digest over.
343    #[arg(long, default_value = "4096")]
344    page_size: usize,
345
346    /// Index of Mach-O binary to operate on within a universal/fat binary
347    #[arg(long, default_value = "0")]
348    universal_index: usize,
349}
350
351impl CliCommand for ComputeCodeHashes {
352    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
353        let data = std::fs::read(&self.path)?;
354        let mach = MachFile::parse(&data)?;
355        let macho = mach.nth_macho(self.universal_index)?;
356
357        let hashes = macho.code_digests(self.hash, self.page_size)?;
358
359        for hash in hashes {
360            println!("{}", hex::encode(hash));
361        }
362
363        Ok(())
364    }
365}
366
367#[derive(Parser)]
368struct DiffSignatures {
369    /// The first path to compare
370    path0: PathBuf,
371
372    /// The second path to compare
373    path1: PathBuf,
374}
375
376impl CliCommand for DiffSignatures {
377    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
378        let reader = SignatureReader::from_path(&self.path0)?;
379
380        let a_entities = reader.entities()?;
381
382        let reader = SignatureReader::from_path(&self.path1)?;
383        let b_entities = reader.entities()?;
384
385        let a = serde_yaml::to_string(&a_entities)?;
386        let b = serde_yaml::to_string(&b_entities)?;
387
388        let Changeset { diffs, .. } = Changeset::new(&a, &b, "\n");
389
390        for item in diffs {
391            match item {
392                Difference::Same(ref x) => {
393                    for line in x.lines() {
394                        println!(" {line}");
395                    }
396                }
397                Difference::Add(ref x) => {
398                    for line in x.lines() {
399                        println!("+{line}");
400                    }
401                }
402                Difference::Rem(ref x) => {
403                    for line in x.lines() {
404                        println!("-{line}");
405                    }
406                }
407            }
408        }
409
410        Ok(())
411    }
412}
413
414#[cfg(feature = "notarize")]
415#[derive(Parser)]
416struct EncodeAppStoreConnectApiKey {
417    /// Path to a JSON file to create the output to
418    #[arg(short = 'o', long)]
419    output_path: Option<PathBuf>,
420
421    /// The issuer of the API Token. Likely a UUID
422    issuer_id: String,
423
424    /// The Key ID. A short alphanumeric string like DEADBEEF42
425    key_id: String,
426
427    /// Path to a file containing the private key downloaded from Apple
428    private_key_path: PathBuf,
429}
430
431#[cfg(feature = "notarize")]
432impl CliCommand for EncodeAppStoreConnectApiKey {
433    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
434        let unified = app_store_connect::UnifiedApiKey::from_ecdsa_pem_path(
435            &self.issuer_id,
436            &self.key_id,
437            &self.private_key_path,
438        )?;
439
440        if let Some(output_path) = &self.output_path {
441            eprintln!("writing unified key JSON to {}", output_path.display());
442            unified.write_json_file(output_path)?;
443            eprintln!(
444                "consider auditing the file's access permissions to ensure its content remains secure"
445            );
446        } else {
447            println!("{}", unified.to_json_string()?);
448        }
449
450        Ok(())
451    }
452}
453
454#[derive(Parser)]
455struct GenerateCertificateSigningRequest {
456    /// Path to file to write PEM encoded CSR to
457    #[arg(long = "csr-pem-file", alias = "csr-pem-path")]
458    csr_pem_path: Option<PathBuf>,
459
460    #[command(flatten)]
461    certificate: CertificateSource,
462}
463
464impl CliCommand for GenerateCertificateSigningRequest {
465    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
466        let signing_certs = self.certificate.resolve_certificates(true)?;
467
468        let private_key = signing_certs.private_key()?;
469
470        let mut builder = X509CertificateBuilder::default();
471        builder
472            .subject()
473            .append_common_name_utf8_string("Apple Code Signing CSR")
474            .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{e:?}")))?;
475
476        warn!("generating CSR; you may be prompted to enter credentials to unlock the signing key");
477        let pem = builder
478            .create_certificate_signing_request(private_key.as_key_info_signer())?
479            .encode_pem()?;
480
481        if let Some(dest_path) = &self.csr_pem_path {
482            if let Some(parent) = dest_path.parent() {
483                std::fs::create_dir_all(parent)?;
484            }
485
486            warn!("writing PEM encoded CSR to {}", dest_path.display());
487            std::fs::write(dest_path, pem.as_bytes())?;
488        }
489
490        print!("{pem}");
491
492        Ok(())
493    }
494}
495
496#[derive(Parser)]
497struct GenerateSelfSignedCertificate {
498    /// Which key type to use
499    #[arg(long, value_parser = ["ecdsa", "ed25519", "rsa"], default_value = "rsa")]
500    algorithm: String,
501
502    #[arg(long, value_parser = CertificateProfile::str_names(), default_value = "apple-development")]
503    profile: String,
504
505    /// Team ID (this is a short string attached to your Apple Developer account)
506    #[arg(long, default_value = "unset")]
507    team_id: String,
508
509    /// The name of the person this certificate is for
510    #[arg(long)]
511    person_name: String,
512
513    /// Country Name (C) value for certificate identifier
514    #[arg(long, default_value = "XX")]
515    country_name: String,
516
517    /// How many days the certificate should be valid for
518    #[arg(long, default_value = "365")]
519    validity_days: i64,
520
521    /// Base name of files to write PEM encoded certificate to
522    #[arg(long)]
523    pem_filename: Option<String>,
524
525    /// Filename to write PEM encoded private key and public certificate to.
526    #[arg(
527        long = "pem-unified-file",
528        alias = "pem-unified-filename",
529        value_name = "PATH"
530    )]
531    pem_unified_path: Option<PathBuf>,
532
533    /// Filename to write a PKCS#12 / p12 / PFX encoded certificate to.
534    #[arg(long = "p12-file", alias = "pfx-file", value_name = "PATH")]
535    p12_path: Option<PathBuf>,
536
537    /// Password to use to encrypt --p12-path.
538    ///
539    /// If not provided you will be prompted for a password.
540    #[arg(long)]
541    p12_password: Option<String>,
542}
543
544impl CliCommand for GenerateSelfSignedCertificate {
545    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
546        let algorithm = match self.algorithm.as_str() {
547            "ecdsa" => KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1),
548            "ed25519" => KeyAlgorithm::Ed25519,
549            "rsa" => KeyAlgorithm::Rsa,
550            value => panic!("algorithm values should have been validated by arg parser: {value}"),
551        };
552
553        let profile = CertificateProfile::from_str(self.profile.as_str())?;
554
555        let validity_duration = chrono::Duration::days(self.validity_days);
556
557        let (cert, key_pair) = create_self_signed_code_signing_certificate(
558            algorithm,
559            profile,
560            &self.team_id,
561            &self.person_name,
562            &self.country_name,
563            validity_duration,
564        )?;
565
566        let cert_pem = cert.encode_pem();
567        let key_pem = pem::encode(&pem::Pem::new(
568            "PRIVATE KEY",
569            key_pair.to_pkcs8_one_asymmetric_key_der().to_vec(),
570        ));
571
572        let mut wrote_file = false;
573
574        if let Some(pem_filename) = &self.pem_filename {
575            let cert_path = PathBuf::from(format!("{pem_filename}.crt"));
576            let key_path = PathBuf::from(format!("{pem_filename}.key"));
577
578            if let Some(parent) = cert_path.parent() {
579                std::fs::create_dir_all(parent)?;
580            }
581
582            println!("writing public certificate to {}", cert_path.display());
583            std::fs::write(&cert_path, cert_pem.as_bytes())?;
584            println!("writing private signing key to {}", key_path.display());
585            std::fs::write(&key_path, key_pem.as_bytes())?;
586
587            wrote_file = true;
588        }
589
590        if let Some(path) = &self.pem_unified_path {
591            let content = format!("{}{}", key_pem, cert_pem);
592
593            if let Some(parent) = path.parent() {
594                std::fs::create_dir_all(parent)?;
595            }
596
597            println!("writing unified PEM to {}", path.display());
598            std::fs::write(path, content.as_bytes())?;
599
600            wrote_file = true;
601        }
602
603        if let Some(path) = &self.p12_path {
604            let password = get_pkcs12_password(self.p12_password.clone(), None::<PathBuf>)?;
605
606            let pfx = p12::PFX::new(
607                &cert.encode_der()?,
608                &key_pair.to_pkcs8_one_asymmetric_key_der(),
609                None,
610                &password,
611                "code-signing",
612            )
613            .ok_or_else(|| {
614                AppleCodesignError::CliGeneralError("failed to create PFX structure".into())
615            })?;
616
617            println!("writing PKCS#12 certificate to {}", path.display());
618
619            if let Some(parent) = path.parent() {
620                std::fs::create_dir_all(parent)?;
621            }
622            std::fs::write(path, pfx.to_der())?;
623
624            wrote_file = true;
625        }
626
627        if !wrote_file {
628            print!("{cert_pem}");
629            print!("{key_pem}");
630        }
631
632        Ok(())
633    }
634}
635
636#[derive(Parser)]
637struct KeychainExportCertificateChain {
638    /// Keychain domain to operate on
639    #[arg(long, value_parser = KEYCHAIN_DOMAINS, default_value = "user")]
640    domain: String,
641
642    /// Password to unlock the Keychain
643    #[arg(long, group = "unlock-password")]
644    password: Option<String>,
645
646    /// File containing password to use to unlock the Keychain
647    #[arg(long = "password-file", group = "unlock-password")]
648    password_path: Option<PathBuf>,
649
650    /// Print only the issuing certificate chain, not the subject certificate
651    #[arg(long)]
652    no_print_self: bool,
653
654    /// User ID value of code signing certificate to find and whose CA chain to export
655    #[arg(long)]
656    user_id: String,
657}
658
659impl CliCommand for KeychainExportCertificateChain {
660    #[cfg(target_os = "macos")]
661    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
662        let domain = KeychainDomain::try_from(self.domain.as_str())
663            .expect("clap should have validated domain values");
664
665        let password = if let Some(path) = &self.password_path {
666            let data = std::fs::read_to_string(path)?;
667
668            Some(
669                data.lines()
670                    .next()
671                    .expect("should get a single line")
672                    .to_string(),
673            )
674        } else {
675            self.password.as_ref().map(|password| password.to_string())
676        };
677
678        let certs =
679            macos_keychain_find_certificate_chain(domain, password.as_deref(), &self.user_id)?;
680
681        for (i, cert) in certs.iter().enumerate() {
682            if self.no_print_self && i == 0 {
683                continue;
684            }
685
686            print!("{}", cert.encode_pem());
687        }
688
689        Ok(())
690    }
691
692    #[cfg(not(target_os = "macos"))]
693    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
694        Err(AppleCodesignError::CliGeneralError(
695            "macOS Keychain export only supported on macOS".to_string(),
696        ))
697    }
698}
699
700#[derive(Parser)]
701struct KeychainPrintCertificates {
702    /// Keychain domain to operate on
703    #[arg(long, value_parser = KEYCHAIN_DOMAINS, default_value = "user")]
704    domain: String,
705}
706
707impl CliCommand for KeychainPrintCertificates {
708    #[cfg(target_os = "macos")]
709    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
710        let domain = KeychainDomain::try_from(self.domain.as_str())
711            .expect("clap should have validated domain values");
712
713        let certs = keychain_find_code_signing_certificates(domain, None)?;
714
715        for (i, cert) in certs.into_iter().enumerate() {
716            println!("# Certificate {}", i);
717            println!();
718            print_certificate_info(&cert)?;
719            println!();
720        }
721
722        Ok(())
723    }
724
725    #[cfg(not(target_os = "macos"))]
726    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
727        Err(AppleCodesignError::CliGeneralError(
728            "macOS Keychain integration supported on macOS".to_string(),
729        ))
730    }
731}
732
733#[derive(Parser)]
734struct MachoUniversalCreate {
735    /// Input Mach-O binaries to combine.
736    input: Vec<PathBuf>,
737
738    /// Output file to write.
739    #[arg(short = 'o', long)]
740    output: PathBuf,
741}
742
743impl CliCommand for MachoUniversalCreate {
744    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
745        let mut builder = crate::macho_universal::UniversalBinaryBuilder::default();
746
747        for path in &self.input {
748            eprintln!("adding {}", path.display());
749            let data = std::fs::read(path)?;
750            builder.add_binary(data)?;
751        }
752
753        eprintln!("writing {}", self.output.display());
754
755        if let Some(parent) = self.output.parent() {
756            std::fs::create_dir_all(parent)?;
757        }
758
759        let mut fh = std::fs::File::create(&self.output)?;
760        simple_file_manifest::set_executable(&mut fh)?;
761        builder.write(&mut fh)?;
762
763        Ok(())
764    }
765}
766
767#[cfg(feature = "notarize")]
768#[derive(Parser)]
769struct NotaryList {
770    #[command(flatten)]
771    api: NotaryApi,
772}
773
774#[cfg(feature = "notarize")]
775impl CliCommand for NotaryList {
776    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
777        let notarizer = self.api.notarizer()?;
778
779        let submissions = notarizer.list_submissions()?;
780
781        for entry in &submissions.data {
782            println!(
783                "{} {} {} {} {}",
784                entry.id,
785                entry.attributes.created_date,
786                entry.attributes.name,
787                entry.r#type,
788                entry.attributes.status
789            );
790        }
791
792        Ok(())
793    }
794}
795
796#[cfg(feature = "notarize")]
797#[derive(Parser)]
798struct NotaryLog {
799    /// The ID of the previous submission to wait on
800    submission_id: String,
801
802    #[command(flatten)]
803    api: NotaryApi,
804}
805
806#[cfg(feature = "notarize")]
807impl CliCommand for NotaryLog {
808    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
809        let notarizer = self.api.notarizer()?;
810
811        let log = notarizer.fetch_notarization_log(&self.submission_id)?;
812
813        for line in serde_json::to_string_pretty(&log)?.lines() {
814            println!("{line}");
815        }
816
817        Ok(())
818    }
819}
820
821#[cfg(feature = "notarize")]
822#[derive(Parser)]
823struct NotarySubmit {
824    /// Whether to wait for upload processing to complete
825    #[arg(long)]
826    wait: bool,
827
828    /// Maximum time in seconds to wait for the upload result
829    #[arg(long, default_value = "600")]
830    max_wait_seconds: u64,
831
832    /// Staple the notarization ticket after successful upload (implies --wait)
833    #[arg(long)]
834    staple: bool,
835
836    /// Path to asset to upload
837    path: PathBuf,
838
839    #[command(flatten)]
840    api: NotaryApi,
841}
842
843#[cfg(feature = "notarize")]
844impl CliCommand for NotarySubmit {
845    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
846        let wait = self.wait || self.staple;
847
848        let wait_limit = if wait {
849            Some(std::time::Duration::from_secs(self.max_wait_seconds))
850        } else {
851            None
852        };
853        let notarizer = self.api.notarizer()?;
854
855        let upload = notarizer.notarize_path(&self.path, wait_limit)?;
856
857        if self.staple {
858            match upload {
859                crate::notarization::NotarizationUpload::UploadId(_) => {
860                    panic!(
861                        "NotarizationUpload::UploadId should not be returned if we waited successfully"
862                    );
863                }
864                crate::notarization::NotarizationUpload::NotaryResponse(_) => {
865                    let stapler = crate::stapling::Stapler::new()?;
866                    stapler.staple_path(&self.path)?;
867                }
868            }
869        }
870
871        Ok(())
872    }
873}
874
875#[cfg(feature = "notarize")]
876#[derive(Parser)]
877struct NotaryWait {
878    /// Maximum time in seconds to wait for the upload result
879    #[arg(long, default_value = "600")]
880    max_wait_seconds: u64,
881
882    /// The ID of the previous submission to wait on
883    submission_id: String,
884
885    #[command(flatten)]
886    api: NotaryApi,
887}
888
889#[cfg(feature = "notarize")]
890impl CliCommand for NotaryWait {
891    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
892        let wait_duration = std::time::Duration::from_secs(self.max_wait_seconds);
893        let notarizer = self.api.notarizer()?;
894
895        notarizer.wait_on_notarization_and_fetch_log(&self.submission_id, wait_duration)?;
896
897        Ok(())
898    }
899}
900
901#[derive(Parser)]
902struct ParseCodeSigningRequirement {
903    /// Output format
904    #[arg(long, value_parser = ["csrl", "expression-tree"], default_value = "csrl")]
905    format: String,
906
907    /// Path to file to parse
908    input_path: PathBuf,
909}
910
911impl CliCommand for ParseCodeSigningRequirement {
912    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
913        let data = std::fs::read(&self.input_path)?;
914
915        let requirements = CodeRequirements::parse_blob(&data)?.0;
916
917        for requirement in requirements.iter() {
918            match self.format.as_str() {
919                "csrl" => {
920                    println!("{requirement}");
921                }
922                "expression-tree" => {
923                    println!("{requirement:#?}");
924                }
925                format => panic!("unhandled format: {format}"),
926            }
927        }
928
929        Ok(())
930    }
931}
932
933#[derive(Parser)]
934struct PrintSignatureInfo {
935    /// Filesystem path to entity whose info to print
936    path: PathBuf,
937}
938
939impl CliCommand for PrintSignatureInfo {
940    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
941        let reader = SignatureReader::from_path(&self.path)?;
942
943        let entities = reader.entities()?;
944        serde_yaml::to_writer(std::io::stdout(), &entities)?;
945
946        Ok(())
947    }
948}
949
950#[derive(Args)]
951#[group(required = true, multiple = false)]
952struct SessionJoinString {
953    /// Open an editor to input the session join string
954    #[arg(long = "editor")]
955    session_join_string_editor: bool,
956
957    /// Path to file containing session join string
958    #[arg(long = "sjs-file", alias = "sjs-path")]
959    session_join_string_path: Option<PathBuf>,
960
961    /// Session join string (provided by the signing initiator)
962    session_join_string: Option<String>,
963}
964
965#[derive(Parser)]
966struct RemoteSign {
967    #[command(flatten)]
968    session_join_string: SessionJoinString,
969
970    #[command(flatten)]
971    certificate: CertificateSource,
972}
973
974impl CliCommand for RemoteSign {
975    fn as_config(&self) -> Result<Option<Config>, AppleCodesignError> {
976        Ok(Some(Config {
977            remote_sign: config::RemoteSignConfig {
978                signer: self.certificate.clone(),
979            },
980            ..Default::default()
981        }))
982    }
983
984    fn run(&self, context: &Context) -> Result<(), AppleCodesignError> {
985        let c = &context.config.remote_sign;
986
987        let session_join_string = if self.session_join_string.session_join_string_editor {
988            let mut value = None;
989
990            for _ in 0..3 {
991                if let Some(content) = dialoguer::Editor::new()
992                    .require_save(true)
993                    .edit("# Please enter the -----BEGIN SESSION JOIN STRING---- content below.\n# Remember to save the file!")?
994                {
995                    value = Some(content);
996                    break;
997                }
998            }
999
1000            value.ok_or_else(|| {
1001                AppleCodesignError::CliGeneralError(
1002                    "session join string not entered in editor".into(),
1003                )
1004            })?
1005        } else if let Some(path) = &self.session_join_string.session_join_string_path {
1006            std::fs::read_to_string(path)?
1007        } else if let Some(value) = &self.session_join_string.session_join_string {
1008            value.to_string()
1009        } else {
1010            return Err(AppleCodesignError::CliGeneralError(
1011                "session join string argument parsing failure".into(),
1012            ));
1013        };
1014
1015        let mut joiner = create_session_joiner(session_join_string)?;
1016
1017        let url = if let Some(key) = &c.signer.remote_signing_key {
1018            if let Some(env) = &key.shared_secret_env {
1019                let secret = std::env::var(env).map_err(|_| AppleCodesignError::CliBadArgument)?;
1020                joiner
1021                    .register_state(SessionJoinState::SharedSecret(secret.as_bytes().to_vec()))?;
1022            } else if let Some(secret) = &key.shared_secret {
1023                joiner
1024                    .register_state(SessionJoinState::SharedSecret(secret.as_bytes().to_vec()))?;
1025            }
1026
1027            key.url()
1028        } else {
1029            crate::remote_signing::DEFAULT_SERVER_URL.to_string()
1030        };
1031
1032        let signing_certs = c.signer.resolve_certificates(true)?;
1033
1034        let private = signing_certs.private_key()?;
1035
1036        let mut public_certificates = signing_certs.certs.clone();
1037        let cert = public_certificates.remove(0);
1038
1039        let certificates = if let Some(chain) = cert.apple_root_certificate_chain() {
1040            // The chain starts with self.
1041            chain.into_iter().skip(1).collect::<Vec<_>>()
1042        } else {
1043            public_certificates
1044        };
1045
1046        joiner.register_state(SessionJoinState::PublicKeyDecrypt(
1047            private.to_public_key_peer_decrypt()?,
1048        ))?;
1049
1050        let client = UnjoinedSigningClient::new_signer(
1051            joiner,
1052            private.as_key_info_signer(),
1053            cert,
1054            certificates,
1055            url,
1056        )?;
1057        client.run()?;
1058
1059        Ok(())
1060    }
1061}
1062
1063/// Signing arguments that can be scoped.
1064#[derive(Args, Clone, Debug, Eq, PartialEq)]
1065pub struct ScopedSigningArgs {
1066    /// Identifier string for binary. The value normally used by CFBundleIdentifier
1067    #[arg(long = "binary-identifier", value_name = "IDENTIFIER")]
1068    binary_identifiers: Vec<String>,
1069
1070    /// Path to a file containing binary code requirements data to be used as designated requirements
1071    #[arg(
1072        long = "code-requirements-file",
1073        alias = "code-requirements-path",
1074        value_name = "PATH"
1075    )]
1076    code_requirements_paths: Vec<String>,
1077
1078    /// Path to an XML plist file containing code resources
1079    #[arg(
1080        long = "code-resources-file",
1081        alias = "code-resources",
1082        value_name = "PATH"
1083    )]
1084    code_resources_paths: Vec<String>,
1085
1086    /// Code signature flags to set.
1087    ///
1088    /// Valid values: host, hard, kill, expires, library, runtime, linker-signed
1089    #[arg(long)]
1090    code_signature_flags: Vec<String>,
1091
1092    /// Digest algorithms to use.
1093    ///
1094    /// This typically doesn't need to be set since the OS targeting information
1095    /// from signed binaries implicitly derives appropriate digests to sign with.
1096    ///
1097    /// However, there are special cases where you may want to force use of
1098    /// specific digests.
1099    ///
1100    /// The first provided value will become the "primary" digest. Subsequent
1101    /// values will become alternative digests. The "primary" digest should be
1102    /// "older" to ensure compatibility with older clients.
1103    ///
1104    /// When targeting older Apple OS versions, SHA-1 should be the primary digest
1105    /// and SHA-256 should also be present for compatibility with newer OS versions.
1106    ///
1107    /// When targeting new OS versions, it is sufficient to only provide SHA-256
1108    /// digests.
1109    ///
1110    /// The following values are accepted: none, sha1, sha256, sha384, sha512.
1111    ///
1112    /// Important: only "sha1" and "sha256" are widely used and use of other
1113    /// algorithms may cause problems.
1114    #[arg(long = "digest", value_name = "DIGEST")]
1115    digests: Vec<String>,
1116
1117    /// Path to a plist file containing entitlements
1118    #[arg(
1119        short = 'e',
1120        long = "entitlements-xml-file",
1121        alias = "entitlements-xml-path",
1122        value_name = "PATH"
1123    )]
1124    entitlements_xml_paths: Vec<String>,
1125
1126    /// Launch constraints on the current executable.
1127    ///
1128    /// Specify the path to a plist XML file defining launch constraints.
1129    #[arg(long = "launch-constraints-self-file", value_name = "PATH")]
1130    launch_constraints_self_paths: Vec<String>,
1131
1132    /// Launch constraints on the parent process.
1133    ///
1134    /// Specify the path to a plist XML file defining launch constraints.
1135    #[arg(long = "launch-constraints-parent-file", value_name = "PATH")]
1136    launch_constraints_parent_paths: Vec<String>,
1137
1138    /// Launch constraints on the responsible process.
1139    ///
1140    /// Specify the path to a plist XML file defining launch constraints.
1141    #[arg(long = "launch-constraints-responsible-file", value_name = "PATH")]
1142    launch_constraints_responsible_paths: Vec<String>,
1143
1144    /// Constraints on loaded libraries.
1145    ///
1146    /// Specify the path to a plist XML file defining launch constraints.
1147    #[arg(long = "library-constraints-file", value_name = "PATH")]
1148    library_constraints_paths: Vec<String>,
1149
1150    /// Hardened runtime version to use (defaults to SDK version used to build binary)
1151    #[arg(long = "runtime-version", value_name = "VERSION")]
1152    runtime_versions: Vec<String>,
1153
1154    /// Path to an Info.plist file whose digest to include in Mach-O signature
1155    #[arg(
1156        long = "info-plist-file",
1157        alias = "info-plist-path",
1158        value_name = "PATH"
1159    )]
1160    info_plist_paths: Vec<String>,
1161}
1162
1163/// Represents the set of scopable signing settings for a given scope.
1164#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
1165#[serde(deny_unknown_fields)]
1166pub struct ScopedSigningSettingsValues {
1167    #[serde(default, skip_serializing_if = "Option::is_none")]
1168    pub binary_identifier: Option<String>,
1169    #[serde(default, skip_serializing_if = "Option::is_none")]
1170    pub code_requirements_file: Option<PathBuf>,
1171    #[serde(default, skip_serializing_if = "Option::is_none")]
1172    pub code_resources_file: Option<PathBuf>,
1173    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1174    pub code_signature_flags: Vec<String>,
1175    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1176    pub digests: Vec<String>,
1177    #[serde(default, skip_serializing_if = "Option::is_none")]
1178    pub entitlements_xml_file: Option<PathBuf>,
1179    #[serde(default, skip_serializing_if = "Option::is_none")]
1180    pub launch_constraints_self_file: Option<PathBuf>,
1181    #[serde(default, skip_serializing_if = "Option::is_none")]
1182    pub launch_constraints_parent_file: Option<PathBuf>,
1183    #[serde(default, skip_serializing_if = "Option::is_none")]
1184    pub launch_constraints_responsible_file: Option<PathBuf>,
1185    #[serde(default, skip_serializing_if = "Option::is_none")]
1186    pub library_constraints_file: Option<PathBuf>,
1187    #[serde(default, skip_serializing_if = "Option::is_none")]
1188    pub runtime_version: Option<String>,
1189    #[serde(default, skip_serializing_if = "Option::is_none")]
1190    pub info_plist_file: Option<PathBuf>,
1191}
1192
1193pub fn split_scoped_value(s: &str) -> (String, &str) {
1194    let parts = s.splitn(2, ':').collect::<Vec<_>>();
1195
1196    match parts.len() {
1197        1 => ("@main".into(), s),
1198        2 => (parts[0].to_string(), parts[1]),
1199        _ => {
1200            panic!("error splitting scoped value; this should not occur");
1201        }
1202    }
1203}
1204
1205/// A mapping of scopes to collections of signing settings.
1206///
1207/// This abstraction exists to make it easier to load config files.
1208pub struct ScopedSigningSettings(pub BTreeMap<String, ScopedSigningSettingsValues>);
1209
1210impl TryFrom<&ScopedSigningArgs> for ScopedSigningSettings {
1211    type Error = AppleCodesignError;
1212
1213    fn try_from(args: &ScopedSigningArgs) -> Result<Self, Self::Error> {
1214        let mut res = BTreeMap::<String, ScopedSigningSettingsValues>::default();
1215
1216        for value in &args.binary_identifiers {
1217            let (scope, value) = split_scoped_value(value);
1218            res.entry(scope).or_default().binary_identifier = Some(value.into());
1219        }
1220
1221        for value in &args.code_requirements_paths {
1222            let (scope, value) = split_scoped_value(value);
1223            res.entry(scope).or_default().code_requirements_file = Some(value.into());
1224        }
1225
1226        for value in &args.code_resources_paths {
1227            let (scope, value) = split_scoped_value(value);
1228            res.entry(scope).or_default().code_resources_file = Some(value.into());
1229        }
1230
1231        for value in &args.code_signature_flags {
1232            let (scope, value) = split_scoped_value(value);
1233            res.entry(scope)
1234                .or_default()
1235                .code_signature_flags
1236                .push(value.into());
1237        }
1238
1239        for value in &args.digests {
1240            let (scope, value) = split_scoped_value(value);
1241            res.entry(scope).or_default().digests.push(value.into());
1242        }
1243
1244        for value in &args.entitlements_xml_paths {
1245            let (scope, value) = split_scoped_value(value);
1246            res.entry(scope).or_default().entitlements_xml_file = Some(value.into());
1247        }
1248
1249        for value in &args.launch_constraints_self_paths {
1250            let (scope, value) = split_scoped_value(value);
1251            res.entry(scope).or_default().launch_constraints_self_file = Some(value.into());
1252        }
1253
1254        for value in &args.launch_constraints_parent_paths {
1255            let (scope, value) = split_scoped_value(value);
1256            res.entry(scope).or_default().launch_constraints_parent_file = Some(value.into());
1257        }
1258
1259        for value in &args.launch_constraints_responsible_paths {
1260            let (scope, value) = split_scoped_value(value);
1261            res.entry(scope)
1262                .or_default()
1263                .launch_constraints_responsible_file = Some(value.into());
1264        }
1265
1266        for value in &args.library_constraints_paths {
1267            let (scope, value) = split_scoped_value(value);
1268            res.entry(scope).or_default().library_constraints_file = Some(value.into());
1269        }
1270
1271        for value in &args.runtime_versions {
1272            let (scope, value) = split_scoped_value(value);
1273            res.entry(scope).or_default().runtime_version = Some(value.into());
1274        }
1275
1276        for value in &args.info_plist_paths {
1277            let (scope, value) = split_scoped_value(value);
1278            res.entry(scope).or_default().info_plist_file = Some(value.into());
1279        }
1280
1281        Ok(Self(res))
1282    }
1283}
1284
1285impl ScopedSigningSettings {
1286    pub fn load_into_settings(
1287        self,
1288        settings: &mut SigningSettings,
1289    ) -> Result<(), AppleCodesignError> {
1290        for (scope, values) in self.0 {
1291            let scope = SettingsScope::try_from(scope.as_str())?;
1292
1293            if let Some(v) = values.binary_identifier {
1294                settings.set_binary_identifier(scope.clone(), v);
1295            }
1296
1297            if let Some(v) = values.code_requirements_file {
1298                let code_requirements_data = std::fs::read(v)?;
1299                let reqs = CodeRequirements::parse_blob(&code_requirements_data)?.0;
1300                for expr in reqs.iter() {
1301                    warn!(
1302                        "setting designated code requirements for {}: {}",
1303                        scope, expr
1304                    );
1305
1306                    settings.set_designated_requirement_expression(scope.clone(), expr)?;
1307                }
1308            }
1309
1310            if let Some(path) = values.code_resources_file {
1311                warn!(
1312                    "setting code resources data for {} from path {}",
1313                    scope,
1314                    path.display()
1315                );
1316                let code_resources_data = std::fs::read(path)?;
1317                settings.set_code_resources_data(scope.clone(), code_resources_data);
1318            }
1319
1320            // If code signature flags are specified, they overwrite defaults. So reset
1321            // current values on the scope before setting anything.
1322            if !values.code_signature_flags.is_empty() {
1323                if let Some(existing) = settings.code_signature_flags(&scope) {
1324                    if existing != CodeSignatureFlags::empty() {
1325                        warn!(
1326                            "removing code signature flags {:?} from {}",
1327                            existing, scope
1328                        );
1329                    }
1330                }
1331
1332                settings.set_code_signature_flags(scope.clone(), CodeSignatureFlags::empty());
1333            }
1334
1335            for value in values.code_signature_flags {
1336                let flags = CodeSignatureFlags::from_str(&value)?;
1337                warn!("adding code signature flag {:?} to {}", flags, scope);
1338                settings.add_code_signature_flags(scope.clone(), flags);
1339            }
1340
1341            for (i, value) in values.digests.into_iter().enumerate() {
1342                let digest_type = DigestType::try_from(value.as_str())?;
1343
1344                if i == 0 {
1345                    settings.set_digest_type(scope.clone(), digest_type);
1346                } else {
1347                    settings.add_extra_digest(scope.clone(), digest_type);
1348                }
1349            }
1350
1351            if let Some(path) = values.entitlements_xml_file {
1352                warn!(
1353                    "setting entitlements XML for {} from path {}",
1354                    scope,
1355                    path.display()
1356                );
1357                let entitlements_data = std::fs::read_to_string(path)?;
1358                settings.set_entitlements_xml(scope.clone(), entitlements_data)?;
1359            }
1360
1361            if let Some(path) = values.launch_constraints_self_file {
1362                warn!(
1363                    "setting self launch constraints for {} from path {}",
1364                    scope,
1365                    path.display()
1366                );
1367                settings.set_launch_constraints_self(
1368                    scope.clone(),
1369                    EncodedEnvironmentConstraints::from_requirements_plist_file(path)?,
1370                );
1371            }
1372
1373            if let Some(path) = values.launch_constraints_parent_file {
1374                warn!(
1375                    "setting parent process launch constraints for {} from path {}",
1376                    scope,
1377                    path.display()
1378                );
1379                settings.set_launch_constraints_parent(
1380                    scope.clone(),
1381                    EncodedEnvironmentConstraints::from_requirements_plist_file(path)?,
1382                );
1383            }
1384
1385            if let Some(path) = values.launch_constraints_responsible_file {
1386                warn!(
1387                    "setting responsible process launch constraints for {} from path {}",
1388                    scope,
1389                    path.display()
1390                );
1391                settings.set_launch_constraints_responsible(
1392                    scope.clone(),
1393                    EncodedEnvironmentConstraints::from_requirements_plist_file(path)?,
1394                );
1395            }
1396
1397            if let Some(path) = values.library_constraints_file {
1398                warn!(
1399                    "setting loaded library constraints for {} from path {}",
1400                    scope,
1401                    path.display()
1402                );
1403                settings.set_library_constraints(
1404                    scope.clone(),
1405                    EncodedEnvironmentConstraints::from_requirements_plist_file(path)?,
1406                );
1407            }
1408
1409            if let Some(value) = values.runtime_version {
1410                let version = semver::Version::parse(&value)?;
1411                settings.set_runtime_version(scope.clone(), version);
1412            }
1413
1414            if let Some(path) = values.info_plist_file {
1415                let data = std::fs::read(path)?;
1416                settings.set_info_plist_data(scope, data);
1417            }
1418        }
1419
1420        Ok(())
1421    }
1422}
1423
1424#[derive(Parser)]
1425struct Sign {
1426    #[command(flatten)]
1427    scoped: ScopedSigningArgs,
1428
1429    /// Team name/identifier to include in code signature
1430    #[arg(long, value_name = "NAME")]
1431    team_name: Option<String>,
1432
1433    /// An RFC 3339 date and time string to be used in signatures.
1434    ///
1435    /// e.g. 2023-11-05T10:42:00Z.
1436    ///
1437    /// If not specified, the current time will be used.
1438    ///
1439    /// Setting is only used when signing with a signing certificate.
1440    ///
1441    /// This setting is typically not necessary. It was added to facilitate
1442    /// deterministic signing behavior.
1443    #[arg(long)]
1444    signing_time: Option<String>,
1445
1446    /// URL of time-stamp server to use to obtain a token of the CMS signature
1447    ///
1448    /// Can be set to the special value `none` to disable the generation of time-stamp
1449    /// tokens and use of a time-stamp server.
1450    #[arg(long, default_value = APPLE_TIMESTAMP_URL)]
1451    timestamp_url: String,
1452
1453    /// Glob expression of paths to exclude from signing
1454    #[arg(long)]
1455    exclude: Vec<String>,
1456
1457    /// Do not traverse into nested entities when signing.
1458    ///
1459    /// Some signable entities (like directory bundles) have child/nested entities
1460    /// that can be signed. By default, signing traversed into these entities and
1461    /// signs all entities recursively.
1462    ///
1463    /// Activating shallow signing mode using this flag overrides the default behavior.
1464    ///
1465    /// The behavior of this flag is subject to change. As currently implemented it
1466    /// will:
1467    ///
1468    /// * Prevent signing nested bundles when signing a bundle. e.g. if an app
1469    ///   bundle contains a framework, only the app bundle will be signed. Additional
1470    ///   Mach-O binaries within a bundle may still be signed with this flag set.
1471    ///
1472    /// Activating shallow signing mode can result in signing failures if the skipped
1473    /// nested entities aren't signed. For example, when signing an application bundle
1474    /// containing an unsigned nested bundle/framework, signing will fail with an
1475    /// error about a missing code signature. Always be sure to sign nested entities
1476    /// before their parents when this mode is activated.
1477    #[arg(long)]
1478    shallow: bool,
1479
1480    /// Indicate that the entity being signed will later be notarized.
1481    ///
1482    /// Notarized software is subject to specific requirements, such as enabling the
1483    /// hardened runtime.
1484    ///
1485    /// The presence of this flag influences signing settings and engages additional
1486    /// checks to help ensure that signed software can be successfully notarized.
1487    ///
1488    /// This flag is best effort. Notarization failures of software signed with
1489    /// this flag may be indicative of bugs in this software.
1490    ///
1491    /// The behavior of this flag is subject to change. As currently implemented,
1492    /// it will:
1493    ///
1494    /// * Require the use of a "Developer ID" signing certificate issued by Apple.
1495    /// * Require the use of a time-stamp server.
1496    /// * Enable the hardened runtime code signature flag on all Mach-O binaries
1497    ///   (equivalent to `--code-signature-flags runtime` for all signed paths).
1498    #[arg(long)]
1499    for_notarization: bool,
1500
1501    /// Path to Mach-O binary to sign
1502    input_path: PathBuf,
1503
1504    /// Path to signed Mach-O binary to write
1505    output_path: Option<PathBuf>,
1506
1507    #[command(flatten)]
1508    certificate: CertificateSource,
1509}
1510
1511impl CliCommand for Sign {
1512    fn as_config(&self) -> Result<Option<Config>, AppleCodesignError> {
1513        let paths = ScopedSigningSettings::try_from(&self.scoped)?;
1514
1515        Ok(Some(Config {
1516            sign: config::SignConfig {
1517                signer: self.certificate.clone(),
1518                paths: paths.0,
1519            },
1520            ..Default::default()
1521        }))
1522    }
1523
1524    fn run(&self, context: &Context) -> Result<(), AppleCodesignError> {
1525        let c = &context.config.sign;
1526
1527        let mut settings = SigningSettings::default();
1528
1529        let certs = c.signer.resolve_certificates(true)?;
1530        certs.load_into_signing_settings(&mut settings)?;
1531
1532        // Doesn't make sense to set a time-stamp server URL unless we're generating
1533        // CMS signatures.
1534        if settings.signing_key().is_some() && self.timestamp_url != "none" {
1535            warn!("using time-stamp protocol server {}", self.timestamp_url);
1536            settings.set_time_stamp_url(&self.timestamp_url)?;
1537        }
1538
1539        if let Some(time) = &self.signing_time {
1540            let time = chrono::DateTime::parse_from_rfc3339(time).map_err(|e| {
1541                AppleCodesignError::CliGeneralError(format!("invalid signing time format: {}", e))
1542            })?;
1543            let time = time.with_timezone(&chrono::Utc);
1544            settings.set_signing_time(time);
1545        }
1546
1547        if let Some(team_id) = settings.set_team_id_from_signing_certificate() {
1548            warn!(
1549                "automatically setting team ID from signing certificate: {}",
1550                team_id
1551            );
1552        }
1553
1554        if let Some(team_name) = &self.team_name {
1555            settings.set_team_id(team_name);
1556        }
1557
1558        settings.set_shallow(self.shallow);
1559        settings.set_for_notarization(self.for_notarization);
1560
1561        for pattern in &self.exclude {
1562            settings.add_path_exclusion(pattern)?;
1563        }
1564
1565        ScopedSigningSettings(c.paths.clone()).load_into_settings(&mut settings)?;
1566
1567        settings.ensure_for_notarization_settings()?;
1568
1569        // Settings are locked in. Proceed to sign.
1570
1571        let signer = UnifiedSigner::new(settings);
1572
1573        if let Some(output_path) = &self.output_path {
1574            warn!(
1575                "signing {} to {}",
1576                self.input_path.display(),
1577                output_path.display()
1578            );
1579
1580            signer.sign_path(&self.input_path, output_path)?;
1581        } else {
1582            warn!("signing {} in place", self.input_path.display());
1583            signer.sign_path_in_place(&self.input_path)?;
1584        }
1585
1586        if let Some(private) = certs.private_key_optional()? {
1587            private.finish()?;
1588        }
1589
1590        Ok(())
1591    }
1592}
1593
1594#[derive(Parser)]
1595struct SmartcardScan {}
1596
1597impl CliCommand for SmartcardScan {
1598    #[cfg(feature = "yubikey")]
1599    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1600        let mut ctx = ::yubikey::reader::Context::open()?;
1601        for (index, reader) in ctx.iter()?.enumerate() {
1602            println!("Device {}: {}", index, reader.name());
1603
1604            if let Ok(yk) = reader.open() {
1605                let mut yk = crate::yubikey::YubiKey::from(yk);
1606                println!("Device {}: Serial: {}", index, yk.inner()?.serial());
1607                println!("Device {}: Version: {}", index, yk.inner()?.version());
1608
1609                for (slot, cert) in yk.find_certificates()? {
1610                    println!(
1611                        "Device {}: Certificate in slot {:?} / {}",
1612                        index,
1613                        slot,
1614                        hex::encode([u8::from(slot)])
1615                    );
1616                    print_certificate_info(&cert)?;
1617                    println!();
1618                }
1619            }
1620        }
1621
1622        Ok(())
1623    }
1624
1625    #[cfg(not(feature = "yubikey"))]
1626    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1627        eprintln!("smartcard reading requires the `yubikey` crate feature, which isn't enabled.");
1628        eprintln!("recompile the crate with `cargo build --features yubikey` to enable support");
1629        std::process::exit(1);
1630    }
1631}
1632
1633#[derive(Parser)]
1634struct SmartcardGenerateKey {
1635    /// Smartcard slot number to store key in (9c is common)
1636    #[arg(long)]
1637    smartcard_slot: String,
1638
1639    #[command(flatten)]
1640    policy: YubikeyPolicy,
1641}
1642
1643impl CliCommand for SmartcardGenerateKey {
1644    #[cfg(feature = "yubikey")]
1645    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1646        let slot_id = ::yubikey::piv::SlotId::from_str(&self.smartcard_slot)?;
1647
1648        let touch_policy = str_to_touch_policy(self.policy.touch_policy.as_str())?;
1649        let pin_policy = str_to_pin_policy(self.policy.pin_policy.as_str())?;
1650
1651        let mut yk = YubiKey::new()?;
1652        yk.set_pin_callback(prompt_smartcard_pin);
1653
1654        yk.generate_key(slot_id, touch_policy, pin_policy)?;
1655
1656        Ok(())
1657    }
1658
1659    #[cfg(not(feature = "yubikey"))]
1660    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1661        eprintln!(
1662            "smartcard integration requires the `yubikey` crate feature, which isn't enabled."
1663        );
1664        eprintln!("recompile the crate with `cargo build --features yubikey` to enable support");
1665        std::process::exit(1);
1666    }
1667}
1668
1669#[derive(Parser)]
1670struct SmartcardImport {
1671    /// Re-use the existing private key in the smartcard slot
1672    #[arg(long)]
1673    existing_key: bool,
1674
1675    /// Don't actually perform the import
1676    #[arg(long)]
1677    dry_run: bool,
1678
1679    #[command(flatten)]
1680    certificate: CertificateSource,
1681
1682    #[command(flatten)]
1683    policy: YubikeyPolicy,
1684}
1685
1686impl CliCommand for SmartcardImport {
1687    #[cfg(feature = "yubikey")]
1688    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1689        let signing_certs = self.certificate.resolve_certificates(false)?;
1690
1691        let slot_id = ::yubikey::piv::SlotId::from_str(
1692            self.certificate
1693                .smartcard_key
1694                .as_ref()
1695                .unwrap()
1696                .slot
1697                .as_ref()
1698                .ok_or_else(|| {
1699                    error!("--smartcard-slot is required");
1700                    AppleCodesignError::CliBadArgument
1701                })?,
1702        )?;
1703        let touch_policy = str_to_touch_policy(self.policy.touch_policy.as_str())?;
1704        let pin_policy = str_to_pin_policy(self.policy.pin_policy.as_str())?;
1705
1706        println!(
1707            "found {} private keys and {} public certificates",
1708            signing_certs.keys.len(),
1709            signing_certs.certs.len()
1710        );
1711
1712        let key = if self.existing_key {
1713            println!("using existing private key in smartcard");
1714
1715            if !signing_certs.keys.is_empty() {
1716                println!(
1717                    "ignoring {} private keys specified via arguments",
1718                    signing_certs.keys.len()
1719                );
1720            }
1721
1722            None
1723        } else {
1724            Some(signing_certs.private_key()?)
1725        };
1726
1727        let cert = signing_certs
1728            .certs
1729            .clone()
1730            .into_iter()
1731            .next()
1732            .ok_or_else(|| {
1733                println!("no public certificates found");
1734                AppleCodesignError::CliBadArgument
1735            })?;
1736
1737        println!(
1738            "Will import the following certificate into slot {}",
1739            hex::encode([u8::from(slot_id)])
1740        );
1741        print_certificate_info(&cert)?;
1742
1743        let mut yk = YubiKey::new()?;
1744        yk.set_pin_callback(prompt_smartcard_pin);
1745
1746        if self.dry_run {
1747            println!("dry run mode enabled; stopping");
1748            return Ok(());
1749        }
1750
1751        if let Some(key) = key {
1752            yk.import_key(
1753                slot_id,
1754                key.as_key_info_signer(),
1755                &cert,
1756                touch_policy,
1757                pin_policy,
1758            )?;
1759        } else {
1760            yk.import_certificate(slot_id, &cert)?;
1761        }
1762
1763        Ok(())
1764    }
1765
1766    #[cfg(not(feature = "yubikey"))]
1767    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1768        eprintln!("smartcard import requires `yubikey` crate feature, which isn't enabled.");
1769        eprintln!("recompile the crate with `cargo build --features yubikey` to enable support");
1770        std::process::exit(1);
1771    }
1772}
1773
1774#[derive(Parser)]
1775struct Staple {
1776    /// Path to entity to attempt to staple
1777    path: PathBuf,
1778}
1779
1780impl CliCommand for Staple {
1781    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1782        let stapler = crate::stapling::Stapler::new()?;
1783        stapler.staple_path(&self.path)?;
1784
1785        Ok(())
1786    }
1787}
1788
1789#[derive(Parser)]
1790struct Verify {
1791    /// Path of Mach-O binary to examine
1792    path: PathBuf,
1793}
1794
1795impl CliCommand for Verify {
1796    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1797        let path_type = crate::PathType::from_path(&self.path)?;
1798
1799        if path_type != crate::PathType::MachO {
1800            return Err(AppleCodesignError::CliGeneralError(format!(
1801                "verify command only works on Mach-O binaries; provided path is a {:?}",
1802                path_type
1803            )));
1804        }
1805
1806        warn!("(the verify command is known to be buggy and gives misleading results; we highly recommend using Apple's tooling until this message is removed)");
1807        let data = std::fs::read(&self.path)?;
1808
1809        let problems = crate::verify::verify_macho_data(data);
1810
1811        for problem in &problems {
1812            println!("{problem}");
1813        }
1814
1815        if problems.is_empty() {
1816            eprintln!("no problems detected!");
1817            eprintln!("(we do not verify everything so please do not assume that the signature meets Apple standards)");
1818            Ok(())
1819        } else {
1820            Err(AppleCodesignError::VerificationProblems)
1821        }
1822    }
1823}
1824
1825#[derive(Parser)]
1826struct WindowsStoreExportCertificateChain {
1827    /// Windows Store to operate on
1828    #[arg(long, value_parser = WINDOWS_STORE_NAMES, default_value = "user", value_name = "STORE")]
1829    windows_store_name: String,
1830
1831    /// Print only the issuing certificate chain, not the subject certificate
1832    #[arg(long)]
1833    no_print_self: bool,
1834
1835    /// SHA-1 thumbprint of code signing certificate to find and whose CA chain to export
1836    #[arg(long)]
1837    thumbprint: String,
1838}
1839
1840impl CliCommand for WindowsStoreExportCertificateChain {
1841    #[cfg(target_os = "windows")]
1842    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1843        let store_name = StoreName::try_from(self.windows_store_name.as_str())
1844            .expect("clap should have validated store name values");
1845
1846        let certs = windows_store_find_certificate_chain(store_name, &self.thumbprint)?;
1847
1848        for (i, cert) in certs.iter().enumerate() {
1849            if self.no_print_self && i == 0 {
1850                continue;
1851            }
1852
1853            print!("{}", cert.encode_pem());
1854        }
1855
1856        Ok(())
1857    }
1858
1859    #[cfg(not(target_os = "windows"))]
1860    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1861        Err(AppleCodesignError::CliGeneralError(
1862            "Windows Store export only supported on Windows".to_string(),
1863        ))
1864    }
1865}
1866
1867#[derive(Parser)]
1868struct WindowsStorePrintCertificates {
1869    /// Windows Store name to operate on
1870    #[arg(long, value_parser = WINDOWS_STORE_NAMES, default_value = "user", value_name = "STORE")]
1871    windows_store_name: String,
1872}
1873
1874impl CliCommand for WindowsStorePrintCertificates {
1875    #[cfg(target_os = "windows")]
1876    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1877        let store_name = StoreName::try_from(self.windows_store_name.as_str())
1878            .expect("clap should have validated store name values");
1879
1880        let certs = windows_store_find_code_signing_certificates(store_name)?;
1881
1882        for (i, cert) in certs.into_iter().enumerate() {
1883            println!("# Certificate {}", i);
1884            println!();
1885            print_certificate_info(&cert)?;
1886            println!();
1887        }
1888
1889        Ok(())
1890    }
1891
1892    #[cfg(not(target_os = "windows"))]
1893    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1894        Err(AppleCodesignError::CliGeneralError(
1895            "Windows Store integration only supported on Windows".to_string(),
1896        ))
1897    }
1898}
1899
1900#[derive(Parser)]
1901struct X509Oids {}
1902
1903impl CliCommand for X509Oids {
1904    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
1905        println!("# Extended Key Usage (EKU) Extension OIDs");
1906        println!();
1907        for ekup in crate::certificate::ExtendedKeyUsagePurpose::all() {
1908            println!("{}\t{:?}", ekup.as_oid(), ekup);
1909        }
1910        println!();
1911        println!("# Code Signing Certificate Extension OIDs");
1912        println!();
1913        for ext in crate::certificate::CodeSigningCertificateExtension::all() {
1914            println!("{}\t{:?}", ext.as_oid(), ext);
1915        }
1916        println!();
1917        println!("# Certificate Authority Certificate Extension OIDs");
1918        println!();
1919        for ext in crate::certificate::CertificateAuthorityExtension::all() {
1920            println!("{}\t{:?}", ext.as_oid(), ext);
1921        }
1922
1923        Ok(())
1924    }
1925}
1926
1927#[derive(Subcommand)]
1928#[allow(clippy::large_enum_variant)]
1929enum Subcommands {
1930    /// Analyze an X.509 certificate for Apple code signing properties.
1931    ///
1932    /// Given the path to a PEM encoded X.509 certificate, this command will read
1933    /// the certificate and print information about it relevant to Apple code
1934    /// signing.
1935    ///
1936    /// The output of the command can be useful to learn about X.509 certificate
1937    /// extensions used by code signing certificates and to debug low-level
1938    /// properties related to certificates.
1939    AnalyzeCertificate(AnalyzeCertificate),
1940
1941    /// Compute code hashes for a binary
1942    ComputeCodeHashes(ComputeCodeHashes),
1943
1944    /// Create a binary code requirements file.
1945    #[command(hide = true)]
1946    DebugCreateCodeRequirements(debug_commands::DebugCreateCodeRequirements),
1947
1948    /// Create a (launch or library) constraints file.
1949    #[command(hide = true)]
1950    DebugCreateConstraints(debug_commands::DebugCreateConstraints),
1951
1952    /// Create an entitlements file.
1953    #[command(hide = true)]
1954    DebugCreateEntitlements(debug_commands::DebugCreateEntitlements),
1955
1956    /// Create an Info.plist file.
1957    #[command(hide = true)]
1958    DebugCreateInfoPlist(debug_commands::DebugCreateInfoPlist),
1959
1960    /// Create a Mach-O binary from parameters.
1961    #[command(hide = true)]
1962    DebugCreateMacho(debug_commands::DebugCreateMachO),
1963
1964    /// Print a filesystem tree with basic metadata.
1965    #[command(hide = true)]
1966    DebugFileTree(debug_commands::DebugFileTree),
1967
1968    /// Print a diff between the signature content of two paths
1969    DiffSignatures(DiffSignatures),
1970
1971    /// Encode App Store Connect API Key metadata to JSON
1972    ///
1973    /// App Store Connect API Keys
1974    /// (https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api)
1975    /// are defined by 3 components:
1976    ///
1977    /// * The Issuer ID (likely a UUID)
1978    /// * A Key ID (an alphanumeric value like `DEADBEEF42`)
1979    /// * A PEM encoded ECDSA private key (typically a file beginning with
1980    ///   `-----BEGIN PRIVATE KEY-----`).
1981    ///
1982    /// This command is used to encode all API Key components into a single JSON
1983    /// object so you only have to refer to a single entity when performing
1984    /// operations (like notarization) using these API Keys.
1985    ///
1986    /// The API Key components are specified as positional arguments.
1987    ///
1988    /// By default, the JSON encoded unified representation is printed to stdout.
1989    /// You can write to a file instead by passing `--output-path <path>`.
1990    ///
1991    /// # Security Considerations
1992    ///
1993    /// The App Store Connect API Key contains a private key and its value should be
1994    /// treated as sensitive: if an unwanted party obtains your private key, they
1995    /// effectively have access to your App Store Connect account.
1996    ///
1997    /// When this command writes JSON files, an attempt is made to limit access
1998    /// to the file. However, file access restrictions may not be as secure as you
1999    /// want. Security conscious individuals should audit the permissions of the
2000    /// file and adjust accordingly.
2001    #[cfg(feature = "notarize")]
2002    #[command(verbatim_doc_comment)]
2003    EncodeAppStoreConnectApiKey(EncodeAppStoreConnectApiKey),
2004
2005    /// Print/extract various information from a Mach-O binary.
2006    ///
2007    /// Given the path to a Mach-O binary (including fat/universal binaries), this
2008    /// command will attempt to locate and format the requested data.
2009    #[command(override_usage = "rcodesign extract [OPTIONS] <COMMAND> <INPUT_PATH>")]
2010    Extract(extract_commands::Extract),
2011
2012    /// Generates a certificate signing request that can be sent to Apple and exchanged for a signing certificate
2013    GenerateCertificateSigningRequest(GenerateCertificateSigningRequest),
2014
2015    /// Generate a self-signed certificate for code signing
2016    ///
2017    /// This command will generate a new key pair using the algorithm of choice
2018    /// then create an X.509 certificate wrapper for it that is signed with the
2019    /// just-generated private key. The created X.509 certificate has extensions
2020    /// that mark it as appropriate for code signing.
2021    ///
2022    /// Certificates generated with this command can be useful for local testing.
2023    /// However, because it is a self-signed certificate and isn't signed by a
2024    /// trusted certificate authority, Apple operating systems may refuse to
2025    /// load binaries signed with it.
2026    ///
2027    /// By default the command prints 2 PEM encoded blocks. One block is for the
2028    /// X.509 public certificate. The other is for the PKCS#8 private key (which
2029    /// can include the public key).
2030    ///
2031    /// The `--pem-filename` argument can be specified to write the generated
2032    /// certificate pair to a pair of files. The destination files will have
2033    /// `.crt` and `.key` appended to the value provided.
2034    ///
2035    /// When the certificate is written to a file, it isn't printed to stdout.
2036    GenerateSelfSignedCertificate(GenerateSelfSignedCertificate),
2037
2038    /// Export Apple CA certificates from the macOS Keychain
2039    KeychainExportCertificateChain(KeychainExportCertificateChain),
2040
2041    /// Print information about certificates in the macOS keychain
2042    KeychainPrintCertificates(KeychainPrintCertificates),
2043
2044    /// Create a universal ("fat") Mach-O binary.
2045    ///
2046    /// This is similar to the `lipo -create` command. Use it to stitch
2047    /// multiple single architecture Mach-O binaries into a single multi-arch
2048    /// binary.
2049    MachoUniversalCreate(MachoUniversalCreate),
2050
2051    #[cfg(feature = "notarize")]
2052    /// List notarization submissions
2053    NotaryList(NotaryList),
2054
2055    #[cfg(feature = "notarize")]
2056    /// Fetch the notarization log for a previous submission
2057    NotaryLog(NotaryLog),
2058
2059    /// Upload an asset to Apple for notarization and possibly staple it
2060    ///
2061    /// This command is used to submit an asset to Apple for notarization. Given
2062    /// a path to an asset with a code signature, this command will connect to Apple's
2063    /// Notary API and upload the asset. It will then optionally wait on the submission
2064    /// to finish processing (which typically takes a few dozen seconds). If the
2065    /// asset validates Apple's requirements, Apple will issue a *notarization ticket*
2066    /// as proof that they approved of it. This ticket is then added to the asset in a
2067    /// process called *stapling*, which this command can do automatically if the
2068    /// `--staple` argument is passed.
2069    ///
2070    /// # App Store Connect API Key
2071    ///
2072    /// In order to communicate with Apple's servers, you need an App Store Connect
2073    /// API Key. This requires an Apple Developer account. You can generate an
2074    /// API Key at https://appstoreconnect.apple.com/access/api.
2075    ///
2076    /// The recommended mechanism to define the API Key is via `--api-key-path`,
2077    /// which takes the path to a file containing JSON produced by the
2078    /// `encode-app-store-connect-api-key` command. See that command's help for
2079    /// more details.
2080    ///
2081    /// If you don't wish to use `--api-key-path`, you can define the key components
2082    /// via the `--api-issuer` and `--api-key` arguments. You will need a file named
2083    /// `AuthKey_<ID>.p8` in one of the following locations: `$(pwd)/private_keys/`,
2084    /// `~/private_keys/`, '~/.private_keys/`, and `~/.appstoreconnect/private_keys/`
2085    /// (searched in that order). The name of the file is derived from the value of
2086    /// `--api-key`.
2087    ///
2088    /// In all cases, App Store Connect API Keys can be managed at
2089    /// https://appstoreconnect.apple.com/access/api.
2090    ///
2091    /// # Modes of Operation
2092    ///
2093    /// By default, the `notarize` command will initiate an upload to Apple and exit
2094    /// once the upload is complete.
2095    ///
2096    /// Once an upload is performed, Apple will asynchronously process the uploaded
2097    /// content. This can take seconds to minutes.
2098    ///
2099    /// To poll Apple's servers and wait on the server-side processing to finish,
2100    /// specify `--wait`. This will query the state of the processing every few seconds
2101    /// until it is finished, the max wait time is reached, or an error occurs.
2102    ///
2103    /// To automatically staple an asset after server-side processing has finished,
2104    /// specify `--staple`. This implies `--wait`.
2105    #[cfg(feature = "notarize")]
2106    #[command(alias = "notarize")]
2107    NotarySubmit(NotarySubmit),
2108
2109    /// Wait for completion of a previous submission
2110    #[cfg(feature = "notarize")]
2111    NotaryWait(NotaryWait),
2112
2113    /// Parse binary Code Signing Requirement data into a human readable string
2114    ///
2115    /// This command can be used to parse binary code signing requirement data and
2116    /// print it in various formats.
2117    ///
2118    /// The source input format is the binary code requirement serialization. This
2119    /// is the format generated by Apple's `csreq` tool via `csreq -b`. The binary
2120    /// data begins with header magic `0xfade0c00`.
2121    ///
2122    /// The default output format is the Code Signing Requirement Language. But the
2123    /// output format can be changed via the --format argument.
2124    ///
2125    /// Our Code Signing Requirement Language output may differ from Apple's. For
2126    /// example, `and` and `or` expressions always have their sub-expressions surrounded
2127    /// by parentheses (e.g. `(a) and (b)` instead of `a and b`) and strings are always
2128    /// quoted. The differences, however, should not matter to the parser or result
2129    /// in a different binary serialization.
2130    ParseCodeSigningRequirement(ParseCodeSigningRequirement),
2131
2132    /// Print signature information for a filesystem path
2133    PrintSignatureInfo(PrintSignatureInfo),
2134
2135    /// Create signatures initiated from a remote signing operation
2136    RemoteSign(RemoteSign),
2137
2138    /// Adds code signatures to a signable entity.
2139    ///
2140    /// This command can sign the following entities:
2141    ///
2142    /// * A single Mach-O binary (specified by its file path)
2143    /// * A bundle (specified by its directory path)
2144    /// * A DMG disk image (specified by its path)
2145    /// * A XAR archive (commonly a .pkg installer file)
2146    ///
2147    /// If the input is Mach-O binary, it can be a single or multiple/fat/universal
2148    /// Mach-O binary. If a fat binary is given, each Mach-O within that binary will
2149    /// be signed.
2150    ///
2151    /// If the input is a bundle, the bundle will be recursively signed. If the
2152    /// bundle contains nested bundles or Mach-O binaries, those will be signed
2153    /// automatically.
2154    ///
2155    /// # Settings Scope
2156    ///
2157    /// The following signing settings are global and apply to all signed entities:
2158    ///
2159    /// * --pem-source
2160    /// * --team-name
2161    /// * --timestamp-url
2162    ///
2163    /// The following signing settings can be scoped so they only apply to certain
2164    /// entities:
2165    ///
2166    /// * --digest
2167    /// * --binary-identifier
2168    /// * --code-requirements-files
2169    /// * --code-resources-file
2170    /// * --code-signature-flags
2171    /// * --entitlements-xml-file
2172    /// * --info-plist-file
2173    ///
2174    /// Scoped settings take the form <value> or <scope>:<value>. If the 2nd form
2175    /// is used, the string before the first colon is parsed as a \"scoping string\".
2176    /// It can have the following values:
2177    ///
2178    /// * `main` - Applies to the main entity being signed and all nested entities.
2179    /// * `@<integer>` - e.g. `@0`. Applies to a Mach-O within a fat binary at the
2180    ///   specified index. 0 means the first Mach-O in a fat binary.
2181    /// * `@[cpu_type=<int>` - e.g. `@[cpu_type=7]`. Applies to a Mach-O within a fat
2182    ///   binary targeting a numbered CPU architecture (using numeric constants
2183    ///   as defined by Mach-O).
2184    /// * `@[cpu_type=<string>` - e.g. `@[cpu_type=x86_64]`. Applies to a Mach-O within
2185    ///   a fat binary targeting a CPU architecture identified by a string. See below
2186    ///   for the list of recognized values.
2187    /// * `<string>` - e.g. `path/to/file`. Applies to content at a given path. This
2188    ///   should be the bundle-relative path to a Mach-O binary, a nested bundle, or
2189    ///   a Mach-O binary within a nested bundle. If a nested bundle is referenced,
2190    ///   settings apply to everything within that bundle.
2191    /// * `<string>@<int>` - e.g. `path/to/file@0`. Applies to a Mach-O within a
2192    ///   fat binary at the given path. If the path is to a bundle, the setting applies
2193    ///   to all Mach-O binaries in that bundle.
2194    /// * `<string>@[cpu_type=<int|string>]` e.g. `Contents/MacOS/binary@[cpu_type=7]`
2195    ///   or `Contents/MacOS/binary@[cpu_type=arm64]`. Applies to a Mach-O within a
2196    ///   fat binary targeting a CPU architecture identified by its integer constant
2197    ///   or string name. If the path is to a bundle, the setting applies to all
2198    ///   Mach-O binaries in that bundle.
2199    ///
2200    /// The following named CPU architectures are recognized:
2201    ///
2202    /// * arm
2203    /// * arm64
2204    /// * arm64_32
2205    /// * x86_64
2206    ///
2207    /// Signing will traverse into nested entities:
2208    ///
2209    /// * A fat Mach-O binary will traverse into the multiple Mach-O binaries within.
2210    /// * A bundle will traverse into nested bundles.
2211    /// * A bundle will traverse non-code "resource" files and sign their digests.
2212    /// * A bundle will traverse non-main Mach-O binaries and sign them, adding their
2213    ///   metadata to the signed resources file.
2214    ///
2215    /// When signing nested entities, only some signing settings will be copied
2216    /// automatically:
2217    ///
2218    /// * All settings related to the signing certificate/key.
2219    /// * --timestamp-url
2220    /// * --signing-time
2221    /// * --exclude
2222    /// * --digest
2223    /// * --runtime-version
2224    ///
2225    /// All other settings only apply to the main entity being signed or the
2226    /// scoped path being annotated.
2227    ///
2228    /// # Bundle Signing Overrides Settings
2229    ///
2230    /// When signing bundles, some settings specified on the command line will be
2231    /// ignored. This is to ensure that the produced signing data is correct. The
2232    /// settings ignored include (but may not be limited to):
2233    ///
2234    /// * --binary-identifier for the main executable. The `CFBundleIdentifier` value
2235    ///   from the bundle's `Info.plist` will be used instead.
2236    /// * --code-resources-path. The code resources data will be computed automatically
2237    ///   as part of signing the bundle.
2238    /// * --info-plist-path. The `Info.plist` from the bundle will be used instead.
2239    /// * --digest
2240    ///
2241    /// # Designated Code Requirements
2242    ///
2243    /// When using Apple issued code signing certificates, we will attempt to apply
2244    /// an appropriate designated requirement automatically during signing which
2245    /// matches the behavior of what `codesign` would do. We do not yet support all
2246    /// signing certificates and signing targets for this, however. So you may
2247    /// need to provide your own requirements.
2248    ///
2249    /// Designated code requirements can be specified via --code-requirements-path.
2250    ///
2251    /// This file MUST contain a binary/compiled code requirements expression. We do
2252    /// not (yet) support parsing the human-friendly code requirements DSL. A
2253    /// binary/compiled file can be produced via Apple's `csreq` tool. e.g.
2254    /// `csreq -r '=<expression>' -b /output/path`. If code requirements data is
2255    /// specified, it will be parsed and displayed as part of signing to ensure it
2256    /// is well-formed.
2257    ///
2258    /// # Code Signing Key Pair
2259    ///
2260    /// By default, the embedded code signature will only contain digests of the
2261    /// binary and other important entities (such as entitlements and resources).
2262    /// This is often referred to as \"ad-hoc\" signing.
2263    ///
2264    /// To use a code signing key/certificate to derive a cryptographic signature,
2265    /// you must specify a source certificate to use. This can be done in the following
2266    /// ways:
2267    ///
2268    /// * The --p12-file denotes the location to a PFX formatted file. These are
2269    ///   often .pfx or .p12 files. A password is required to open these files.
2270    ///   Specify one via --p12-password or --p12-password-file or enter a password
2271    ///   when prompted.
2272    /// * The --pem-file argument defines paths to files containing PEM encoded
2273    ///   certificate/key data. (e.g. files with \"===== BEGIN CERTIFICATE =====\").
2274    /// * The --certificate-der-file argument defines paths to files containing DER
2275    ///   encoded certificate/key data.
2276    /// * The --keychain-domain and --keychain-fingerprint arguments can be used to
2277    ///   load code signing certificates from macOS keychains. These arguments are
2278    ///   ignored on non-macOS platforms.
2279    /// * The --windows-store-name and --windows-store-cert-fingerprint arguments can be used to
2280    ///   load code signing certificates from the Windows store. These arguments are
2281    ///   ignored on non-Windows platforms.
2282    /// * The --smartcard-slot argument defines the name of a slot in a connected
2283    ///   smartcard device to read from. `9c` is common.
2284    /// * Arguments beginning with --remote activate *remote signing mode* and can
2285    ///   be used to delegate cryptographic signing operations to a separate machine.
2286    ///   It is strongly advised to read the user documentation on remote signing
2287    ///   mode at https://gregoryszorc.com/docs/apple-codesign/main/.
2288    ///
2289    /// If you export a code signing certificate from the macOS keychain via the
2290    /// `Keychain Access` application as a .p12 file, we should be able to read these
2291    /// files via --p12-file.
2292    ///
2293    /// When using --pem-file, certificates and public keys are parsed from
2294    /// `BEGIN CERTIFICATE` and `BEGIN PRIVATE KEY` sections in the files.
2295    ///
2296    /// The way certificate discovery works is that --p12-file is read followed by
2297    /// all values to --pem-file. The seen signing keys and certificates are
2298    /// collected. After collection, there must be 0 or 1 signing keys present, or
2299    /// an error occurs. The first encountered public certificate is assigned
2300    /// to be paired with the signing key. All remaining certificates are assumed
2301    /// to constitute the CA issuing chain and will be added to the signature
2302    /// data to facilitate validation.
2303    ///
2304    /// If you are using an Apple-issued code signing certificate, we detect this
2305    /// and automatically register the Apple CA certificate chain so it is included
2306    /// in the digital signature. This matches the behavior of the `codesign` tool.
2307    ///
2308    /// For best results, put your private key and its corresponding X.509 certificate
2309    /// in a single file, either a PFX or PEM formatted file. Then add any additional
2310    /// certificates constituting the signing chain in a separate PEM file.
2311    ///
2312    /// When using a code signing key/certificate, a Time-Stamp Protocol server URL
2313    /// can be specified via --timestamp-url. By default, Apple's server is used. The
2314    /// special value \"none\" can disable using a timestamp server.
2315    ///
2316    /// # Selecting What to Sign
2317    ///
2318    /// By default, this command attempts to recursively sign everything in the source
2319    /// path. This applies to:
2320    ///
2321    /// * Bundles. If the specified bundle has nested bundles, those nested bundles
2322    ///   will be signed automatically.
2323    ///
2324    /// It is possible to exclude nested items from signing using --exclude. This
2325    /// argument takes a glob expression that matches *relative paths* from the
2326    /// source path. Glob expressions can be literal string compares. Or the
2327    /// following special syntax is recognized:
2328    ///
2329    /// * `?` matches any single character.
2330    /// * `*` matches any (possibly empty) sequence of characters.
2331    /// * `**` matches the current directory and arbitrary subdirectories. This sequence
2332    ///   must form a single path component, so both **a and b** are invalid and will
2333    ///   result in an error. A sequence of more than two consecutive * characters is
2334    ///   also invalid.
2335    /// * `[...]` matches any character inside the brackets. Character sequences can also
2336    ///   specify ranges of characters, as ordered by Unicode, so e.g. [0-9] specifies any
2337    ///   character between 0 and 9 inclusive. An unclosed bracket is invalid.
2338    /// * `[!...]` is the negation of `[...]`, i.e. it matches any characters not in the
2339    ///   brackets.
2340    /// * The metacharacters `?`, `*`, `[`, `]` can be matched by using brackets (e.g.
2341    ///   `[?]`). When a `]` occurs immediately following `[` or `[!` then it is
2342    ///   interpreted as being part of, rather then ending, the character set, so `]` and
2343    ///   `NOT ]` can be matched by `[]]` and `[!]]` respectively. The `-` character can
2344    ///   be specified inside a character sequence pattern by placing it at the start or
2345    ///   the end, e.g. `[abc-]`.
2346    ///
2347    /// Currently, --exclude only applies to the relative path of nested bundles within
2348    /// the main bundle to sign. e.g. if you sign `MyApp.app` and it has a
2349    /// `Contents/Frameworks/MyFramework.framework` that you wish to exclude, you would
2350    /// `--exclude Contents/Frameworks/MyFramework.framework` or even
2351    /// `--exclude Contents/Frameworks/**` to exclude the entire directory tree.
2352    ///
2353    /// Exclusions will still be copied and parents that need to reference exclude
2354    /// entities will continue to do so. If you wish to make a file or directory
2355    /// disappear, create a new directory without the file(s) and sign that.
2356    ///
2357    /// To exclude all nested bundles from being signed and only sign the main bundle
2358    /// (the default behavior of ``codesign`` without ``--deep``), use `--exclude '**'`.
2359    #[command(verbatim_doc_comment)]
2360    Sign(Sign),
2361
2362    /// Generate a new private key on a smartcard
2363    SmartcardGenerateKey(SmartcardGenerateKey),
2364
2365    /// Import a code signing certificate and key into a smartcard
2366    SmartcardImport(SmartcardImport),
2367
2368    /// Show information about available smartcard (SC) devices
2369    SmartcardScan(SmartcardScan),
2370
2371    /// Staples a notarization ticket to an entity
2372    Staple(Staple),
2373
2374    /// Verifies code signature data
2375    Verify(Verify),
2376
2377    /// Export CA certificates from the Windows Store
2378    WindowsStoreExportCertificateChain(WindowsStoreExportCertificateChain),
2379
2380    /// Print information about certificates in the Windows Store
2381    WindowsStorePrintCertificates(WindowsStorePrintCertificates),
2382
2383    /// Print information about X.509 OIDs related to Apple code signing
2384    X509Oids(X509Oids),
2385}
2386
2387impl Subcommands {
2388    fn as_cli_command(&self) -> &dyn CliCommand {
2389        match self {
2390            Subcommands::AnalyzeCertificate(c) => c,
2391            Subcommands::ComputeCodeHashes(c) => c,
2392            Subcommands::DebugCreateCodeRequirements(c) => c,
2393            Subcommands::DebugCreateConstraints(c) => c,
2394            Subcommands::DebugCreateEntitlements(c) => c,
2395            Subcommands::DebugCreateInfoPlist(c) => c,
2396            Subcommands::DebugCreateMacho(c) => c,
2397            Subcommands::DebugFileTree(c) => c,
2398            Subcommands::DiffSignatures(c) => c,
2399            #[cfg(feature = "notarize")]
2400            Subcommands::EncodeAppStoreConnectApiKey(c) => c,
2401            Subcommands::Extract(c) => c,
2402            Subcommands::GenerateCertificateSigningRequest(c) => c,
2403            Subcommands::GenerateSelfSignedCertificate(c) => c,
2404            Subcommands::KeychainExportCertificateChain(c) => c,
2405            Subcommands::KeychainPrintCertificates(c) => c,
2406            Subcommands::MachoUniversalCreate(c) => c,
2407            #[cfg(feature = "notarize")]
2408            Subcommands::NotaryLog(c) => c,
2409            #[cfg(feature = "notarize")]
2410            Subcommands::NotaryList(c) => c,
2411            #[cfg(feature = "notarize")]
2412            Subcommands::NotarySubmit(c) => c,
2413            #[cfg(feature = "notarize")]
2414            Subcommands::NotaryWait(c) => c,
2415            Subcommands::ParseCodeSigningRequirement(c) => c,
2416            Subcommands::PrintSignatureInfo(c) => c,
2417            Subcommands::RemoteSign(c) => c,
2418            Subcommands::Sign(c) => c,
2419            Subcommands::SmartcardGenerateKey(c) => c,
2420            Subcommands::SmartcardImport(c) => c,
2421            Subcommands::SmartcardScan(c) => c,
2422            Subcommands::Staple(c) => c,
2423            Subcommands::Verify(c) => c,
2424            Subcommands::WindowsStoreExportCertificateChain(c) => c,
2425            Subcommands::WindowsStorePrintCertificates(c) => c,
2426            Subcommands::X509Oids(c) => c,
2427        }
2428    }
2429}
2430
2431/// Sign and notarize Apple programs. See https://gregoryszorc.com/docs/apple-codesign/main/ for more docs
2432#[derive(Parser)]
2433#[command(author, version, arg_required_else_help = true)]
2434struct Cli {
2435    /// Explicit configuration file to load.
2436    ///
2437    /// If provided, the default configuration files are not loaded, even
2438    /// if they exist.
2439    ///
2440    /// Can be specified multiple times. Files are loaded/merged in the order
2441    /// given.
2442    ///
2443    /// The special value `/dev/null` can be used to specify an empty/null
2444    /// config file. It can be used to short-circuit loading of default config
2445    /// files.
2446    #[arg(short = 'C', long = "config-file", global = true)]
2447    config_path: Vec<PathBuf>,
2448
2449    /// Configuration profile to load.
2450    ///
2451    /// If not specified, the implicit "default" profile is loaded.
2452    #[arg(short = 'P', long, global = true)]
2453    profile: Option<String>,
2454
2455    /// Increase logging verbosity. Can be specified multiple times
2456    #[arg(short = 'v', long, global = true, action = ArgAction::Count)]
2457    verbose: u8,
2458
2459    #[command(subcommand)]
2460    command: Subcommands,
2461}
2462
2463impl Cli {
2464    pub fn config_builder(&self) -> ConfigBuilder {
2465        let mut config = ConfigBuilder::default();
2466
2467        config = if self.config_path.is_empty() {
2468            config.with_user_config_file().with_cwd_config_file()
2469        } else {
2470            for path in &self.config_path {
2471                if path.display().to_string() == "/dev/null" {
2472                    break;
2473                }
2474
2475                config = config.toml_file(path);
2476            }
2477
2478            config
2479        };
2480
2481        if let Some(profile) = &self.profile {
2482            config = config.profile(profile.to_string());
2483        }
2484
2485        // Environment variables override everything.
2486        config = config.with_env_prefix();
2487
2488        config
2489    }
2490}
2491
2492pub fn main_impl() -> Result<(), AppleCodesignError> {
2493    let cli = Cli::parse();
2494
2495    let log_level = match cli.verbose {
2496        0 => LevelFilter::Warn,
2497        1 => LevelFilter::Info,
2498        2 => LevelFilter::Debug,
2499        _ => LevelFilter::Trace,
2500    };
2501
2502    let mut builder = env_logger::Builder::new();
2503
2504    builder
2505        .filter_level(log_level)
2506        .parse_default_env();
2507
2508    // Disable log context except at higher log levels.
2509    if log_level <= LevelFilter::Info {
2510        builder
2511            .format_timestamp(None)
2512            .format_level(false)
2513            .format_target(false);
2514    }
2515
2516    // This spews unwanted output at default level. Nerf it by default.
2517    if log_level == LevelFilter::Info {
2518        builder.filter_module("rustls", LevelFilter::Error);
2519    }
2520
2521    builder.init();
2522
2523    let mut config_builder = cli.config_builder();
2524
2525    let command = cli.command.as_cli_command();
2526
2527    if let Some(config) = command.as_config()? {
2528        config_builder = config_builder.with_config_struct(config);
2529    }
2530
2531    let config = config_builder.config()?;
2532
2533    let context = Context { config };
2534
2535    command.run(&context)
2536}
2537
2538#[cfg(test)]
2539mod test {
2540    use super::*;
2541    use clap::CommandFactory;
2542
2543    #[test]
2544    fn verify_cli() {
2545        Cli::command().debug_assert();
2546    }
2547}