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::{parse_version_nibbles, MachFile},
17    },
18    glob::Pattern,
19    goblin::mach::cputype::{
20        CpuType, CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_ARM64_32, CPU_TYPE_X86_64,
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.as_ref() {
1011                            info!("identifiers within Mach-O do not agree (initial: {initial_identifier}, subsequent: {}); reconciling to {initial_identifier}",
1012                            cd.ident);
1013                            self.set_binary_identifier(scope_index.clone(), initial_identifier);
1014                        }
1015                    } else {
1016                        info!(
1017                            "preserving existing binary identifier in Mach-O ({})",
1018                            cd.ident.to_string()
1019                        );
1020                        self.set_binary_identifier(scope_index.clone(), cd.ident.to_string());
1021                        seen_identifier = Some(cd.ident.to_string());
1022                    }
1023
1024                    if self.team_id.contains_key(&scope_main)
1025                        || self.team_id.contains_key(&scope_index)
1026                        || self.team_id.contains_key(&scope_arch)
1027                    {
1028                        info!("using team ID from settings");
1029                    } else if let Some(team_id) = cd.team_name {
1030                        // Team ID is only included when signing with an Apple signed
1031                        // certificate.
1032                        if self.signing_certificate_apple_signed() {
1033                            info!(
1034                                "preserving team ID in existing Mach-O signature ({})",
1035                                team_id
1036                            );
1037                            self.team_id
1038                                .insert(scope_index.clone(), team_id.to_string());
1039                        } else {
1040                            info!("dropping team ID {} because not signing with an Apple signed certificate", team_id);
1041                        }
1042                    }
1043
1044                    if self.code_signature_flags(&scope_main).is_some()
1045                        || self.code_signature_flags(&scope_index).is_some()
1046                        || self.code_signature_flags(&scope_arch).is_some()
1047                    {
1048                        info!("using code signature flags from settings");
1049                    } else if !cd.flags.is_empty() {
1050                        info!(
1051                            "preserving code signature flags in existing Mach-O signature ({:?})",
1052                            cd.flags
1053                        );
1054                        self.set_code_signature_flags(scope_index.clone(), cd.flags);
1055                    }
1056
1057                    if self.runtime_version(&scope_main).is_some()
1058                        || self.runtime_version(&scope_index).is_some()
1059                        || self.runtime_version(&scope_arch).is_some()
1060                    {
1061                        info!("using runtime version from settings");
1062                    } else if let Some(version) = cd.runtime {
1063                        let version = parse_version_nibbles(version);
1064
1065                        info!(
1066                            "preserving runtime version in existing Mach-O signature ({})",
1067                            version
1068                        );
1069                        self.set_runtime_version(scope_index.clone(), version);
1070                    }
1071                }
1072
1073                if let Some(entitlements) = sig.entitlements()? {
1074                    if self.entitlements_plist(&scope_main).is_some()
1075                        || self.entitlements_plist(&scope_index).is_some()
1076                        || self.entitlements_plist(&scope_arch).is_some()
1077                    {
1078                        info!("using entitlements from settings");
1079                    } else {
1080                        info!("preserving existing entitlements in Mach-O");
1081                        self.set_entitlements_xml(
1082                            SettingsScope::MultiArchIndex(index),
1083                            entitlements.as_str(),
1084                        )?;
1085                    }
1086                }
1087
1088                if let Some(constraints) = sig.launch_constraints_self()? {
1089                    if self.launch_constraints_self(&scope_main).is_some()
1090                        || self.launch_constraints_self(&scope_index).is_some()
1091                        || self.launch_constraints_self(&scope_arch).is_some()
1092                    {
1093                        info!("using self launch constraints from settings");
1094                    } else {
1095                        info!("preserving existing self launch constraints in Mach-O");
1096                        self.set_launch_constraints_self(
1097                            SettingsScope::MultiArchIndex(index),
1098                            constraints.parse_encoded_constraints()?,
1099                        );
1100                    }
1101                }
1102
1103                if let Some(constraints) = sig.launch_constraints_parent()? {
1104                    if self.launch_constraints_parent(&scope_main).is_some()
1105                        || self.launch_constraints_parent(&scope_index).is_some()
1106                        || self.launch_constraints_parent(&scope_arch).is_some()
1107                    {
1108                        info!("using parent launch constraints from settings");
1109                    } else {
1110                        info!("preserving existing parent launch constraints in Mach-O");
1111                        self.set_launch_constraints_parent(
1112                            SettingsScope::MultiArchIndex(index),
1113                            constraints.parse_encoded_constraints()?,
1114                        );
1115                    }
1116                }
1117
1118                if let Some(constraints) = sig.launch_constraints_responsible()? {
1119                    if self.launch_constraints_responsible(&scope_main).is_some()
1120                        || self.launch_constraints_responsible(&scope_index).is_some()
1121                        || self.launch_constraints_responsible(&scope_arch).is_some()
1122                    {
1123                        info!("using responsible process launch constraints from settings");
1124                    } else {
1125                        info!(
1126                            "preserving existing responsible process launch constraints in Mach-O"
1127                        );
1128                        self.set_launch_constraints_responsible(
1129                            SettingsScope::MultiArchIndex(index),
1130                            constraints.parse_encoded_constraints()?,
1131                        );
1132                    }
1133                }
1134
1135                if let Some(constraints) = sig.library_constraints()? {
1136                    if self.library_constraints(&scope_main).is_some()
1137                        || self.library_constraints(&scope_index).is_some()
1138                        || self.library_constraints(&scope_arch).is_some()
1139                    {
1140                        info!("using library constraints from settings");
1141                    } else {
1142                        info!("preserving existing library constraints in Mach-O");
1143                        self.set_library_constraints(
1144                            SettingsScope::MultiArchIndex(index),
1145                            constraints.parse_encoded_constraints()?,
1146                        );
1147                    }
1148                }
1149            }
1150        }
1151
1152        Ok(())
1153    }
1154
1155    /// Convert this instance to settings appropriate for a nested bundle.
1156    #[must_use]
1157    pub fn as_nested_bundle_settings(&self, bundle_path: &str) -> Self {
1158        self.clone_strip_prefix(
1159            bundle_path,
1160            format!("{bundle_path}/"),
1161            ScopedSetting::inherit_nested_bundle(),
1162        )
1163    }
1164
1165    /// Obtain the settings for a bundle's main executable.
1166    #[must_use]
1167    pub fn as_bundle_main_executable_settings(&self, path: &str) -> Self {
1168        self.clone_strip_prefix(path, path.to_string(), ScopedSetting::all())
1169    }
1170
1171    /// Convert this instance to settings appropriate for a Mach-O binary in a bundle.
1172    ///
1173    /// Only some settings are inherited from the bundle.
1174    #[must_use]
1175    pub fn as_bundle_macho_settings(&self, path: &str) -> Self {
1176        self.clone_strip_prefix(
1177            path,
1178            path.to_string(),
1179            ScopedSetting::inherit_nested_macho(),
1180        )
1181    }
1182
1183    /// Convert this instance to settings appropriate for a Mach-O within a universal one.
1184    ///
1185    /// It is assumed the main scope of these settings is already targeted for
1186    /// a Mach-O binary. Any scoped settings for the Mach-O binary index and CPU type
1187    /// will be applied. CPU type settings take precedence over index scoped settings.
1188    #[must_use]
1189    pub fn as_universal_macho_settings(&self, index: usize, cpu_type: CpuType) -> Self {
1190        self.clone_with_filter_map(|_, key| {
1191            if key == SettingsScope::Main
1192                || key == SettingsScope::MultiArchCpuType(cpu_type)
1193                || key == SettingsScope::MultiArchIndex(index)
1194            {
1195                Some(SettingsScope::Main)
1196            } else {
1197                None
1198            }
1199        })
1200    }
1201
1202    // Clones this instance, promoting `main_path` to the main scope and stripping
1203    // a prefix from other keys.
1204    fn clone_strip_prefix(
1205        &self,
1206        main_path: &str,
1207        prefix: String,
1208        preserve_settings: &[ScopedSetting],
1209    ) -> Self {
1210        self.clone_with_filter_map(|setting, key| match key {
1211            SettingsScope::Main => {
1212                if preserve_settings.contains(&setting) {
1213                    Some(SettingsScope::Main)
1214                } else {
1215                    None
1216                }
1217            }
1218            SettingsScope::Path(path) => {
1219                if path == main_path {
1220                    Some(SettingsScope::Main)
1221                } else {
1222                    path.strip_prefix(&prefix)
1223                        .map(|path| SettingsScope::Path(path.to_string()))
1224                }
1225            }
1226
1227            // Top-level multiarch settings are a bit wonky: it doesn't really
1228            // make much sense for them to propagate across binaries. But we do
1229            // allow it.
1230            SettingsScope::MultiArchIndex(index) => {
1231                if preserve_settings.contains(&setting) {
1232                    Some(SettingsScope::MultiArchIndex(index))
1233                } else {
1234                    None
1235                }
1236            }
1237            SettingsScope::MultiArchCpuType(cpu_type) => {
1238                if preserve_settings.contains(&setting) {
1239                    Some(SettingsScope::MultiArchCpuType(cpu_type))
1240                } else {
1241                    None
1242                }
1243            }
1244
1245            SettingsScope::PathMultiArchIndex(path, index) => {
1246                if path == main_path {
1247                    Some(SettingsScope::MultiArchIndex(index))
1248                } else {
1249                    path.strip_prefix(&prefix)
1250                        .map(|path| SettingsScope::PathMultiArchIndex(path.to_string(), index))
1251                }
1252            }
1253            SettingsScope::PathMultiArchCpuType(path, cpu_type) => {
1254                if path == main_path {
1255                    Some(SettingsScope::MultiArchCpuType(cpu_type))
1256                } else {
1257                    path.strip_prefix(&prefix)
1258                        .map(|path| SettingsScope::PathMultiArchCpuType(path.to_string(), cpu_type))
1259                }
1260            }
1261        })
1262    }
1263
1264    fn clone_with_filter_map(
1265        &self,
1266        key_map: impl Fn(ScopedSetting, SettingsScope) -> Option<SettingsScope>,
1267    ) -> Self {
1268        Self {
1269            signing_key: self.signing_key.clone(),
1270            certificates: self.certificates.clone(),
1271            time_stamp_url: self.time_stamp_url.clone(),
1272            signing_time: self.signing_time,
1273            team_id: self.team_id.clone(),
1274            path_exclusion_patterns: self.path_exclusion_patterns.clone(),
1275            shallow: self.shallow,
1276            for_notarization: self.for_notarization,
1277            digest_type: self
1278                .digest_type
1279                .clone()
1280                .into_iter()
1281                .filter_map(|(key, value)| {
1282                    key_map(ScopedSetting::Digest, key).map(|key| (key, value))
1283                })
1284                .collect::<BTreeMap<_, _>>(),
1285            identifiers: self
1286                .identifiers
1287                .clone()
1288                .into_iter()
1289                .filter_map(|(key, value)| {
1290                    key_map(ScopedSetting::BinaryIdentifier, key).map(|key| (key, value))
1291                })
1292                .collect::<BTreeMap<_, _>>(),
1293            entitlements: self
1294                .entitlements
1295                .clone()
1296                .into_iter()
1297                .filter_map(|(key, value)| {
1298                    key_map(ScopedSetting::Entitlements, key).map(|key| (key, value))
1299                })
1300                .collect::<BTreeMap<_, _>>(),
1301            designated_requirement: self
1302                .designated_requirement
1303                .clone()
1304                .into_iter()
1305                .filter_map(|(key, value)| {
1306                    key_map(ScopedSetting::DesignatedRequirements, key).map(|key| (key, value))
1307                })
1308                .collect::<BTreeMap<_, _>>(),
1309            code_signature_flags: self
1310                .code_signature_flags
1311                .clone()
1312                .into_iter()
1313                .filter_map(|(key, value)| {
1314                    key_map(ScopedSetting::CodeSignatureFlags, key).map(|key| (key, value))
1315                })
1316                .collect::<BTreeMap<_, _>>(),
1317            runtime_version: self
1318                .runtime_version
1319                .clone()
1320                .into_iter()
1321                .filter_map(|(key, value)| {
1322                    key_map(ScopedSetting::RuntimeVersion, key).map(|key| (key, value))
1323                })
1324                .collect::<BTreeMap<_, _>>(),
1325            info_plist_data: self
1326                .info_plist_data
1327                .clone()
1328                .into_iter()
1329                .filter_map(|(key, value)| {
1330                    key_map(ScopedSetting::InfoPlist, key).map(|key| (key, value))
1331                })
1332                .collect::<BTreeMap<_, _>>(),
1333            code_resources_data: self
1334                .code_resources_data
1335                .clone()
1336                .into_iter()
1337                .filter_map(|(key, value)| {
1338                    key_map(ScopedSetting::CodeResources, key).map(|key| (key, value))
1339                })
1340                .collect::<BTreeMap<_, _>>(),
1341            extra_digests: self
1342                .extra_digests
1343                .clone()
1344                .into_iter()
1345                .filter_map(|(key, value)| {
1346                    key_map(ScopedSetting::ExtraDigests, key).map(|key| (key, value))
1347                })
1348                .collect::<BTreeMap<_, _>>(),
1349            launch_constraints_self: self
1350                .launch_constraints_self
1351                .clone()
1352                .into_iter()
1353                .filter_map(|(key, value)| {
1354                    key_map(ScopedSetting::LaunchConstraintsSelf, key).map(|key| (key, value))
1355                })
1356                .collect::<BTreeMap<_, _>>(),
1357            launch_constraints_parent: self
1358                .launch_constraints_parent
1359                .clone()
1360                .into_iter()
1361                .filter_map(|(key, value)| {
1362                    key_map(ScopedSetting::LaunchConstraintsParent, key).map(|key| (key, value))
1363                })
1364                .collect::<BTreeMap<_, _>>(),
1365            launch_constraints_responsible: self
1366                .launch_constraints_responsible
1367                .clone()
1368                .into_iter()
1369                .filter_map(|(key, value)| {
1370                    key_map(ScopedSetting::LaunchConstraintsResponsible, key)
1371                        .map(|key| (key, value))
1372                })
1373                .collect::<BTreeMap<_, _>>(),
1374            library_constraints: self
1375                .library_constraints
1376                .clone()
1377                .into_iter()
1378                .filter_map(|(key, value)| {
1379                    key_map(ScopedSetting::LibraryConstraints, key).map(|key| (key, value))
1380                })
1381                .collect::<BTreeMap<_, _>>(),
1382        }
1383    }
1384
1385    /// Attempt to validate the settings consistency when the `for notarization` flag is set.
1386    ///
1387    /// On error, logs errors at error level and returns an Err.
1388    pub fn ensure_for_notarization_settings(&self) -> Result<(), AppleCodesignError> {
1389        if !self.for_notarization {
1390            return Ok(());
1391        }
1392
1393        let mut have_error = false;
1394
1395        if let Some((_, cert)) = self.signing_key() {
1396            if !cert.chains_to_apple_root_ca() && !cert.is_test_apple_signed_certificate() {
1397                error!("--for-notarization requires use of an Apple-issued signing certificate; current certificate is not signed by Apple");
1398                error!("hint: use a signing certificate issued by Apple that is signed by an Apple certificate authority");
1399                have_error = true;
1400            }
1401
1402            if !cert.apple_code_signing_extensions().into_iter().any(|e| {
1403                e == CodeSigningCertificateExtension::DeveloperIdApplication
1404                    || e == CodeSigningCertificateExtension::DeveloperIdInstaller
1405                    || e == CodeSigningCertificateExtension::DeveloperIdKernel {}
1406            }) {
1407                error!("--for-notarization requires use of a Developer ID signing certificate; current certificate doesn't appear to be such a certificate");
1408                error!("hint: use a `Developer ID Application`, `Developer ID Installer`, or `Developer ID Kernel` certificate");
1409                have_error = true;
1410            }
1411
1412            if self.time_stamp_url().is_none() {
1413                error!("--for-notarization requires use of a time-stamp protocol server; none configured");
1414                have_error = true;
1415            }
1416        } else {
1417            error!("--for-notarization requires use of a Developer ID signing certificate; no signing certificate was provided");
1418            have_error = true;
1419        }
1420
1421        if have_error {
1422            Err(AppleCodesignError::ForNotarizationInvalidSettings)
1423        } else {
1424            Ok(())
1425        }
1426    }
1427}
1428
1429#[cfg(test)]
1430mod tests {
1431    use {super::*, indoc::indoc};
1432
1433    const ENTITLEMENTS_XML: &str = indoc! {r#"
1434        <?xml version="1.0" encoding="UTF-8"?>
1435        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1436        <plist version="1.0">
1437        <dict>
1438            <key>application-identifier</key>
1439            <string>appid</string>
1440            <key>com.apple.developer.team-identifier</key>
1441            <string>ABCDEF</string>
1442        </dict>
1443        </plist>
1444    "#};
1445
1446    #[test]
1447    fn parse_settings_scope() {
1448        assert_eq!(
1449            SettingsScope::try_from("@main").unwrap(),
1450            SettingsScope::Main
1451        );
1452        assert_eq!(
1453            SettingsScope::try_from("@0").unwrap(),
1454            SettingsScope::MultiArchIndex(0)
1455        );
1456        assert_eq!(
1457            SettingsScope::try_from("@42").unwrap(),
1458            SettingsScope::MultiArchIndex(42)
1459        );
1460        assert_eq!(
1461            SettingsScope::try_from("@[cpu_type=7]").unwrap(),
1462            SettingsScope::MultiArchCpuType(7)
1463        );
1464        assert_eq!(
1465            SettingsScope::try_from("@[cpu_type=arm]").unwrap(),
1466            SettingsScope::MultiArchCpuType(CPU_TYPE_ARM)
1467        );
1468        assert_eq!(
1469            SettingsScope::try_from("@[cpu_type=arm64]").unwrap(),
1470            SettingsScope::MultiArchCpuType(CPU_TYPE_ARM64)
1471        );
1472        assert_eq!(
1473            SettingsScope::try_from("@[cpu_type=arm64_32]").unwrap(),
1474            SettingsScope::MultiArchCpuType(CPU_TYPE_ARM64_32)
1475        );
1476        assert_eq!(
1477            SettingsScope::try_from("@[cpu_type=x86_64]").unwrap(),
1478            SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64)
1479        );
1480        assert_eq!(
1481            SettingsScope::try_from("foo/bar").unwrap(),
1482            SettingsScope::Path("foo/bar".into())
1483        );
1484        assert_eq!(
1485            SettingsScope::try_from("foo/bar@0").unwrap(),
1486            SettingsScope::PathMultiArchIndex("foo/bar".into(), 0)
1487        );
1488        assert_eq!(
1489            SettingsScope::try_from("foo/bar@[cpu_type=7]").unwrap(),
1490            SettingsScope::PathMultiArchCpuType("foo/bar".into(), 7_u32)
1491        );
1492    }
1493
1494    #[test]
1495    fn as_nested_macho_settings() {
1496        let mut main_settings = SigningSettings::default();
1497        main_settings.set_binary_identifier(SettingsScope::Main, "ident");
1498        main_settings
1499            .set_code_signature_flags(SettingsScope::Main, CodeSignatureFlags::FORCE_EXPIRATION);
1500
1501        main_settings.set_code_signature_flags(
1502            SettingsScope::MultiArchIndex(0),
1503            CodeSignatureFlags::FORCE_HARD,
1504        );
1505        main_settings.set_code_signature_flags(
1506            SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
1507            CodeSignatureFlags::RESTRICT,
1508        );
1509        main_settings.set_info_plist_data(SettingsScope::MultiArchIndex(0), b"index_0".to_vec());
1510        main_settings.set_info_plist_data(
1511            SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
1512            b"cpu_x86_64".to_vec(),
1513        );
1514
1515        let macho_settings = main_settings.as_universal_macho_settings(0, CPU_TYPE_ARM64);
1516        assert_eq!(
1517            macho_settings.binary_identifier(SettingsScope::Main),
1518            Some("ident")
1519        );
1520        assert_eq!(
1521            macho_settings.code_signature_flags(SettingsScope::Main),
1522            Some(CodeSignatureFlags::FORCE_HARD)
1523        );
1524        assert_eq!(
1525            macho_settings.info_plist_data(SettingsScope::Main),
1526            Some(b"index_0".as_ref())
1527        );
1528
1529        let macho_settings = main_settings.as_universal_macho_settings(0, CPU_TYPE_X86_64);
1530        assert_eq!(
1531            macho_settings.binary_identifier(SettingsScope::Main),
1532            Some("ident")
1533        );
1534        assert_eq!(
1535            macho_settings.code_signature_flags(SettingsScope::Main),
1536            Some(CodeSignatureFlags::RESTRICT)
1537        );
1538        assert_eq!(
1539            macho_settings.info_plist_data(SettingsScope::Main),
1540            Some(b"cpu_x86_64".as_ref())
1541        );
1542    }
1543
1544    #[test]
1545    fn as_bundle_macho_settings() {
1546        let mut main_settings = SigningSettings::default();
1547        main_settings.set_info_plist_data(SettingsScope::Main, b"main".to_vec());
1548        main_settings.set_info_plist_data(
1549            SettingsScope::Path("Contents/MacOS/main".into()),
1550            b"main_exe".to_vec(),
1551        );
1552        main_settings.set_info_plist_data(
1553            SettingsScope::PathMultiArchIndex("Contents/MacOS/main".into(), 0),
1554            b"main_exe_index_0".to_vec(),
1555        );
1556        main_settings.set_info_plist_data(
1557            SettingsScope::PathMultiArchCpuType("Contents/MacOS/main".into(), CPU_TYPE_X86_64),
1558            b"main_exe_x86_64".to_vec(),
1559        );
1560
1561        let macho_settings = main_settings.as_bundle_macho_settings("Contents/MacOS/main");
1562        assert_eq!(
1563            macho_settings.info_plist_data(SettingsScope::Main),
1564            Some(b"main_exe".as_ref())
1565        );
1566        assert_eq!(
1567            macho_settings.info_plist_data,
1568            [
1569                (SettingsScope::Main, b"main_exe".to_vec()),
1570                (
1571                    SettingsScope::MultiArchIndex(0),
1572                    b"main_exe_index_0".to_vec()
1573                ),
1574                (
1575                    SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
1576                    b"main_exe_x86_64".to_vec()
1577                ),
1578            ]
1579            .iter()
1580            .cloned()
1581            .collect::<BTreeMap<SettingsScope, Vec<u8>>>()
1582        );
1583    }
1584
1585    #[test]
1586    fn as_nested_bundle_settings() {
1587        let mut main_settings = SigningSettings::default();
1588        main_settings.set_info_plist_data(SettingsScope::Main, b"main".to_vec());
1589        main_settings.set_info_plist_data(
1590            SettingsScope::Path("Contents/MacOS/main".into()),
1591            b"main_exe".to_vec(),
1592        );
1593        main_settings.set_info_plist_data(
1594            SettingsScope::Path("Contents/MacOS/nested.app".into()),
1595            b"bundle".to_vec(),
1596        );
1597        main_settings.set_info_plist_data(
1598            SettingsScope::PathMultiArchIndex("Contents/MacOS/nested.app".into(), 0),
1599            b"bundle_index_0".to_vec(),
1600        );
1601        main_settings.set_info_plist_data(
1602            SettingsScope::PathMultiArchCpuType(
1603                "Contents/MacOS/nested.app".into(),
1604                CPU_TYPE_X86_64,
1605            ),
1606            b"bundle_x86_64".to_vec(),
1607        );
1608        main_settings.set_info_plist_data(
1609            SettingsScope::Path("Contents/MacOS/nested.app/Contents/MacOS/nested".into()),
1610            b"nested_main_exe".to_vec(),
1611        );
1612        main_settings.set_info_plist_data(
1613            SettingsScope::PathMultiArchIndex(
1614                "Contents/MacOS/nested.app/Contents/MacOS/nested".into(),
1615                0,
1616            ),
1617            b"nested_main_exe_index_0".to_vec(),
1618        );
1619        main_settings.set_info_plist_data(
1620            SettingsScope::PathMultiArchCpuType(
1621                "Contents/MacOS/nested.app/Contents/MacOS/nested".into(),
1622                CPU_TYPE_X86_64,
1623            ),
1624            b"nested_main_exe_x86_64".to_vec(),
1625        );
1626
1627        let bundle_settings = main_settings.as_nested_bundle_settings("Contents/MacOS/nested.app");
1628        assert_eq!(
1629            bundle_settings.info_plist_data(SettingsScope::Main),
1630            Some(b"bundle".as_ref())
1631        );
1632        assert_eq!(
1633            bundle_settings.info_plist_data(SettingsScope::Path("Contents/MacOS/nested".into())),
1634            Some(b"nested_main_exe".as_ref())
1635        );
1636        assert_eq!(
1637            bundle_settings.info_plist_data,
1638            [
1639                (SettingsScope::Main, b"bundle".to_vec()),
1640                (SettingsScope::MultiArchIndex(0), b"bundle_index_0".to_vec()),
1641                (
1642                    SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
1643                    b"bundle_x86_64".to_vec()
1644                ),
1645                (
1646                    SettingsScope::Path("Contents/MacOS/nested".into()),
1647                    b"nested_main_exe".to_vec()
1648                ),
1649                (
1650                    SettingsScope::PathMultiArchIndex("Contents/MacOS/nested".into(), 0),
1651                    b"nested_main_exe_index_0".to_vec()
1652                ),
1653                (
1654                    SettingsScope::PathMultiArchCpuType(
1655                        "Contents/MacOS/nested".into(),
1656                        CPU_TYPE_X86_64
1657                    ),
1658                    b"nested_main_exe_x86_64".to_vec()
1659                ),
1660            ]
1661            .iter()
1662            .cloned()
1663            .collect::<BTreeMap<SettingsScope, Vec<u8>>>()
1664        );
1665    }
1666
1667    #[test]
1668    fn entitlements_handling() -> Result<(), AppleCodesignError> {
1669        let mut settings = SigningSettings::default();
1670        settings.set_entitlements_xml(SettingsScope::Main, ENTITLEMENTS_XML)?;
1671
1672        let s = settings.entitlements_xml(SettingsScope::Main)?;
1673        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()));
1674
1675        Ok(())
1676    }
1677
1678    #[test]
1679    fn for_notarization_handling() -> Result<(), AppleCodesignError> {
1680        let mut settings = SigningSettings::default();
1681        settings.set_for_notarization(true);
1682
1683        assert_eq!(
1684            settings.code_signature_flags(SettingsScope::Main),
1685            Some(CodeSignatureFlags::RUNTIME)
1686        );
1687
1688        assert_eq!(
1689            settings
1690                .as_bundle_macho_settings("")
1691                .code_signature_flags(SettingsScope::Main),
1692            Some(CodeSignatureFlags::RUNTIME)
1693        );
1694
1695        Ok(())
1696    }
1697}