Skip to main content

isideload_apple_codesign/
signing_settings.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
5//! Code signing settings.
6
7use {
8    crate::{
9        certificate::{AppleCertificate, CodeSigningCertificateExtension},
10        code_directory::CodeSignatureFlags,
11        code_requirement::CodeRequirementExpression,
12        cryptography::DigestType,
13        embedded_signature::{Blob, RequirementBlob},
14        environment_constraints::EncodedEnvironmentConstraints,
15        error::AppleCodesignError,
16        macho::{MachFile, parse_version_nibbles},
17    },
18    glob::Pattern,
19    goblin::mach::cputype::{
20        CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_ARM64_32, CPU_TYPE_X86_64, CpuType,
21    },
22    log::{error, info},
23    reqwest::{IntoUrl, Url},
24    std::{
25        collections::{BTreeMap, BTreeSet},
26        fmt::Formatter,
27    },
28    x509_certificate::{CapturedX509Certificate, KeyInfoSigner},
29};
30
31/// Denotes the scope for a setting.
32///
33/// Settings have an associated scope defined by this type. This allows settings
34/// to apply to exactly what you want them to apply to.
35///
36/// Scopes can be converted from a string representation. The following syntax is
37/// recognized:
38///
39/// * `@main` - Maps to [SettingsScope::Main]
40/// * `@<int>` - e.g. `@0`. Maps to [SettingsScope::MultiArchIndex].Index
41/// * `@[cpu_type=<int>]` - e.g. `@[cpu_type=7]`. Maps to [SettingsScope::MultiArchCpuType].
42/// * `@[cpu_type=<string>]` - e.g. `@[cpu_type=x86_64]`. Maps to [SettingsScope::MultiArchCpuType]
43///    for recognized string values (see below).
44/// * `<string>` - e.g. `path/to/file`. Maps to [SettingsScope::Path].
45/// * `<string>@<int>` - e.g. `path/to/file@0`. Maps to [SettingsScope::PathMultiArchIndex].
46/// * `<string>@[cpu_type=<int>]` - e.g. `path/to/file@[cpu_type=7]`. Maps to
47///   [SettingsScope::PathMultiArchCpuType].
48/// * `<string>@[cpu_type=<string>]` - e.g. `path/to/file@[cpu_type=arm64]`. Maps to
49///   [SettingsScope::PathMultiArchCpuType] for recognized string values (see below).
50///
51/// # Recognized cpu_type String Values
52///
53/// The following `cpu_type=` string values are recognized:
54///
55/// * `arm` -> [CPU_TYPE_ARM]
56/// * `arm64` -> [CPU_TYPE_ARM64]
57/// * `arm64_32` -> [CPU_TYPE_ARM64_32]
58/// * `x86_64` -> [CPU_TYPE_X86_64]
59#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
60pub enum SettingsScope {
61    // The order of the variants is important. Instance cloning iterates keys in
62    // sorted order and last write wins. So the order here should be from widest to
63    // most granular.
64    /// The main entity being signed.
65    ///
66    /// Can be a Mach-O file, a bundle, or any other primitive this crate
67    /// supports signing.
68    ///
69    /// When signing a bundle or any primitive with nested elements (such as a
70    /// fat/universal Mach-O binary), settings can propagate to nested elements.
71    Main,
72
73    /// Filesystem path.
74    ///
75    /// Can refer to a Mach-O file, a nested bundle, or any other filesystem
76    /// based primitive that can be traversed into when performing nested signing.
77    ///
78    /// The string value refers to the filesystem relative path of the entity
79    /// relative to the main entity being signed.
80    Path(String),
81
82    /// A single Mach-O binary within a fat/universal Mach-O binary.
83    ///
84    /// The binary to operate on is defined by its 0-based index within the
85    /// fat/universal Mach-O container.
86    MultiArchIndex(usize),
87
88    /// A single Mach-O binary within a fat/universal Mach-O binary.
89    ///
90    /// The binary to operate on is defined by its CPU architecture.
91    MultiArchCpuType(CpuType),
92
93    /// Combination of [SettingsScope::Path] and [SettingsScope::MultiArchIndex].
94    ///
95    /// This refers to a single Mach-O binary within a fat/universal binary at a
96    /// given relative path.
97    PathMultiArchIndex(String, usize),
98
99    /// Combination of [SettingsScope::Path] and [SettingsScope::MultiArchCpuType].
100    ///
101    /// This refers to a single Mach-O binary within a fat/universal binary at a
102    /// given relative path.
103    PathMultiArchCpuType(String, CpuType),
104}
105
106impl std::fmt::Display for SettingsScope {
107    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108        match self {
109            Self::Main => f.write_str("main signing target"),
110            Self::Path(path) => f.write_fmt(format_args!("path {path}")),
111            Self::MultiArchIndex(index) => f.write_fmt(format_args!(
112                "fat/universal Mach-O binaries at index {index}"
113            )),
114            Self::MultiArchCpuType(cpu_type) => f.write_fmt(format_args!(
115                "fat/universal Mach-O binaries for CPU {cpu_type}"
116            )),
117            Self::PathMultiArchIndex(path, index) => f.write_fmt(format_args!(
118                "fat/universal Mach-O binaries at index {index} under path {path}"
119            )),
120            Self::PathMultiArchCpuType(path, cpu_type) => f.write_fmt(format_args!(
121                "fat/universal Mach-O binaries for CPU {cpu_type} under path {path}"
122            )),
123        }
124    }
125}
126
127impl SettingsScope {
128    fn parse_at_expr(
129        at_expr: &str,
130    ) -> Result<(Option<usize>, Option<CpuType>), AppleCodesignError> {
131        match at_expr.parse::<usize>() {
132            Ok(index) => Ok((Some(index), None)),
133            Err(_) => {
134                if at_expr.starts_with('[') && at_expr.ends_with(']') {
135                    let v = &at_expr[1..at_expr.len() - 1];
136                    let parts = v.split('=').collect::<Vec<_>>();
137
138                    if parts.len() == 2 {
139                        let (key, value) = (parts[0], parts[1]);
140
141                        if key != "cpu_type" {
142                            return Err(AppleCodesignError::ParseSettingsScope(format!(
143                                "in '@{at_expr}', {key} not recognized; must be cpu_type"
144                            )));
145                        }
146
147                        if let Some(cpu_type) = match value {
148                            "arm" => Some(CPU_TYPE_ARM),
149                            "arm64" => Some(CPU_TYPE_ARM64),
150                            "arm64_32" => Some(CPU_TYPE_ARM64_32),
151                            "x86_64" => Some(CPU_TYPE_X86_64),
152                            _ => None,
153                        } {
154                            return Ok((None, Some(cpu_type)));
155                        }
156
157                        match value.parse::<u32>() {
158                            Ok(cpu_type) => Ok((None, Some(cpu_type as CpuType))),
159                            Err(_) => Err(AppleCodesignError::ParseSettingsScope(format!(
160                                "in '@{at_expr}', cpu_arch value {value} not recognized"
161                            ))),
162                        }
163                    } else {
164                        Err(AppleCodesignError::ParseSettingsScope(format!(
165                            "'{v}' sub-expression isn't of form <key>=<value>"
166                        )))
167                    }
168                } else {
169                    Err(AppleCodesignError::ParseSettingsScope(format!(
170                        "in '{at_expr}', @ expression not recognized"
171                    )))
172                }
173            }
174        }
175    }
176}
177
178impl AsRef<SettingsScope> for SettingsScope {
179    fn as_ref(&self) -> &SettingsScope {
180        self
181    }
182}
183
184impl TryFrom<&str> for SettingsScope {
185    type Error = AppleCodesignError;
186
187    fn try_from(s: &str) -> Result<Self, Self::Error> {
188        if s == "@main" {
189            Ok(Self::Main)
190        } else if let Some(at_expr) = s.strip_prefix('@') {
191            match Self::parse_at_expr(at_expr)? {
192                (Some(index), None) => Ok(Self::MultiArchIndex(index)),
193                (None, Some(cpu_type)) => Ok(Self::MultiArchCpuType(cpu_type)),
194                _ => panic!("this shouldn't happen"),
195            }
196        } else {
197            // Looks like a path.
198            let parts = s.rsplitn(2, '@').collect::<Vec<_>>();
199
200            match parts.len() {
201                1 => Ok(Self::Path(s.to_string())),
202                2 => {
203                    // Parts are reversed since splitting at end.
204                    let (at_expr, path) = (parts[0], parts[1]);
205
206                    match Self::parse_at_expr(at_expr)? {
207                        (Some(index), None) => {
208                            Ok(Self::PathMultiArchIndex(path.to_string(), index))
209                        }
210                        (None, Some(cpu_type)) => {
211                            Ok(Self::PathMultiArchCpuType(path.to_string(), cpu_type))
212                        }
213                        _ => panic!("this shouldn't happen"),
214                    }
215                }
216                _ => panic!("this shouldn't happen"),
217            }
218        }
219    }
220}
221
222/// Describes how to derive designated requirements during signing.
223#[derive(Clone, Debug)]
224pub enum DesignatedRequirementMode {
225    /// Automatically attempt to derive an appropriate expression given the
226    /// code signing certificate and entity being signed.
227    Auto,
228
229    /// Provide an explicit designated requirement.
230    Explicit(Vec<Vec<u8>>),
231}
232
233/// Describes the type of a scoped setting.
234#[derive(Clone, Copy, Debug, Eq, PartialEq)]
235pub enum ScopedSetting {
236    Digest,
237    BinaryIdentifier,
238    Entitlements,
239    DesignatedRequirements,
240    CodeSignatureFlags,
241    RuntimeVersion,
242    InfoPlist,
243    CodeResources,
244    ExtraDigests,
245    LaunchConstraintsSelf,
246    LaunchConstraintsParent,
247    LaunchConstraintsResponsible,
248    LibraryConstraints,
249}
250
251impl ScopedSetting {
252    pub fn all() -> &'static [Self] {
253        &[
254            Self::Digest,
255            Self::BinaryIdentifier,
256            Self::Entitlements,
257            Self::DesignatedRequirements,
258            Self::CodeSignatureFlags,
259            Self::RuntimeVersion,
260            Self::InfoPlist,
261            Self::CodeResources,
262            Self::ExtraDigests,
263            Self::LaunchConstraintsSelf,
264            Self::LaunchConstraintsParent,
265            Self::LaunchConstraintsResponsible,
266            Self::LibraryConstraints,
267        ]
268    }
269
270    pub fn inherit_nested_bundle() -> &'static [Self] {
271        &[Self::Digest, Self::ExtraDigests, Self::RuntimeVersion]
272    }
273
274    pub fn inherit_nested_macho() -> &'static [Self] {
275        &[Self::Digest, Self::ExtraDigests, Self::RuntimeVersion]
276    }
277}
278
279/// Represents code signing settings.
280///
281/// This type holds settings related to a single logical signing operation.
282/// Some settings (such as the signing key-pair are global). Other settings
283/// (such as the entitlements or designated requirement) can be applied on a
284/// more granular, scoped basis. The scoping of these lower-level settings is
285/// controlled via [SettingsScope]. If a setting is specified with a scope, it
286/// only applies to that scope. See that type's documentation for more.
287///
288/// An instance of this type is bound to a signing operation. When the
289/// signing operation traverses into nested primitives (e.g. when traversing
290/// into the individual Mach-O binaries in a fat/universal binary or when
291/// traversing into nested bundles or non-main binaries within a bundle), a
292/// new instance of this type is transparently constructed by merging global
293/// settings with settings for the target scope. This allows granular control
294/// over which signing settings apply to which entity and enables a signing
295/// operation over a complex primitive to be configured/performed via a single
296/// [SigningSettings] and signing operation.
297#[derive(Clone, Default)]
298pub struct SigningSettings<'key> {
299    // Global settings.
300    signing_key: Option<(&'key dyn KeyInfoSigner, CapturedX509Certificate)>,
301    certificates: Vec<CapturedX509Certificate>,
302    time_stamp_url: Option<Url>,
303    signing_time: Option<chrono::DateTime<chrono::Utc>>,
304    path_exclusion_patterns: Vec<Pattern>,
305    shallow: bool,
306    for_notarization: bool,
307
308    // Scope-specific settings.
309    // These are BTreeMap so when we filter the keys, keys with higher precedence come
310    // last and last write wins.
311    digest_type: BTreeMap<SettingsScope, DigestType>,
312    team_id: BTreeMap<SettingsScope, String>,
313    identifiers: BTreeMap<SettingsScope, String>,
314    entitlements: BTreeMap<SettingsScope, plist::Value>,
315    designated_requirement: BTreeMap<SettingsScope, DesignatedRequirementMode>,
316    code_signature_flags: BTreeMap<SettingsScope, CodeSignatureFlags>,
317    runtime_version: BTreeMap<SettingsScope, semver::Version>,
318    info_plist_data: BTreeMap<SettingsScope, Vec<u8>>,
319    code_resources_data: BTreeMap<SettingsScope, Vec<u8>>,
320    extra_digests: BTreeMap<SettingsScope, BTreeSet<DigestType>>,
321    launch_constraints_self: BTreeMap<SettingsScope, EncodedEnvironmentConstraints>,
322    launch_constraints_parent: BTreeMap<SettingsScope, EncodedEnvironmentConstraints>,
323    launch_constraints_responsible: BTreeMap<SettingsScope, EncodedEnvironmentConstraints>,
324    library_constraints: BTreeMap<SettingsScope, EncodedEnvironmentConstraints>,
325}
326
327impl<'key> SigningSettings<'key> {
328    /// Obtain the signing key to use.
329    pub fn signing_key(&self) -> Option<(&'key dyn KeyInfoSigner, &CapturedX509Certificate)> {
330        self.signing_key.as_ref().map(|(key, cert)| (*key, cert))
331    }
332
333    /// Set the signing key-pair for producing a cryptographic signature over code.
334    ///
335    /// If this is not called, signing will lack a cryptographic signature and will only
336    /// contain digests of content. This is known as "ad-hoc" mode. Binaries lacking a
337    /// cryptographic signature or signed without a key-pair issued/signed by Apple may
338    /// not run in all environments.
339    pub fn set_signing_key(
340        &mut self,
341        private: &'key dyn KeyInfoSigner,
342        public: CapturedX509Certificate,
343    ) {
344        self.signing_key = Some((private, public));
345    }
346
347    /// Obtain the certificate chain.
348    pub fn certificate_chain(&self) -> &[CapturedX509Certificate] {
349        &self.certificates
350    }
351
352    /// Attempt to chain Apple CA certificates from a loaded Apple signed signing key.
353    ///
354    /// If you are calling `set_signing_key()`, you probably want to call this immediately
355    /// afterwards, as it will automatically register Apple CA certificates if you are
356    /// using an Apple signed code signing certificate.
357    pub fn chain_apple_certificates(&mut self) -> Option<Vec<CapturedX509Certificate>> {
358        if let Some((_, cert)) = &self.signing_key {
359            if let Some(chain) = cert.apple_root_certificate_chain() {
360                // The chain starts with self.
361                let chain = chain.into_iter().skip(1).collect::<Vec<_>>();
362                self.certificates.extend(chain.clone());
363                Some(chain)
364            } else {
365                None
366            }
367        } else {
368            None
369        }
370    }
371
372    /// Whether the signing certificate is signed by Apple.
373    pub fn signing_certificate_apple_signed(&self) -> bool {
374        if let Some((_, cert)) = &self.signing_key {
375            cert.chains_to_apple_root_ca()
376        } else {
377            false
378        }
379    }
380
381    /// Add a parsed certificate to the signing certificate chain.
382    ///
383    /// When producing a cryptographic signature (see [SigningSettings::set_signing_key]),
384    /// information about the signing key-pair is included in the signature. The signing
385    /// key's public certificate is always included. This function can be used to define
386    /// additional X.509 public certificates to include. Typically, the signing chain
387    /// of the signing key-pair up until the root Certificate Authority (CA) is added
388    /// so clients have access to the full certificate chain for validation purposes.
389    ///
390    /// This setting has no effect if [SigningSettings::set_signing_key] is not called.
391    pub fn chain_certificate(&mut self, cert: CapturedX509Certificate) {
392        self.certificates.push(cert);
393    }
394
395    /// Add a DER encoded X.509 public certificate to the signing certificate chain.
396    ///
397    /// This is like [Self::chain_certificate] except the certificate data is provided in
398    /// its binary, DER encoded form.
399    pub fn chain_certificate_der(
400        &mut self,
401        data: impl AsRef<[u8]>,
402    ) -> Result<(), AppleCodesignError> {
403        self.chain_certificate(CapturedX509Certificate::from_der(data.as_ref())?);
404
405        Ok(())
406    }
407
408    /// Add a PEM encoded X.509 public certificate to the signing certificate chain.
409    ///
410    /// This is like [Self::chain_certificate] except the certificate is
411    /// specified as PEM encoded data. This is a human readable string like
412    /// `-----BEGIN CERTIFICATE-----` and is a common method for encoding certificate data.
413    /// (PEM is effectively base64 encoded DER data.)
414    ///
415    /// Only a single certificate is read from the PEM data.
416    pub fn chain_certificate_pem(
417        &mut self,
418        data: impl AsRef<[u8]>,
419    ) -> Result<(), AppleCodesignError> {
420        self.chain_certificate(CapturedX509Certificate::from_pem(data.as_ref())?);
421
422        Ok(())
423    }
424
425    /// Obtain the Time-Stamp Protocol server URL.
426    pub fn time_stamp_url(&self) -> Option<&Url> {
427        self.time_stamp_url.as_ref()
428    }
429
430    /// Set the Time-Stamp Protocol server URL to use to generate a Time-Stamp Token.
431    ///
432    /// When set and a signing key-pair is defined, the server will be contacted during
433    /// signing and a Time-Stamp Token will be embedded in the cryptographic signature.
434    /// This Time-Stamp Token is a cryptographic proof that someone in possession of
435    /// the signing key-pair produced the cryptographic signature at a given time. It
436    /// facilitates validation of the signing time via an independent (presumably trusted)
437    /// entity.
438    pub fn set_time_stamp_url(&mut self, url: impl IntoUrl) -> Result<(), AppleCodesignError> {
439        self.time_stamp_url = Some(url.into_url()?);
440
441        Ok(())
442    }
443
444    /// Obtain the signing time to embed in signatures.
445    ///
446    /// If None, the current time at the time of signing is used.
447    pub fn signing_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
448        self.signing_time
449    }
450
451    /// Set the signing time to embed in signatures.
452    ///
453    /// If not called, the current time at time of signing will be used.
454    pub fn set_signing_time(&mut self, time: chrono::DateTime<chrono::Utc>) {
455        self.signing_time = Some(time);
456    }
457
458    /// Obtain the team identifier for signed binaries.
459    pub fn team_id(&self) -> Option<&str> {
460        self.team_id.get(&SettingsScope::Main).map(|x| x.as_str())
461    }
462
463    /// Set the team identifier for signed binaries.
464    pub fn set_team_id(&mut self, value: impl ToString) {
465        self.team_id.insert(SettingsScope::Main, value.to_string());
466    }
467
468    /// Attempt to set the team ID from the signing certificate.
469    ///
470    /// Apple signing certificates have the team ID embedded within the certificate.
471    /// By calling this method, the team ID embedded within the certificate will
472    /// be propagated to the code signature.
473    ///
474    /// Callers will typically want to call this after registering the signing
475    /// certificate with [Self::set_signing_key()] but before specifying an explicit
476    /// team ID via [Self::set_team_id()].
477    ///
478    /// Calling this will replace a registered team IDs if the signing
479    /// certificate contains a team ID. If no signing certificate is registered or
480    /// it doesn't contain a team ID, no changes will be made.
481    ///
482    /// Returns `Some` if a team ID was set from the signing certificate or `None`
483    /// otherwise.
484    pub fn set_team_id_from_signing_certificate(&mut self) -> Option<&str> {
485        // The team ID is only included for Apple signed certificates.
486        if !self.signing_certificate_apple_signed() {
487            None
488        } else if let Some((_, cert)) = &self.signing_key {
489            if let Some(team_id) = cert.apple_team_id() {
490                self.set_team_id(team_id);
491                Some(
492                    self.team_id
493                        .get(&SettingsScope::Main)
494                        .expect("we just set a team id"),
495                )
496            } else {
497                None
498            }
499        } else {
500            None
501        }
502    }
503
504    /// Whether a given path matches a path exclusion pattern.
505    pub fn path_exclusion_pattern_matches(&self, path: &str) -> bool {
506        self.path_exclusion_patterns
507            .iter()
508            .any(|pattern| pattern.matches(path))
509    }
510
511    /// Add a path to the exclusions list.
512    pub fn add_path_exclusion(&mut self, v: &str) -> Result<(), AppleCodesignError> {
513        self.path_exclusion_patterns.push(Pattern::new(v)?);
514        Ok(())
515    }
516
517    /// Whether to perform a shallow, non-nested signing operation.
518    ///
519    /// Can mean different things to different entities. For bundle signing, shallow
520    /// mode means not to recurse into nested bundles.
521    pub fn shallow(&self) -> bool {
522        self.shallow
523    }
524
525    /// Set whether to perform a shallow signing operation.
526    pub fn set_shallow(&mut self, v: bool) {
527        self.shallow = v;
528    }
529
530    /// Whether the signed asset will later be notarized.
531    ///
532    /// This serves as a hint to engage additional signing settings that are required
533    /// for an asset to be successfully notarized by Apple.
534    pub fn for_notarization(&self) -> bool {
535        self.for_notarization
536    }
537
538    /// Set whether to engage notarization compatibility mode.
539    pub fn set_for_notarization(&mut self, v: bool) {
540        self.for_notarization = v;
541    }
542
543    /// Obtain the primary digest type to use.
544    pub fn digest_type(&self, scope: impl AsRef<SettingsScope>) -> DigestType {
545        self.digest_type
546            .get(scope.as_ref())
547            .copied()
548            .unwrap_or_default()
549    }
550
551    /// Set the content digest to use.
552    ///
553    /// The default is SHA-256. Changing this to SHA-1 can weaken security of digital
554    /// signatures and may prevent the binary from running in environments that enforce
555    /// more modern signatures.
556    pub fn set_digest_type(&mut self, scope: SettingsScope, digest_type: DigestType) {
557        self.digest_type.insert(scope, digest_type);
558    }
559
560    /// Obtain the binary identifier string for a given scope.
561    pub fn binary_identifier(&self, scope: impl AsRef<SettingsScope>) -> Option<&str> {
562        self.identifiers.get(scope.as_ref()).map(|s| s.as_str())
563    }
564
565    /// Set the binary identifier string for a binary at a path.
566    ///
567    /// This only has an effect when signing an individual Mach-O file (use the `None` path)
568    /// or the non-main executable in a bundle: when signing the main executable in a bundle,
569    /// the binary's identifier is retrieved from the mandatory `CFBundleIdentifier` value in
570    /// the bundle's `Info.plist` file.
571    ///
572    /// The binary identifier should be a DNS-like name and should uniquely identify the
573    /// binary. e.g. `com.example.my_program`
574    pub fn set_binary_identifier(&mut self, scope: SettingsScope, value: impl ToString) {
575        self.identifiers.insert(scope, value.to_string());
576    }
577
578    /// Obtain the entitlements plist as a [plist::Value].
579    ///
580    /// The value should be a [plist::Value::Dictionary] variant.
581    pub fn entitlements_plist(&self, scope: impl AsRef<SettingsScope>) -> Option<&plist::Value> {
582        self.entitlements.get(scope.as_ref())
583    }
584
585    /// Obtain the entitlements XML string for a given scope.
586    pub fn entitlements_xml(
587        &self,
588        scope: impl AsRef<SettingsScope>,
589    ) -> Result<Option<String>, AppleCodesignError> {
590        if let Some(value) = self.entitlements_plist(scope) {
591            let mut buffer = vec![];
592            let writer = std::io::Cursor::new(&mut buffer);
593            value
594                .to_writer_xml(writer)
595                .map_err(AppleCodesignError::PlistSerializeXml)?;
596
597            Ok(Some(
598                String::from_utf8(buffer).expect("plist XML serialization should produce UTF-8"),
599            ))
600        } else {
601            Ok(None)
602        }
603    }
604
605    /// Set the entitlements to sign via an XML string.
606    ///
607    /// The value should be an XML plist. The value is parsed and stored as
608    /// a native plist value.
609    pub fn set_entitlements_xml(
610        &mut self,
611        scope: SettingsScope,
612        value: impl ToString,
613    ) -> Result<(), AppleCodesignError> {
614        let cursor = std::io::Cursor::new(value.to_string().into_bytes());
615        let value =
616            plist::Value::from_reader_xml(cursor).map_err(AppleCodesignError::PlistParseXml)?;
617
618        self.entitlements.insert(scope, value);
619
620        Ok(())
621    }
622
623    /// Obtain the designated requirements for a given scope.
624    pub fn designated_requirement(
625        &self,
626        scope: impl AsRef<SettingsScope>,
627    ) -> &DesignatedRequirementMode {
628        self.designated_requirement
629            .get(scope.as_ref())
630            .unwrap_or(&DesignatedRequirementMode::Auto)
631    }
632
633    /// Set the designated requirement for a Mach-O binary given a [CodeRequirementExpression].
634    ///
635    /// The designated requirement (also known as "code requirements") specifies run-time
636    /// requirements for the binary. e.g. you can stipulate that the binary must be
637    /// signed by a certificate issued/signed/chained to Apple. The designated requirement
638    /// is embedded in Mach-O binaries and signed.
639    pub fn set_designated_requirement_expression(
640        &mut self,
641        scope: SettingsScope,
642        expr: &CodeRequirementExpression,
643    ) -> Result<(), AppleCodesignError> {
644        self.designated_requirement.insert(
645            scope,
646            DesignatedRequirementMode::Explicit(vec![expr.to_bytes()?]),
647        );
648
649        Ok(())
650    }
651
652    /// Set the designated requirement expression for a Mach-O binary given serialized bytes.
653    ///
654    /// This is like [SigningSettings::set_designated_requirement_expression] except the
655    /// designated requirement expression is given as serialized bytes. The bytes passed are
656    /// the value that would be produced by compiling a code requirement expression via
657    /// `csreq -b`.
658    pub fn set_designated_requirement_bytes(
659        &mut self,
660        scope: SettingsScope,
661        data: impl AsRef<[u8]>,
662    ) -> Result<(), AppleCodesignError> {
663        let blob = RequirementBlob::from_blob_bytes(data.as_ref())?;
664
665        self.designated_requirement.insert(
666            scope,
667            DesignatedRequirementMode::Explicit(
668                blob.parse_expressions()?
669                    .iter()
670                    .map(|x| x.to_bytes())
671                    .collect::<Result<Vec<_>, AppleCodesignError>>()?,
672            ),
673        );
674
675        Ok(())
676    }
677
678    /// Set the designated requirement mode to auto, which will attempt to derive requirements
679    /// automatically.
680    ///
681    /// This setting recognizes when code signing is being performed with Apple issued code signing
682    /// certificates and automatically applies appropriate settings for the certificate being
683    /// used and the entity being signed.
684    ///
685    /// Not all combinations may be supported. If you get an error, you will need to
686    /// provide your own explicit requirement expression.
687    pub fn set_auto_designated_requirement(&mut self, scope: SettingsScope) {
688        self.designated_requirement
689            .insert(scope, DesignatedRequirementMode::Auto);
690    }
691
692    /// Obtain the code signature flags for a given scope.
693    pub fn code_signature_flags(
694        &self,
695        scope: impl AsRef<SettingsScope>,
696    ) -> Option<CodeSignatureFlags> {
697        let mut flags = self.code_signature_flags.get(scope.as_ref()).copied();
698
699        if self.for_notarization {
700            flags.get_or_insert(CodeSignatureFlags::default());
701
702            flags.as_mut().map(|flags| {
703                if !flags.contains(CodeSignatureFlags::RUNTIME) {
704                    info!("adding hardened runtime flag because notarization mode enabled");
705                }
706
707                flags.insert(CodeSignatureFlags::RUNTIME);
708            });
709        }
710
711        flags
712    }
713
714    /// Set code signature flags for signed Mach-O binaries.
715    ///
716    /// The incoming flags will replace any already-defined flags.
717    pub fn set_code_signature_flags(&mut self, scope: SettingsScope, flags: CodeSignatureFlags) {
718        self.code_signature_flags.insert(scope, flags);
719    }
720
721    /// Add code signature flags.
722    ///
723    /// The incoming flags will be ORd with any existing flags for the path
724    /// specified. The new flags will be returned.
725    pub fn add_code_signature_flags(
726        &mut self,
727        scope: SettingsScope,
728        flags: CodeSignatureFlags,
729    ) -> CodeSignatureFlags {
730        let existing = self
731            .code_signature_flags
732            .get(&scope)
733            .copied()
734            .unwrap_or_else(CodeSignatureFlags::empty);
735
736        let new = existing | flags;
737
738        self.code_signature_flags.insert(scope, new);
739
740        new
741    }
742
743    /// Remove code signature flags.
744    ///
745    /// The incoming flags will be removed from any existing flags for the path
746    /// specified. The new flags will be returned.
747    pub fn remove_code_signature_flags(
748        &mut self,
749        scope: SettingsScope,
750        flags: CodeSignatureFlags,
751    ) -> CodeSignatureFlags {
752        let existing = self
753            .code_signature_flags
754            .get(&scope)
755            .copied()
756            .unwrap_or_else(CodeSignatureFlags::empty);
757
758        let new = existing - flags;
759
760        self.code_signature_flags.insert(scope, new);
761
762        new
763    }
764
765    /// Obtain the `Info.plist` data registered to a given scope.
766    pub fn info_plist_data(&self, scope: impl AsRef<SettingsScope>) -> Option<&[u8]> {
767        self.info_plist_data
768            .get(scope.as_ref())
769            .map(|x| x.as_slice())
770    }
771
772    /// Obtain the runtime version for a given scope.
773    ///
774    /// The runtime version represents an OS version.
775    pub fn runtime_version(&self, scope: impl AsRef<SettingsScope>) -> Option<&semver::Version> {
776        self.runtime_version.get(scope.as_ref())
777    }
778
779    /// Set the runtime version to use in the code directory for a given scope.
780    ///
781    /// The runtime version corresponds to an OS version. The runtime version is usually
782    /// derived from the SDK version used to build the binary.
783    pub fn set_runtime_version(&mut self, scope: SettingsScope, version: semver::Version) {
784        self.runtime_version.insert(scope, version);
785    }
786
787    /// Define the `Info.plist` content.
788    ///
789    /// Signatures can reference the digest of an external `Info.plist` file in
790    /// the bundle the binary is located in.
791    ///
792    /// This function registers the raw content of that file is so that the
793    /// content can be digested and the digest can be included in the code directory.
794    ///
795    /// The value passed here should be the raw content of the `Info.plist` XML file.
796    ///
797    /// When signing bundles, this function is called automatically with the `Info.plist`
798    /// from the bundle. This function exists for cases where you are signing
799    /// individual Mach-O binaries and the `Info.plist` cannot be automatically
800    /// discovered.
801    pub fn set_info_plist_data(&mut self, scope: SettingsScope, data: Vec<u8>) {
802        self.info_plist_data.insert(scope, data);
803    }
804
805    /// Obtain the `CodeResources` XML file data registered to a given scope.
806    pub fn code_resources_data(&self, scope: impl AsRef<SettingsScope>) -> Option<&[u8]> {
807        self.code_resources_data
808            .get(scope.as_ref())
809            .map(|x| x.as_slice())
810    }
811
812    /// Define the `CodeResources` XML file content for a given scope.
813    ///
814    /// Bundles may contain a `CodeResources` XML file which defines additional
815    /// resource files and binaries outside the bundle's main executable. The code
816    /// directory of the main executable contains a digest of this file to establish
817    /// a chain of trust of the content of this XML file.
818    ///
819    /// This function defines the content of this external file so that the content
820    /// can be digested and that digest included in the code directory of the
821    /// binary being signed.
822    ///
823    /// When signing bundles, this function is called automatically with the content
824    /// of the `CodeResources` XML file, if present. This function exists for cases
825    /// where you are signing individual Mach-O binaries and the `CodeResources` XML
826    /// file cannot be automatically discovered.
827    pub fn set_code_resources_data(&mut self, scope: SettingsScope, data: Vec<u8>) {
828        self.code_resources_data.insert(scope, data);
829    }
830
831    /// Obtain extra digests to include in signatures.
832    pub fn extra_digests(&self, scope: impl AsRef<SettingsScope>) -> Option<&BTreeSet<DigestType>> {
833        self.extra_digests.get(scope.as_ref())
834    }
835
836    /// Register an addition content digest to use in signatures.
837    ///
838    /// Extra digests supplement the primary registered digest when the signer supports
839    /// it. Calling this likely results in an additional code directory being included
840    /// in embedded signatures.
841    ///
842    /// A common use case for this is to have the primary digest contain a legacy
843    /// digest type (namely SHA-1) but include stronger digests as well. This enables
844    /// signatures to have compatibility with older operating systems but still be modern.
845    pub fn add_extra_digest(&mut self, scope: SettingsScope, digest_type: DigestType) {
846        self.extra_digests
847            .entry(scope)
848            .or_default()
849            .insert(digest_type);
850    }
851
852    /// Obtain all configured digests for a scope.
853    pub fn all_digests(&self, scope: SettingsScope) -> Vec<DigestType> {
854        let mut res = vec![self.digest_type(scope.clone())];
855
856        if let Some(extra) = self.extra_digests(scope) {
857            res.extend(extra.iter());
858        }
859
860        res
861    }
862
863    /// Obtain the launch constraints on self.
864    pub fn launch_constraints_self(
865        &self,
866        scope: impl AsRef<SettingsScope>,
867    ) -> Option<&EncodedEnvironmentConstraints> {
868        self.launch_constraints_self.get(scope.as_ref())
869    }
870
871    /// Set the launch constraints on the current binary.
872    pub fn set_launch_constraints_self(
873        &mut self,
874        scope: SettingsScope,
875        constraints: EncodedEnvironmentConstraints,
876    ) {
877        self.launch_constraints_self.insert(scope, constraints);
878    }
879
880    /// Obtain the launch constraints on the parent process.
881    pub fn launch_constraints_parent(
882        &self,
883        scope: impl AsRef<SettingsScope>,
884    ) -> Option<&EncodedEnvironmentConstraints> {
885        self.launch_constraints_parent.get(scope.as_ref())
886    }
887
888    /// Set the launch constraints on the parent process.
889    pub fn set_launch_constraints_parent(
890        &mut self,
891        scope: SettingsScope,
892        constraints: EncodedEnvironmentConstraints,
893    ) {
894        self.launch_constraints_parent.insert(scope, constraints);
895    }
896
897    /// Obtain the launch constraints on the responsible process.
898    pub fn launch_constraints_responsible(
899        &self,
900        scope: impl AsRef<SettingsScope>,
901    ) -> Option<&EncodedEnvironmentConstraints> {
902        self.launch_constraints_responsible.get(scope.as_ref())
903    }
904
905    /// Set the launch constraints on the responsible process.
906    pub fn set_launch_constraints_responsible(
907        &mut self,
908        scope: SettingsScope,
909        constraints: EncodedEnvironmentConstraints,
910    ) {
911        self.launch_constraints_responsible
912            .insert(scope, constraints);
913    }
914
915    /// Obtain the constraints on loaded libraries.
916    pub fn library_constraints(
917        &self,
918        scope: impl AsRef<SettingsScope>,
919    ) -> Option<&EncodedEnvironmentConstraints> {
920        self.library_constraints.get(scope.as_ref())
921    }
922
923    /// Set the constraints on loaded libraries.
924    pub fn set_library_constraints(
925        &mut self,
926        scope: SettingsScope,
927        constraints: EncodedEnvironmentConstraints,
928    ) {
929        self.library_constraints.insert(scope, constraints);
930    }
931
932    /// Import existing state from Mach-O data.
933    ///
934    /// This will synchronize the signing settings with the state in the Mach-O file.
935    ///
936    /// If existing settings are explicitly set, they will be honored. Otherwise the state from
937    /// the Mach-O is imported into the settings.
938    pub fn import_settings_from_macho(&mut self, data: &[u8]) -> Result<(), AppleCodesignError> {
939        info!("inferring default signing settings from Mach-O binary");
940
941        let mut seen_identifier = None;
942
943        for macho in MachFile::parse(data)?.into_iter() {
944            let index = macho.index.unwrap_or(0);
945
946            let scope_main = SettingsScope::Main;
947            let scope_index = SettingsScope::MultiArchIndex(index);
948            let scope_arch = SettingsScope::MultiArchCpuType(macho.macho.header.cputype());
949
950            // Older operating system versions don't have support for SHA-256 in
951            // signatures. If the minimum version targeting in the binary doesn't
952            // support SHA-256, we automatically change the digest targeting settings
953            // so the binary will be signed correctly.
954            //
955            // And to maintain compatibility with Apple's tooling, if no targeting
956            // settings are present we also opt into SHA-1 + SHA-256.
957            let need_sha1_sha256 = if let Some(targeting) = macho.find_targeting()? {
958                let sha256_version = targeting.platform.sha256_digest_support()?;
959
960                if !sha256_version.matches(&targeting.minimum_os_version) {
961                    info!(
962                        "activating SHA-1 digests because minimum OS target {} is not {}",
963                        targeting.minimum_os_version, sha256_version
964                    );
965                    true
966                } else {
967                    false
968                }
969            } else {
970                info!("activating SHA-1 digests because no platform targeting in Mach-O");
971                true
972            };
973
974            if need_sha1_sha256 {
975                // This logic is a bit wonky. We want SHA-1 to be present on all binaries
976                // within a fat binary. So if we need SHA-1 mode, we set the setting on the
977                // main scope and then clear any overrides on fat binary scopes so our
978                // settings are canonical.
979                self.set_digest_type(scope_main.clone(), DigestType::Sha1);
980                self.add_extra_digest(scope_main.clone(), DigestType::Sha256);
981                self.extra_digests.remove(&scope_arch);
982                self.extra_digests.remove(&scope_index);
983            }
984
985            // The Mach-O can have embedded Info.plist data. Use it if available and not
986            // already defined in settings.
987            if let Some(info_plist) = macho.embedded_info_plist()? {
988                if self.info_plist_data(&scope_main).is_some()
989                    || self.info_plist_data(&scope_index).is_some()
990                    || self.info_plist_data(&scope_arch).is_some()
991                {
992                    info!("using Info.plist data from settings");
993                } else {
994                    info!("preserving Info.plist data already present in Mach-O");
995                    self.set_info_plist_data(scope_index.clone(), info_plist);
996                }
997            }
998
999            if let Some(sig) = macho.code_signature()? {
1000                if let Some(cd) = sig.code_directory()? {
1001                    if self.binary_identifier(&scope_main).is_some()
1002                        || self.binary_identifier(&scope_index).is_some()
1003                        || self.binary_identifier(&scope_arch).is_some()
1004                    {
1005                        info!("using binary identifier from settings");
1006                    } else if let Some(initial_identifier) = &seen_identifier {
1007                        // The binary identifier should agree between all Mach-O within a
1008                        // universal binary. If we've already seen an identifier, use it
1009                        // implicitly.
1010                        if initial_identifier != &*cd.ident {
1011                            info!(
1012                                "identifiers within Mach-O do not agree (initial: {initial_identifier}, subsequent: {}); reconciling to {initial_identifier}",
1013                                cd.ident
1014                            );
1015                            self.set_binary_identifier(scope_index.clone(), initial_identifier);
1016                        }
1017                    } else {
1018                        info!(
1019                            "preserving existing binary identifier in Mach-O ({})",
1020                            cd.ident
1021                        );
1022                        self.set_binary_identifier(scope_index.clone(), cd.ident.to_string());
1023                        seen_identifier = Some(cd.ident.to_string());
1024                    }
1025
1026                    if self.team_id.contains_key(&scope_main)
1027                        || self.team_id.contains_key(&scope_index)
1028                        || self.team_id.contains_key(&scope_arch)
1029                    {
1030                        info!("using team ID from settings");
1031                    } else if let Some(team_id) = cd.team_name {
1032                        // Team ID is only included when signing with an Apple signed
1033                        // certificate.
1034                        if self.signing_certificate_apple_signed() {
1035                            info!(
1036                                "preserving team ID in existing Mach-O signature ({})",
1037                                team_id
1038                            );
1039                            self.team_id
1040                                .insert(scope_index.clone(), team_id.to_string());
1041                        } else {
1042                            info!(
1043                                "dropping team ID {} because not signing with an Apple signed certificate",
1044                                team_id
1045                            );
1046                        }
1047                    }
1048
1049                    if self.code_signature_flags(&scope_main).is_some()
1050                        || self.code_signature_flags(&scope_index).is_some()
1051                        || self.code_signature_flags(&scope_arch).is_some()
1052                    {
1053                        info!("using code signature flags from settings");
1054                    } else if !cd.flags.is_empty() {
1055                        info!(
1056                            "preserving code signature flags in existing Mach-O signature ({:?})",
1057                            cd.flags
1058                        );
1059                        self.set_code_signature_flags(scope_index.clone(), cd.flags);
1060                    }
1061
1062                    if self.runtime_version(&scope_main).is_some()
1063                        || self.runtime_version(&scope_index).is_some()
1064                        || self.runtime_version(&scope_arch).is_some()
1065                    {
1066                        info!("using runtime version from settings");
1067                    } else if let Some(version) = cd.runtime {
1068                        let version = parse_version_nibbles(version);
1069
1070                        info!(
1071                            "preserving runtime version in existing Mach-O signature ({})",
1072                            version
1073                        );
1074                        self.set_runtime_version(scope_index.clone(), version);
1075                    }
1076                }
1077
1078                if let Some(entitlements) = sig.entitlements()? {
1079                    if self.entitlements_plist(&scope_main).is_some()
1080                        || self.entitlements_plist(&scope_index).is_some()
1081                        || self.entitlements_plist(&scope_arch).is_some()
1082                    {
1083                        info!("using entitlements from settings");
1084                    } else {
1085                        info!("preserving existing entitlements in Mach-O");
1086                        self.set_entitlements_xml(
1087                            SettingsScope::MultiArchIndex(index),
1088                            entitlements.as_str(),
1089                        )?;
1090                    }
1091                }
1092
1093                if let Some(constraints) = sig.launch_constraints_self()? {
1094                    if self.launch_constraints_self(&scope_main).is_some()
1095                        || self.launch_constraints_self(&scope_index).is_some()
1096                        || self.launch_constraints_self(&scope_arch).is_some()
1097                    {
1098                        info!("using self launch constraints from settings");
1099                    } else {
1100                        info!("preserving existing self launch constraints in Mach-O");
1101                        self.set_launch_constraints_self(
1102                            SettingsScope::MultiArchIndex(index),
1103                            constraints.parse_encoded_constraints()?,
1104                        );
1105                    }
1106                }
1107
1108                if let Some(constraints) = sig.launch_constraints_parent()? {
1109                    if self.launch_constraints_parent(&scope_main).is_some()
1110                        || self.launch_constraints_parent(&scope_index).is_some()
1111                        || self.launch_constraints_parent(&scope_arch).is_some()
1112                    {
1113                        info!("using parent launch constraints from settings");
1114                    } else {
1115                        info!("preserving existing parent launch constraints in Mach-O");
1116                        self.set_launch_constraints_parent(
1117                            SettingsScope::MultiArchIndex(index),
1118                            constraints.parse_encoded_constraints()?,
1119                        );
1120                    }
1121                }
1122
1123                if let Some(constraints) = sig.launch_constraints_responsible()? {
1124                    if self.launch_constraints_responsible(&scope_main).is_some()
1125                        || self.launch_constraints_responsible(&scope_index).is_some()
1126                        || self.launch_constraints_responsible(&scope_arch).is_some()
1127                    {
1128                        info!("using responsible process launch constraints from settings");
1129                    } else {
1130                        info!(
1131                            "preserving existing responsible process launch constraints in Mach-O"
1132                        );
1133                        self.set_launch_constraints_responsible(
1134                            SettingsScope::MultiArchIndex(index),
1135                            constraints.parse_encoded_constraints()?,
1136                        );
1137                    }
1138                }
1139
1140                if let Some(constraints) = sig.library_constraints()? {
1141                    if self.library_constraints(&scope_main).is_some()
1142                        || self.library_constraints(&scope_index).is_some()
1143                        || self.library_constraints(&scope_arch).is_some()
1144                    {
1145                        info!("using library constraints from settings");
1146                    } else {
1147                        info!("preserving existing library constraints in Mach-O");
1148                        self.set_library_constraints(
1149                            SettingsScope::MultiArchIndex(index),
1150                            constraints.parse_encoded_constraints()?,
1151                        );
1152                    }
1153                }
1154            }
1155        }
1156
1157        Ok(())
1158    }
1159
1160    /// Convert this instance to settings appropriate for a nested bundle.
1161    #[must_use]
1162    pub fn as_nested_bundle_settings(&self, bundle_path: &str) -> Self {
1163        self.clone_strip_prefix(
1164            bundle_path,
1165            format!("{bundle_path}/"),
1166            ScopedSetting::inherit_nested_bundle(),
1167        )
1168    }
1169
1170    /// Obtain the settings for a bundle's main executable.
1171    #[must_use]
1172    pub fn as_bundle_main_executable_settings(&self, path: &str) -> Self {
1173        self.clone_strip_prefix(path, path.to_string(), ScopedSetting::all())
1174    }
1175
1176    /// Convert this instance to settings appropriate for a Mach-O binary in a bundle.
1177    ///
1178    /// Only some settings are inherited from the bundle.
1179    #[must_use]
1180    pub fn as_bundle_macho_settings(&self, path: &str) -> Self {
1181        self.clone_strip_prefix(
1182            path,
1183            path.to_string(),
1184            ScopedSetting::inherit_nested_macho(),
1185        )
1186    }
1187
1188    /// Convert this instance to settings appropriate for a Mach-O within a universal one.
1189    ///
1190    /// It is assumed the main scope of these settings is already targeted for
1191    /// a Mach-O binary. Any scoped settings for the Mach-O binary index and CPU type
1192    /// will be applied. CPU type settings take precedence over index scoped settings.
1193    #[must_use]
1194    pub fn as_universal_macho_settings(&self, index: usize, cpu_type: CpuType) -> Self {
1195        self.clone_with_filter_map(|_, key| {
1196            if key == SettingsScope::Main
1197                || key == SettingsScope::MultiArchCpuType(cpu_type)
1198                || key == SettingsScope::MultiArchIndex(index)
1199            {
1200                Some(SettingsScope::Main)
1201            } else {
1202                None
1203            }
1204        })
1205    }
1206
1207    // Clones this instance, promoting `main_path` to the main scope and stripping
1208    // a prefix from other keys.
1209    fn clone_strip_prefix(
1210        &self,
1211        main_path: &str,
1212        prefix: String,
1213        preserve_settings: &[ScopedSetting],
1214    ) -> Self {
1215        self.clone_with_filter_map(|setting, key| match key {
1216            SettingsScope::Main => {
1217                if preserve_settings.contains(&setting) {
1218                    Some(SettingsScope::Main)
1219                } else {
1220                    None
1221                }
1222            }
1223            SettingsScope::Path(path) => {
1224                if path == main_path {
1225                    Some(SettingsScope::Main)
1226                } else {
1227                    path.strip_prefix(&prefix)
1228                        .map(|path| SettingsScope::Path(path.to_string()))
1229                }
1230            }
1231
1232            // Top-level multiarch settings are a bit wonky: it doesn't really
1233            // make much sense for them to propagate across binaries. But we do
1234            // allow it.
1235            SettingsScope::MultiArchIndex(index) => {
1236                if preserve_settings.contains(&setting) {
1237                    Some(SettingsScope::MultiArchIndex(index))
1238                } else {
1239                    None
1240                }
1241            }
1242            SettingsScope::MultiArchCpuType(cpu_type) => {
1243                if preserve_settings.contains(&setting) {
1244                    Some(SettingsScope::MultiArchCpuType(cpu_type))
1245                } else {
1246                    None
1247                }
1248            }
1249
1250            SettingsScope::PathMultiArchIndex(path, index) => {
1251                if path == main_path {
1252                    Some(SettingsScope::MultiArchIndex(index))
1253                } else {
1254                    path.strip_prefix(&prefix)
1255                        .map(|path| SettingsScope::PathMultiArchIndex(path.to_string(), index))
1256                }
1257            }
1258            SettingsScope::PathMultiArchCpuType(path, cpu_type) => {
1259                if path == main_path {
1260                    Some(SettingsScope::MultiArchCpuType(cpu_type))
1261                } else {
1262                    path.strip_prefix(&prefix)
1263                        .map(|path| SettingsScope::PathMultiArchCpuType(path.to_string(), cpu_type))
1264                }
1265            }
1266        })
1267    }
1268
1269    fn clone_with_filter_map(
1270        &self,
1271        key_map: impl Fn(ScopedSetting, SettingsScope) -> Option<SettingsScope>,
1272    ) -> Self {
1273        Self {
1274            signing_key: self.signing_key.clone(),
1275            certificates: self.certificates.clone(),
1276            time_stamp_url: self.time_stamp_url.clone(),
1277            signing_time: self.signing_time,
1278            team_id: self.team_id.clone(),
1279            path_exclusion_patterns: self.path_exclusion_patterns.clone(),
1280            shallow: self.shallow,
1281            for_notarization: self.for_notarization,
1282            digest_type: self
1283                .digest_type
1284                .clone()
1285                .into_iter()
1286                .filter_map(|(key, value)| {
1287                    key_map(ScopedSetting::Digest, key).map(|key| (key, value))
1288                })
1289                .collect::<BTreeMap<_, _>>(),
1290            identifiers: self
1291                .identifiers
1292                .clone()
1293                .into_iter()
1294                .filter_map(|(key, value)| {
1295                    key_map(ScopedSetting::BinaryIdentifier, key).map(|key| (key, value))
1296                })
1297                .collect::<BTreeMap<_, _>>(),
1298            entitlements: self
1299                .entitlements
1300                .clone()
1301                .into_iter()
1302                .filter_map(|(key, value)| {
1303                    key_map(ScopedSetting::Entitlements, key).map(|key| (key, value))
1304                })
1305                .collect::<BTreeMap<_, _>>(),
1306            designated_requirement: self
1307                .designated_requirement
1308                .clone()
1309                .into_iter()
1310                .filter_map(|(key, value)| {
1311                    key_map(ScopedSetting::DesignatedRequirements, key).map(|key| (key, value))
1312                })
1313                .collect::<BTreeMap<_, _>>(),
1314            code_signature_flags: self
1315                .code_signature_flags
1316                .clone()
1317                .into_iter()
1318                .filter_map(|(key, value)| {
1319                    key_map(ScopedSetting::CodeSignatureFlags, key).map(|key| (key, value))
1320                })
1321                .collect::<BTreeMap<_, _>>(),
1322            runtime_version: self
1323                .runtime_version
1324                .clone()
1325                .into_iter()
1326                .filter_map(|(key, value)| {
1327                    key_map(ScopedSetting::RuntimeVersion, key).map(|key| (key, value))
1328                })
1329                .collect::<BTreeMap<_, _>>(),
1330            info_plist_data: self
1331                .info_plist_data
1332                .clone()
1333                .into_iter()
1334                .filter_map(|(key, value)| {
1335                    key_map(ScopedSetting::InfoPlist, key).map(|key| (key, value))
1336                })
1337                .collect::<BTreeMap<_, _>>(),
1338            code_resources_data: self
1339                .code_resources_data
1340                .clone()
1341                .into_iter()
1342                .filter_map(|(key, value)| {
1343                    key_map(ScopedSetting::CodeResources, key).map(|key| (key, value))
1344                })
1345                .collect::<BTreeMap<_, _>>(),
1346            extra_digests: self
1347                .extra_digests
1348                .clone()
1349                .into_iter()
1350                .filter_map(|(key, value)| {
1351                    key_map(ScopedSetting::ExtraDigests, key).map(|key| (key, value))
1352                })
1353                .collect::<BTreeMap<_, _>>(),
1354            launch_constraints_self: self
1355                .launch_constraints_self
1356                .clone()
1357                .into_iter()
1358                .filter_map(|(key, value)| {
1359                    key_map(ScopedSetting::LaunchConstraintsSelf, key).map(|key| (key, value))
1360                })
1361                .collect::<BTreeMap<_, _>>(),
1362            launch_constraints_parent: self
1363                .launch_constraints_parent
1364                .clone()
1365                .into_iter()
1366                .filter_map(|(key, value)| {
1367                    key_map(ScopedSetting::LaunchConstraintsParent, key).map(|key| (key, value))
1368                })
1369                .collect::<BTreeMap<_, _>>(),
1370            launch_constraints_responsible: self
1371                .launch_constraints_responsible
1372                .clone()
1373                .into_iter()
1374                .filter_map(|(key, value)| {
1375                    key_map(ScopedSetting::LaunchConstraintsResponsible, key)
1376                        .map(|key| (key, value))
1377                })
1378                .collect::<BTreeMap<_, _>>(),
1379            library_constraints: self
1380                .library_constraints
1381                .clone()
1382                .into_iter()
1383                .filter_map(|(key, value)| {
1384                    key_map(ScopedSetting::LibraryConstraints, key).map(|key| (key, value))
1385                })
1386                .collect::<BTreeMap<_, _>>(),
1387        }
1388    }
1389
1390    /// Attempt to validate the settings consistency when the `for notarization` flag is set.
1391    ///
1392    /// On error, logs errors at error level and returns an Err.
1393    pub fn ensure_for_notarization_settings(&self) -> Result<(), AppleCodesignError> {
1394        if !self.for_notarization {
1395            return Ok(());
1396        }
1397
1398        let mut have_error = false;
1399
1400        if let Some((_, cert)) = self.signing_key() {
1401            if !cert.chains_to_apple_root_ca() && !cert.is_test_apple_signed_certificate() {
1402                error!(
1403                    "--for-notarization requires use of an Apple-issued signing certificate; current certificate is not signed by Apple"
1404                );
1405                error!(
1406                    "hint: use a signing certificate issued by Apple that is signed by an Apple certificate authority"
1407                );
1408                have_error = true;
1409            }
1410
1411            if !cert.apple_code_signing_extensions().into_iter().any(|e| {
1412                e == CodeSigningCertificateExtension::DeveloperIdApplication
1413                    || e == CodeSigningCertificateExtension::DeveloperIdInstaller
1414                    || e == CodeSigningCertificateExtension::DeveloperIdKernel {}
1415            }) {
1416                error!(
1417                    "--for-notarization requires use of a Developer ID signing certificate; current certificate doesn't appear to be such a certificate"
1418                );
1419                error!(
1420                    "hint: use a `Developer ID Application`, `Developer ID Installer`, or `Developer ID Kernel` certificate"
1421                );
1422                have_error = true;
1423            }
1424
1425            if self.time_stamp_url().is_none() {
1426                error!(
1427                    "--for-notarization requires use of a time-stamp protocol server; none configured"
1428                );
1429                have_error = true;
1430            }
1431        } else {
1432            error!(
1433                "--for-notarization requires use of a Developer ID signing certificate; no signing certificate was provided"
1434            );
1435            have_error = true;
1436        }
1437
1438        if have_error {
1439            Err(AppleCodesignError::ForNotarizationInvalidSettings)
1440        } else {
1441            Ok(())
1442        }
1443    }
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448    use {super::*, indoc::indoc};
1449
1450    const ENTITLEMENTS_XML: &str = indoc! {r#"
1451        <?xml version="1.0" encoding="UTF-8"?>
1452        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1453        <plist version="1.0">
1454        <dict>
1455            <key>application-identifier</key>
1456            <string>appid</string>
1457            <key>com.apple.developer.team-identifier</key>
1458            <string>ABCDEF</string>
1459        </dict>
1460        </plist>
1461    "#};
1462
1463    #[test]
1464    fn parse_settings_scope() {
1465        assert_eq!(
1466            SettingsScope::try_from("@main").unwrap(),
1467            SettingsScope::Main
1468        );
1469        assert_eq!(
1470            SettingsScope::try_from("@0").unwrap(),
1471            SettingsScope::MultiArchIndex(0)
1472        );
1473        assert_eq!(
1474            SettingsScope::try_from("@42").unwrap(),
1475            SettingsScope::MultiArchIndex(42)
1476        );
1477        assert_eq!(
1478            SettingsScope::try_from("@[cpu_type=7]").unwrap(),
1479            SettingsScope::MultiArchCpuType(7)
1480        );
1481        assert_eq!(
1482            SettingsScope::try_from("@[cpu_type=arm]").unwrap(),
1483            SettingsScope::MultiArchCpuType(CPU_TYPE_ARM)
1484        );
1485        assert_eq!(
1486            SettingsScope::try_from("@[cpu_type=arm64]").unwrap(),
1487            SettingsScope::MultiArchCpuType(CPU_TYPE_ARM64)
1488        );
1489        assert_eq!(
1490            SettingsScope::try_from("@[cpu_type=arm64_32]").unwrap(),
1491            SettingsScope::MultiArchCpuType(CPU_TYPE_ARM64_32)
1492        );
1493        assert_eq!(
1494            SettingsScope::try_from("@[cpu_type=x86_64]").unwrap(),
1495            SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64)
1496        );
1497        assert_eq!(
1498            SettingsScope::try_from("foo/bar").unwrap(),
1499            SettingsScope::Path("foo/bar".into())
1500        );
1501        assert_eq!(
1502            SettingsScope::try_from("foo/bar@0").unwrap(),
1503            SettingsScope::PathMultiArchIndex("foo/bar".into(), 0)
1504        );
1505        assert_eq!(
1506            SettingsScope::try_from("foo/bar@[cpu_type=7]").unwrap(),
1507            SettingsScope::PathMultiArchCpuType("foo/bar".into(), 7_u32)
1508        );
1509    }
1510
1511    #[test]
1512    fn as_nested_macho_settings() {
1513        let mut main_settings = SigningSettings::default();
1514        main_settings.set_binary_identifier(SettingsScope::Main, "ident");
1515        main_settings
1516            .set_code_signature_flags(SettingsScope::Main, CodeSignatureFlags::FORCE_EXPIRATION);
1517
1518        main_settings.set_code_signature_flags(
1519            SettingsScope::MultiArchIndex(0),
1520            CodeSignatureFlags::FORCE_HARD,
1521        );
1522        main_settings.set_code_signature_flags(
1523            SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
1524            CodeSignatureFlags::RESTRICT,
1525        );
1526        main_settings.set_info_plist_data(SettingsScope::MultiArchIndex(0), b"index_0".to_vec());
1527        main_settings.set_info_plist_data(
1528            SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
1529            b"cpu_x86_64".to_vec(),
1530        );
1531
1532        let macho_settings = main_settings.as_universal_macho_settings(0, CPU_TYPE_ARM64);
1533        assert_eq!(
1534            macho_settings.binary_identifier(SettingsScope::Main),
1535            Some("ident")
1536        );
1537        assert_eq!(
1538            macho_settings.code_signature_flags(SettingsScope::Main),
1539            Some(CodeSignatureFlags::FORCE_HARD)
1540        );
1541        assert_eq!(
1542            macho_settings.info_plist_data(SettingsScope::Main),
1543            Some(b"index_0".as_ref())
1544        );
1545
1546        let macho_settings = main_settings.as_universal_macho_settings(0, CPU_TYPE_X86_64);
1547        assert_eq!(
1548            macho_settings.binary_identifier(SettingsScope::Main),
1549            Some("ident")
1550        );
1551        assert_eq!(
1552            macho_settings.code_signature_flags(SettingsScope::Main),
1553            Some(CodeSignatureFlags::RESTRICT)
1554        );
1555        assert_eq!(
1556            macho_settings.info_plist_data(SettingsScope::Main),
1557            Some(b"cpu_x86_64".as_ref())
1558        );
1559    }
1560
1561    #[test]
1562    fn as_bundle_macho_settings() {
1563        let mut main_settings = SigningSettings::default();
1564        main_settings.set_info_plist_data(SettingsScope::Main, b"main".to_vec());
1565        main_settings.set_info_plist_data(
1566            SettingsScope::Path("Contents/MacOS/main".into()),
1567            b"main_exe".to_vec(),
1568        );
1569        main_settings.set_info_plist_data(
1570            SettingsScope::PathMultiArchIndex("Contents/MacOS/main".into(), 0),
1571            b"main_exe_index_0".to_vec(),
1572        );
1573        main_settings.set_info_plist_data(
1574            SettingsScope::PathMultiArchCpuType("Contents/MacOS/main".into(), CPU_TYPE_X86_64),
1575            b"main_exe_x86_64".to_vec(),
1576        );
1577
1578        let macho_settings = main_settings.as_bundle_macho_settings("Contents/MacOS/main");
1579        assert_eq!(
1580            macho_settings.info_plist_data(SettingsScope::Main),
1581            Some(b"main_exe".as_ref())
1582        );
1583        assert_eq!(
1584            macho_settings.info_plist_data,
1585            [
1586                (SettingsScope::Main, b"main_exe".to_vec()),
1587                (
1588                    SettingsScope::MultiArchIndex(0),
1589                    b"main_exe_index_0".to_vec()
1590                ),
1591                (
1592                    SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
1593                    b"main_exe_x86_64".to_vec()
1594                ),
1595            ]
1596            .iter()
1597            .cloned()
1598            .collect::<BTreeMap<SettingsScope, Vec<u8>>>()
1599        );
1600    }
1601
1602    #[test]
1603    fn as_nested_bundle_settings() {
1604        let mut main_settings = SigningSettings::default();
1605        main_settings.set_info_plist_data(SettingsScope::Main, b"main".to_vec());
1606        main_settings.set_info_plist_data(
1607            SettingsScope::Path("Contents/MacOS/main".into()),
1608            b"main_exe".to_vec(),
1609        );
1610        main_settings.set_info_plist_data(
1611            SettingsScope::Path("Contents/MacOS/nested.app".into()),
1612            b"bundle".to_vec(),
1613        );
1614        main_settings.set_info_plist_data(
1615            SettingsScope::PathMultiArchIndex("Contents/MacOS/nested.app".into(), 0),
1616            b"bundle_index_0".to_vec(),
1617        );
1618        main_settings.set_info_plist_data(
1619            SettingsScope::PathMultiArchCpuType(
1620                "Contents/MacOS/nested.app".into(),
1621                CPU_TYPE_X86_64,
1622            ),
1623            b"bundle_x86_64".to_vec(),
1624        );
1625        main_settings.set_info_plist_data(
1626            SettingsScope::Path("Contents/MacOS/nested.app/Contents/MacOS/nested".into()),
1627            b"nested_main_exe".to_vec(),
1628        );
1629        main_settings.set_info_plist_data(
1630            SettingsScope::PathMultiArchIndex(
1631                "Contents/MacOS/nested.app/Contents/MacOS/nested".into(),
1632                0,
1633            ),
1634            b"nested_main_exe_index_0".to_vec(),
1635        );
1636        main_settings.set_info_plist_data(
1637            SettingsScope::PathMultiArchCpuType(
1638                "Contents/MacOS/nested.app/Contents/MacOS/nested".into(),
1639                CPU_TYPE_X86_64,
1640            ),
1641            b"nested_main_exe_x86_64".to_vec(),
1642        );
1643
1644        let bundle_settings = main_settings.as_nested_bundle_settings("Contents/MacOS/nested.app");
1645        assert_eq!(
1646            bundle_settings.info_plist_data(SettingsScope::Main),
1647            Some(b"bundle".as_ref())
1648        );
1649        assert_eq!(
1650            bundle_settings.info_plist_data(SettingsScope::Path("Contents/MacOS/nested".into())),
1651            Some(b"nested_main_exe".as_ref())
1652        );
1653        assert_eq!(
1654            bundle_settings.info_plist_data,
1655            [
1656                (SettingsScope::Main, b"bundle".to_vec()),
1657                (SettingsScope::MultiArchIndex(0), b"bundle_index_0".to_vec()),
1658                (
1659                    SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
1660                    b"bundle_x86_64".to_vec()
1661                ),
1662                (
1663                    SettingsScope::Path("Contents/MacOS/nested".into()),
1664                    b"nested_main_exe".to_vec()
1665                ),
1666                (
1667                    SettingsScope::PathMultiArchIndex("Contents/MacOS/nested".into(), 0),
1668                    b"nested_main_exe_index_0".to_vec()
1669                ),
1670                (
1671                    SettingsScope::PathMultiArchCpuType(
1672                        "Contents/MacOS/nested".into(),
1673                        CPU_TYPE_X86_64
1674                    ),
1675                    b"nested_main_exe_x86_64".to_vec()
1676                ),
1677            ]
1678            .iter()
1679            .cloned()
1680            .collect::<BTreeMap<SettingsScope, Vec<u8>>>()
1681        );
1682    }
1683
1684    #[test]
1685    fn entitlements_handling() -> Result<(), AppleCodesignError> {
1686        let mut settings = SigningSettings::default();
1687        settings.set_entitlements_xml(SettingsScope::Main, ENTITLEMENTS_XML)?;
1688
1689        let s = settings.entitlements_xml(SettingsScope::Main)?;
1690        assert_eq!(s, Some("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>application-identifier</key>\n\t<string>appid</string>\n\t<key>com.apple.developer.team-identifier</key>\n\t<string>ABCDEF</string>\n</dict>\n</plist>".into()));
1691
1692        Ok(())
1693    }
1694
1695    #[test]
1696    fn for_notarization_handling() -> Result<(), AppleCodesignError> {
1697        let mut settings = SigningSettings::default();
1698        settings.set_for_notarization(true);
1699
1700        assert_eq!(
1701            settings.code_signature_flags(SettingsScope::Main),
1702            Some(CodeSignatureFlags::RUNTIME)
1703        );
1704
1705        assert_eq!(
1706            settings
1707                .as_bundle_macho_settings("")
1708                .code_signature_flags(SettingsScope::Main),
1709            Some(CodeSignatureFlags::RUNTIME)
1710        );
1711
1712        Ok(())
1713    }
1714}