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