apple_codesign/
policy.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//! Apple trust policies.
6//!
7//! Apple operating systems have a number of pre-canned trust policies
8//! that must be fulfilled in order to trust signed code. These are
9//! often based off the presence of specific X.509 certificates in the
10//! issuing chain and/or the presence of attributes in X.509 certificates.
11//!
12//! Trust policies are often engraved in code signatures as part of the
13//! signed code requirements expression.
14//!
15//! This module defines a bunch of metadata for describing Apple trust
16//! entities and also provides pre-canned policies that can be easily
17//! constructed to match those employed by Apple's official signing tools.
18//!
19//! Apple's certificates can be found at
20//! <https://www.apple.com/certificateauthority/>.
21
22use {
23    crate::{
24        certificate::{
25            AppleCertificate, CertificateAuthorityExtension, CodeSigningCertificateExtension,
26        },
27        code_requirement::{CodeRequirementExpression, CodeRequirementMatchExpression},
28        error::AppleCodesignError,
29    },
30    once_cell::sync::Lazy,
31    std::ops::Deref,
32    x509_certificate::CapturedX509Certificate,
33};
34
35/// Code signing requirement for Mac Developer ID.
36///
37/// `anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and
38/// (certificate leaf[field.1.2.840.113635.100.6.1.14] or certificate leaf[field.1.2.840.113635.100.6.1.13])`
39static POLICY_MAC_DEVELOPER_ID: Lazy<CodeRequirementExpression<'static>> = Lazy::new(|| {
40    CodeRequirementExpression::And(
41        Box::new(CodeRequirementExpression::And(
42            Box::new(CodeRequirementExpression::AnchorAppleGeneric),
43            Box::new(CodeRequirementExpression::CertificateGeneric(
44                1,
45                CertificateAuthorityExtension::DeveloperId.as_oid(),
46                CodeRequirementMatchExpression::Exists,
47            )),
48        )),
49        Box::new(CodeRequirementExpression::Or(
50            Box::new(CodeRequirementExpression::CertificateGeneric(
51                0,
52                CodeSigningCertificateExtension::DeveloperIdInstaller.as_oid(),
53                CodeRequirementMatchExpression::Exists,
54            )),
55            Box::new(CodeRequirementExpression::CertificateGeneric(
56                0,
57                CodeSigningCertificateExtension::DeveloperIdApplication.as_oid(),
58                CodeRequirementMatchExpression::Exists,
59            )),
60        )),
61    )
62});
63
64/// Notarized executable.
65///
66/// `anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and
67/// certificate leaf[field.1.2.840.113635.100.6.1.13] exists and notarized'`
68///
69static POLICY_NOTARIZED_EXECUTABLE: Lazy<CodeRequirementExpression<'static>> = Lazy::new(|| {
70    CodeRequirementExpression::And(
71        Box::new(CodeRequirementExpression::And(
72            Box::new(CodeRequirementExpression::And(
73                Box::new(CodeRequirementExpression::AnchorAppleGeneric),
74                Box::new(CodeRequirementExpression::CertificateGeneric(
75                    1,
76                    CertificateAuthorityExtension::DeveloperId.as_oid(),
77                    CodeRequirementMatchExpression::Exists,
78                )),
79            )),
80            Box::new(CodeRequirementExpression::CertificateGeneric(
81                0,
82                CodeSigningCertificateExtension::DeveloperIdApplication.as_oid(),
83                CodeRequirementMatchExpression::Exists,
84            )),
85        )),
86        Box::new(CodeRequirementExpression::Notarized),
87    )
88});
89
90/// Notarized installer.
91///
92/// `'anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists
93/// and (certificate leaf[field.1.2.840.113635.100.6.1.14] or certificate
94/// leaf[field.1.2.840.113635.100.6.1.13]) and notarized'`
95static POLICY_NOTARIZED_INSTALLER: Lazy<CodeRequirementExpression<'static>> = Lazy::new(|| {
96    CodeRequirementExpression::And(
97        Box::new(CodeRequirementExpression::And(
98            Box::new(CodeRequirementExpression::And(
99                Box::new(CodeRequirementExpression::AnchorAppleGeneric),
100                Box::new(CodeRequirementExpression::CertificateGeneric(
101                    1,
102                    CertificateAuthorityExtension::DeveloperId.as_oid(),
103                    CodeRequirementMatchExpression::Exists,
104                )),
105            )),
106            Box::new(CodeRequirementExpression::Or(
107                Box::new(CodeRequirementExpression::CertificateGeneric(
108                    0,
109                    CodeSigningCertificateExtension::DeveloperIdInstaller.as_oid(),
110                    CodeRequirementMatchExpression::Exists,
111                )),
112                Box::new(CodeRequirementExpression::CertificateGeneric(
113                    0,
114                    CodeSigningCertificateExtension::DeveloperIdApplication.as_oid(),
115                    CodeRequirementMatchExpression::Exists,
116                )),
117            )),
118        )),
119        Box::new(CodeRequirementExpression::Notarized),
120    )
121});
122
123/// Defines well-known execution policies for signed code.
124///
125/// Instances can be obtained from a human-readable string for convenience. Those
126/// strings are:
127///
128/// * `developer-id-signed`
129/// * `developer-id-notarized-executable`
130/// * `developer-id-notarized-installer`
131#[allow(clippy::enum_variant_names)]
132#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, clap::ValueEnum)]
133pub enum ExecutionPolicy {
134    /// Code is signed by a certificate authorized for signing Mac applications or
135    /// installers and that certificate was issued by
136    /// [crate::apple_certificates::KnownCertificate::DeveloperIdG1] or
137    /// [crate::apple_certificates::KnownCertificate::DeveloperIdG2].
138    ///
139    /// This is the policy that applies when you get a `Developer ID Application` or
140    /// `Developer ID Installer` certificate from Apple.
141    DeveloperIdSigned,
142
143    /// Like [Self::DeveloperIdSigned] but only applies to executables (not installers)
144    /// and the executable must be notarized.
145    ///
146    /// If you notarize an individual executable, you effectively convert the
147    /// [Self::DeveloperIdSigned] policy into this variant.
148    DeveloperIdNotarizedExecutable,
149
150    /// Like [Self::DeveloperIdSigned] but only applies to installers (not executables)
151    /// and the installer must be notarized.
152    ///
153    /// If you notarize an individual installer, you effectively convert the
154    /// [Self::DeveloperIdSigned] policy into this variant.
155    DeveloperIdNotarizedInstaller,
156}
157
158impl Deref for ExecutionPolicy {
159    type Target = CodeRequirementExpression<'static>;
160
161    fn deref(&self) -> &Self::Target {
162        match self {
163            Self::DeveloperIdSigned => POLICY_MAC_DEVELOPER_ID.deref(),
164            Self::DeveloperIdNotarizedExecutable => POLICY_NOTARIZED_EXECUTABLE.deref(),
165            Self::DeveloperIdNotarizedInstaller => POLICY_NOTARIZED_INSTALLER.deref(),
166        }
167    }
168}
169
170impl TryFrom<&str> for ExecutionPolicy {
171    type Error = AppleCodesignError;
172
173    fn try_from(s: &str) -> Result<Self, Self::Error> {
174        match s {
175            "developer-id-signed" => Ok(Self::DeveloperIdSigned),
176            "developer-id-notarized-executable" => Ok(Self::DeveloperIdNotarizedExecutable),
177            "developer-id-notarized-installer" => Ok(Self::DeveloperIdNotarizedInstaller),
178            _ => Err(AppleCodesignError::UnknownPolicy(s.to_string())),
179        }
180    }
181}
182
183/// Derive a designated requirements expression given a code signing certificate.
184///
185/// The default expression is derived from properties of the signing
186/// certificate. If it is an Apple signed certificate, extensions on the
187/// issuer CA denote which expression to use.
188///
189/// For non-Apple signed certificates, the expression self-references the
190/// issuing certificate in the same Organization as the signing certificate.
191pub fn derive_designated_requirements(
192    signing_cert: &CapturedX509Certificate,
193    chain: &[CapturedX509Certificate],
194    identifier: Option<String>,
195) -> Result<CodeRequirementExpression<'static>, AppleCodesignError> {
196    let expr = if signing_cert.chains_to_apple_root_ca() {
197        let apple_chain = signing_cert.apple_issuing_chain();
198
199        assert!(
200            !apple_chain.is_empty(),
201            "we should be able to resolve the Apple CA chain if chains_to_apple_root_ca() is true"
202        );
203
204        let first = apple_chain[0];
205
206        if first
207            .apple_ca_extensions()
208            .into_iter()
209            .any(|ext| ext == CertificateAuthorityExtension::AppleWorldwideDeveloperRelations)
210        {
211            let cn = signing_cert.subject_common_name().ok_or_else(|| {
212                AppleCodesignError::PolicyFormulationError(
213                    "certificate common name not available".to_string(),
214                )
215            })?;
216            worldwide_developer_relations_signed_expression(cn)
217        } else if first
218            .apple_ca_extensions()
219            .into_iter()
220            .any(|ext| ext == CertificateAuthorityExtension::DeveloperId)
221        {
222            let team_id = signing_cert.apple_team_id().ok_or_else(|| {
223                AppleCodesignError::PolicyFormulationError(
224                    "could not find team identifier in signing certificate".to_string(),
225                )
226            })?;
227
228            developer_id_signed_expression(team_id)
229        } else {
230            CodeRequirementExpression::AnchorApple
231        }
232    } else {
233        // Ensure the chain is sorted.
234        let chain = signing_cert
235            .resolve_signing_chain(chain.iter())
236            .into_iter()
237            .cloned()
238            .collect::<Vec<_>>();
239
240        non_apple_signed_expression(signing_cert, &chain)?
241    };
242
243    // Chain the expression with the identifier, if given.
244    Ok(if let Some(identifier) = identifier {
245        CodeRequirementExpression::And(
246            Box::new(CodeRequirementExpression::Identifier(identifier.into())),
247            Box::new(expr),
248        )
249    } else {
250        expr
251    })
252}
253
254/// Derive a code requirements expression for a Developer ID issued certificate.
255///
256/// The expression is pinned to the team ID / organization unit of the signing
257/// certificate, which must be passed in.
258pub fn developer_id_signed_expression(
259    team_id: impl ToString,
260) -> CodeRequirementExpression<'static> {
261    CodeRequirementExpression::And(
262        // Chains to Apple root CA.
263        Box::new(CodeRequirementExpression::AnchorAppleGeneric),
264        Box::new(CodeRequirementExpression::And(
265            // Certificate issued by CA with Developer ID extension.
266            Box::new(CodeRequirementExpression::CertificateGeneric(
267                1,
268                CertificateAuthorityExtension::DeveloperId.as_oid(),
269                CodeRequirementMatchExpression::Exists,
270            )),
271            Box::new(CodeRequirementExpression::And(
272                // A certificate entrusted with Developer ID Application signing rights.
273                Box::new(CodeRequirementExpression::CertificateGeneric(
274                    0,
275                    CodeSigningCertificateExtension::DeveloperIdApplication.as_oid(),
276                    CodeRequirementMatchExpression::Exists,
277                )),
278                // Signed by this team ID.
279                Box::new(CodeRequirementExpression::CertificateField(
280                    0,
281                    "subject.OU".to_string().into(),
282                    CodeRequirementMatchExpression::Equal(team_id.to_string().into()),
283                )),
284            )),
285        )),
286    )
287}
288
289/// Derive the requirements expression for a cert signed by the Worldwide Developer Relations CA.
290///
291/// The expression is pinned to the Common Name (CN) field of the signing
292/// certificate, which must be passed in.
293pub fn worldwide_developer_relations_signed_expression(
294    leaf_common_name: impl ToString,
295) -> CodeRequirementExpression<'static> {
296    // anchor apple generic and
297    CodeRequirementExpression::And(
298        Box::new(CodeRequirementExpression::AnchorAppleGeneric),
299        // leaf[subject.CN] = <leaf subject> and
300        Box::new(CodeRequirementExpression::And(
301            Box::new(CodeRequirementExpression::CertificateField(
302                0,
303                "subject.CN".to_string().into(),
304                CodeRequirementMatchExpression::Equal(leaf_common_name.to_string().into()),
305            )),
306            // certificate 1[field.1.2.840.113635.100.6.2.1] exists
307            Box::new(CodeRequirementExpression::CertificateGeneric(
308                1,
309                CertificateAuthorityExtension::AppleWorldwideDeveloperRelations.as_oid(),
310                CodeRequirementMatchExpression::Exists,
311            )),
312        )),
313    )
314}
315
316/// Derive the requirements expression for non Apple signed certificates.
317///
318/// The signing certificate should be the first certificate in the passed chain.
319/// The chain should be sorted so the root CA is last.
320pub fn non_apple_signed_expression(
321    signing_cert: &CapturedX509Certificate,
322    chain: &[CapturedX509Certificate],
323) -> Result<CodeRequirementExpression<'static>, AppleCodesignError> {
324    let leaf_raw: &x509_certificate::rfc5280::Certificate = signing_cert.as_ref();
325
326    let leaf_organization = leaf_raw
327        .tbs_certificate
328        .subject
329        .iter_organization()
330        .next()
331        .and_then(|o| o.to_string().ok());
332
333    // We pin the last certificate in the signing chain having the same
334    // organization as the signing certificate.
335
336    let mut pin_index = 0i32;
337
338    if let Some(leaf_organization) = leaf_organization {
339        for cert in chain.iter() {
340            let ca_raw: &x509_certificate::rfc5280::Certificate = cert.as_ref();
341
342            if let Some(org) = ca_raw
343                .tbs_certificate
344                .subject
345                .iter_organization()
346                .next()
347                .and_then(|o| o.to_string().ok())
348            {
349                if org != leaf_organization {
350                    break;
351                }
352
353                pin_index += 1;
354            }
355        }
356    }
357
358    // If the entire chain is signed by the same Organization, use the
359    // special cert index value to pin the root cert.
360    if pin_index as usize == chain.len() {
361        pin_index = -1;
362    }
363
364    let digest = signing_cert
365        .fingerprint(x509_certificate::DigestAlgorithm::Sha1)?
366        .as_ref()
367        .to_vec();
368
369    Ok(CodeRequirementExpression::AnchorCertificateHash(
370        pin_index,
371        digest.into(),
372    ))
373}
374
375#[cfg(test)]
376mod test {
377    use super::*;
378
379    #[test]
380    fn get_policies() {
381        ExecutionPolicy::DeveloperIdSigned.to_bytes().unwrap();
382        ExecutionPolicy::DeveloperIdNotarizedExecutable
383            .to_bytes()
384            .unwrap();
385        ExecutionPolicy::DeveloperIdNotarizedInstaller
386            .to_bytes()
387            .unwrap();
388    }
389
390    const APPLE_SIGNED_CN: &str = "Apple Development: Gregory Szorc (DD5YMVP48D)";
391    const DEVELOPER_ID_TEXT: &str = "(anchor apple generic) and ((certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */) and ((certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */) and (certificate leaf[subject.OU] = \"MK22MZP987\")))";
392    const WWDR_TEXT: &str = "(anchor apple generic) and ((certificate leaf[subject.CN] = \"Apple Development: Gregory Szorc (DD5YMVP48D)\") and (certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */))";
393
394    fn load_unified_pem(pem_data: &[u8]) -> CapturedX509Certificate {
395        pem::parse_many(pem_data)
396            .unwrap()
397            .into_iter()
398            .filter_map(|doc| {
399                if doc.tag() == "CERTIFICATE" {
400                    Some(doc.contents().to_vec())
401                } else {
402                    None
403                }
404            })
405            .map(|der| CapturedX509Certificate::from_der(der).unwrap())
406            .next()
407            .unwrap()
408    }
409
410    #[test]
411    fn developer_id_requirements_derive() {
412        let der = include_bytes!("testdata/apple-signed-developer-id-application.cer");
413        let cert = CapturedX509Certificate::from_der(der.to_vec()).unwrap();
414
415        assert_eq!(
416            developer_id_signed_expression(cert.apple_team_id().unwrap()).to_string(),
417            DEVELOPER_ID_TEXT
418        );
419        assert_eq!(
420            derive_designated_requirements(&cert, &[], None)
421                .unwrap()
422                .to_string(),
423            DEVELOPER_ID_TEXT
424        );
425    }
426
427    #[test]
428    fn worldwide_developer_relations() {
429        assert_eq!(
430            worldwide_developer_relations_signed_expression(APPLE_SIGNED_CN).to_string(),
431            WWDR_TEXT
432        );
433    }
434
435    #[test]
436    fn non_apple_signed() {
437        let self_signed = load_unified_pem(include_bytes!(
438            "testdata/self-signed-rsa-apple-development.pem"
439        ));
440
441        assert_eq!(
442            non_apple_signed_expression(&self_signed, &[])
443                .unwrap()
444                .to_string(),
445            "certificate root = H\"e1c7216e46533c923b7cfc94e86c7043790b96e9\""
446        );
447
448        // Now try with an Apple chain. The function doesn't care that it is
449        // operating on a non-Apple chain.
450        let apple_development = CapturedX509Certificate::from_der(
451            include_bytes!("testdata/apple-signed-apple-development.cer").to_vec(),
452        )
453        .unwrap();
454        let chain = apple_development.apple_root_certificate_chain().unwrap();
455
456        assert_eq!(
457            non_apple_signed_expression(&apple_development, &chain[1..])
458                .unwrap()
459                .to_string(),
460            "certificate leaf = H\"5eeadb4befce055e06b4239ad4c5f0d1bfd6af8f\""
461        );
462    }
463
464    #[test]
465    fn apple_signed_auto_derive() {
466        let apple_development = CapturedX509Certificate::from_der(
467            include_bytes!("testdata/apple-signed-apple-development.cer").to_vec(),
468        )
469        .unwrap();
470        let apple_distribution = CapturedX509Certificate::from_der(
471            include_bytes!("testdata/apple-signed-apple-distribution.cer").to_vec(),
472        )
473        .unwrap();
474        let developer_id_application = CapturedX509Certificate::from_der(
475            include_bytes!("testdata/apple-signed-developer-id-application.cer").to_vec(),
476        )
477        .unwrap();
478        let developer_id_installer = CapturedX509Certificate::from_der(
479            include_bytes!("testdata/apple-signed-developer-id-installer.cer").to_vec(),
480        )
481        .unwrap();
482        let mac_installer_distribution = CapturedX509Certificate::from_der(
483            include_bytes!("testdata/apple-signed-3rd-party-mac.cer").to_vec(),
484        )
485        .unwrap();
486
487        assert_eq!(
488            derive_designated_requirements(&apple_development, &[], None)
489                .unwrap()
490                .to_string(),
491            WWDR_TEXT
492        );
493        assert_eq!(
494            derive_designated_requirements(&apple_distribution, &[], None)
495                .unwrap()
496                .to_string(),
497            worldwide_developer_relations_signed_expression(
498                "Apple Distribution: Gregory Szorc (MK22MZP987)"
499            )
500            .to_string()
501        );
502        assert_eq!(
503            derive_designated_requirements(&developer_id_application, &[], None)
504                .unwrap()
505                .to_string(),
506            DEVELOPER_ID_TEXT
507        );
508        assert_eq!(
509            derive_designated_requirements(&developer_id_installer, &[], None)
510                .unwrap()
511                .to_string(),
512            developer_id_signed_expression("MK22MZP987").to_string()
513        );
514        assert_eq!(
515            derive_designated_requirements(&mac_installer_distribution, &[], None)
516                .unwrap()
517                .to_string(),
518            worldwide_developer_relations_signed_expression(
519                "3rd Party Mac Developer Installer: Gregory Szorc (MK22MZP987)"
520            )
521            .to_string()
522        );
523    }
524
525    #[test]
526    fn self_signed_auto_derive() {
527        let apple_development = load_unified_pem(include_bytes!(
528            "testdata/self-signed-rsa-apple-development.pem"
529        ));
530        let apple_distribution = load_unified_pem(include_bytes!(
531            "testdata/self-signed-rsa-apple-distribution.pem"
532        ));
533        let developer_id_application = load_unified_pem(include_bytes!(
534            "testdata/self-signed-rsa-developer-id-application.pem"
535        ));
536        let developer_id_installer = load_unified_pem(include_bytes!(
537            "testdata/self-signed-rsa-developer-id-installer.pem"
538        ));
539        let mac_installer_distribution = load_unified_pem(include_bytes!(
540            "testdata/self-signed-rsa-mac-installer-distribution.pem"
541        ));
542
543        let derive = |cert| -> String {
544            derive_designated_requirements(cert, &[], None)
545                .unwrap()
546                .to_string()
547        };
548
549        assert_eq!(
550            derive(&apple_development),
551            "certificate root = H\"e1c7216e46533c923b7cfc94e86c7043790b96e9\""
552        );
553        assert_eq!(
554            derive(&apple_distribution),
555            "certificate root = H\"0383efdf909250708bf2de4d43753836ccb3d608\""
556        );
557        assert_eq!(
558            derive(&developer_id_application),
559            "certificate root = H\"3acf1d302fe3a4bba06a3c16aadc908045bc9162\""
560        );
561        assert_eq!(
562            derive(&developer_id_installer),
563            "certificate root = H\"5c1314a89e5a486ac7b1da86b38e08777adca4af\""
564        );
565        assert_eq!(
566            derive(&mac_installer_distribution),
567            "certificate root = H\"58e39fe0fca55e7af4ca00027bc7c59e566e960a\""
568        );
569    }
570}