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