1pub 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
71pub struct Context {
73 pub config: Config,
74}
75
76pub trait CliCommand {
77 fn as_config(&self) -> Result<Option<Config>, AppleCodesignError> {
79 Ok(None)
80 }
81
82 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 #[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 #[arg(long, requires = "api_key")]
130 api_issuer: Option<String>,
131
132 #[arg(long, requires = "api_issuer")]
133 api_key: Option<String>,
135}
136
137#[cfg(feature = "notarize")]
138impl NotaryApi {
139 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 #[arg(long, value_parser = ["default", "always", "never", "cached"], default_value = "default")]
155 touch_policy: String,
156
157 #[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: PathBuf,
337
338 #[arg(long, default_value_t = DigestType::Sha256)]
340 hash: DigestType,
341
342 #[arg(long, default_value = "4096")]
344 page_size: usize,
345
346 #[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 path0: PathBuf,
371
372 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 #[arg(short = 'o', long)]
419 output_path: Option<PathBuf>,
420
421 issuer_id: String,
423
424 key_id: String,
426
427 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 #[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 #[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 #[arg(long, default_value = "unset")]
507 team_id: String,
508
509 #[arg(long)]
511 person_name: String,
512
513 #[arg(long, default_value = "XX")]
515 country_name: String,
516
517 #[arg(long, default_value = "365")]
519 validity_days: i64,
520
521 #[arg(long)]
523 pem_filename: Option<String>,
524
525 #[arg(
527 long = "pem-unified-file",
528 alias = "pem-unified-filename",
529 value_name = "PATH"
530 )]
531 pem_unified_path: Option<PathBuf>,
532
533 #[arg(long = "p12-file", alias = "pfx-file", value_name = "PATH")]
535 p12_path: Option<PathBuf>,
536
537 #[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 #[arg(long, value_parser = KEYCHAIN_DOMAINS, default_value = "user")]
640 domain: String,
641
642 #[arg(long, group = "unlock-password")]
644 password: Option<String>,
645
646 #[arg(long = "password-file", group = "unlock-password")]
648 password_path: Option<PathBuf>,
649
650 #[arg(long)]
652 no_print_self: bool,
653
654 #[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 #[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: Vec<PathBuf>,
737
738 #[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 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 #[arg(long)]
826 wait: bool,
827
828 #[arg(long, default_value = "600")]
830 max_wait_seconds: u64,
831
832 #[arg(long)]
834 staple: bool,
835
836 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 #[arg(long, default_value = "600")]
880 max_wait_seconds: u64,
881
882 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 #[arg(long, value_parser = ["csrl", "expression-tree"], default_value = "csrl")]
905 format: String,
906
907 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 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 #[arg(long = "editor")]
955 session_join_string_editor: bool,
956
957 #[arg(long = "sjs-file", alias = "sjs-path")]
959 session_join_string_path: Option<PathBuf>,
960
961 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 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#[derive(Args, Clone, Debug, Eq, PartialEq)]
1065pub struct ScopedSigningArgs {
1066 #[arg(long = "binary-identifier", value_name = "IDENTIFIER")]
1068 binary_identifiers: Vec<String>,
1069
1070 #[arg(
1072 long = "code-requirements-file",
1073 alias = "code-requirements-path",
1074 value_name = "PATH"
1075 )]
1076 code_requirements_paths: Vec<String>,
1077
1078 #[arg(
1080 long = "code-resources-file",
1081 alias = "code-resources",
1082 value_name = "PATH"
1083 )]
1084 code_resources_paths: Vec<String>,
1085
1086 #[arg(long)]
1090 code_signature_flags: Vec<String>,
1091
1092 #[arg(long = "digest", value_name = "DIGEST")]
1115 digests: Vec<String>,
1116
1117 #[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 #[arg(long = "launch-constraints-self-file", value_name = "PATH")]
1130 launch_constraints_self_paths: Vec<String>,
1131
1132 #[arg(long = "launch-constraints-parent-file", value_name = "PATH")]
1136 launch_constraints_parent_paths: Vec<String>,
1137
1138 #[arg(long = "launch-constraints-responsible-file", value_name = "PATH")]
1142 launch_constraints_responsible_paths: Vec<String>,
1143
1144 #[arg(long = "library-constraints-file", value_name = "PATH")]
1148 library_constraints_paths: Vec<String>,
1149
1150 #[arg(long = "runtime-version", value_name = "VERSION")]
1152 runtime_versions: Vec<String>,
1153
1154 #[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#[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
1205pub 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 !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 #[arg(long, value_name = "NAME")]
1431 team_name: Option<String>,
1432
1433 #[arg(long)]
1444 signing_time: Option<String>,
1445
1446 #[arg(long, default_value = APPLE_TIMESTAMP_URL)]
1451 timestamp_url: String,
1452
1453 #[arg(long)]
1455 exclude: Vec<String>,
1456
1457 #[arg(long)]
1478 shallow: bool,
1479
1480 #[arg(long)]
1499 for_notarization: bool,
1500
1501 input_path: PathBuf,
1503
1504 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 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 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 #[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 #[arg(long)]
1673 existing_key: bool,
1674
1675 #[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: 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: 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 #[arg(long, value_parser = WINDOWS_STORE_NAMES, default_value = "user", value_name = "STORE")]
1829 windows_store_name: String,
1830
1831 #[arg(long)]
1833 no_print_self: bool,
1834
1835 #[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 #[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 AnalyzeCertificate(AnalyzeCertificate),
1940
1941 ComputeCodeHashes(ComputeCodeHashes),
1943
1944 #[command(hide = true)]
1946 DebugCreateCodeRequirements(debug_commands::DebugCreateCodeRequirements),
1947
1948 #[command(hide = true)]
1950 DebugCreateConstraints(debug_commands::DebugCreateConstraints),
1951
1952 #[command(hide = true)]
1954 DebugCreateEntitlements(debug_commands::DebugCreateEntitlements),
1955
1956 #[command(hide = true)]
1958 DebugCreateInfoPlist(debug_commands::DebugCreateInfoPlist),
1959
1960 #[command(hide = true)]
1962 DebugCreateMacho(debug_commands::DebugCreateMachO),
1963
1964 #[command(hide = true)]
1966 DebugFileTree(debug_commands::DebugFileTree),
1967
1968 DiffSignatures(DiffSignatures),
1970
1971 #[cfg(feature = "notarize")]
2002 #[command(verbatim_doc_comment)]
2003 EncodeAppStoreConnectApiKey(EncodeAppStoreConnectApiKey),
2004
2005 #[command(override_usage = "rcodesign extract [OPTIONS] <COMMAND> <INPUT_PATH>")]
2010 Extract(extract_commands::Extract),
2011
2012 GenerateCertificateSigningRequest(GenerateCertificateSigningRequest),
2014
2015 GenerateSelfSignedCertificate(GenerateSelfSignedCertificate),
2037
2038 KeychainExportCertificateChain(KeychainExportCertificateChain),
2040
2041 KeychainPrintCertificates(KeychainPrintCertificates),
2043
2044 MachoUniversalCreate(MachoUniversalCreate),
2050
2051 #[cfg(feature = "notarize")]
2052 NotaryList(NotaryList),
2054
2055 #[cfg(feature = "notarize")]
2056 NotaryLog(NotaryLog),
2058
2059 #[cfg(feature = "notarize")]
2106 #[command(alias = "notarize")]
2107 NotarySubmit(NotarySubmit),
2108
2109 #[cfg(feature = "notarize")]
2111 NotaryWait(NotaryWait),
2112
2113 ParseCodeSigningRequirement(ParseCodeSigningRequirement),
2131
2132 PrintSignatureInfo(PrintSignatureInfo),
2134
2135 RemoteSign(RemoteSign),
2137
2138 #[command(verbatim_doc_comment)]
2360 Sign(Sign),
2361
2362 SmartcardGenerateKey(SmartcardGenerateKey),
2364
2365 SmartcardImport(SmartcardImport),
2367
2368 SmartcardScan(SmartcardScan),
2370
2371 Staple(Staple),
2373
2374 Verify(Verify),
2376
2377 WindowsStoreExportCertificateChain(WindowsStoreExportCertificateChain),
2379
2380 WindowsStorePrintCertificates(WindowsStorePrintCertificates),
2382
2383 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#[derive(Parser)]
2433#[command(author, version, arg_required_else_help = true)]
2434struct Cli {
2435 #[arg(short = 'C', long = "config-file", global = true)]
2447 config_path: Vec<PathBuf>,
2448
2449 #[arg(short = 'P', long, global = true)]
2453 profile: Option<String>,
2454
2455 #[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 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 if log_level <= LevelFilter::Info {
2510 builder
2511 .format_timestamp(None)
2512 .format_level(false)
2513 .format_target(false);
2514 }
2515
2516 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}