Skip to main content

rustferry_apple/
signing_assets.rs

1//! Cross-platform validation for user-supplied Apple manual-signing assets.
2
3use std::{
4    cmp::Ordering,
5    error::Error,
6    fmt, str,
7    time::{SystemTime, UNIX_EPOCH},
8};
9
10use openssl::{
11    asn1::{Asn1Time, Asn1TimeRef},
12    cms::{CMSOptions, CmsContentInfo},
13    hash::MessageDigest,
14    nid::Nid,
15    pkcs12::Pkcs12,
16    pkey::{PKeyRef, Private},
17    provider::Provider,
18    stack::{Stack, StackRef},
19    x509::{
20        X509, X509PurposeId, X509StoreContext,
21        store::{X509Store, X509StoreBuilder},
22        verify::X509VerifyParam,
23    },
24};
25use rustferry_remote::{
26    DevelopmentTeam, ProvisioningProfile, ProvisioningProfileParseError, SecretBytes,
27    SigningCertificate, parse_decoded_provisioning_profile,
28};
29
30/// Maximum accepted PKCS#12 archive size.
31pub const MAX_MANUAL_SIGNING_PKCS12_BYTES: usize = 64 * 1024 * 1024;
32/// Maximum accepted PKCS#12 password size.
33pub const MAX_MANUAL_SIGNING_PASSWORD_BYTES: usize = 4 * 1024;
34/// Maximum accepted CMS provisioning-profile size.
35pub const MAX_MANUAL_SIGNING_PROFILE_BYTES: usize = 8 * 1024 * 1024;
36
37/// SHA-256 fingerprint of Apple's original Apple Root CA.
38pub const APPLE_ROOT_CA_SHA256: &str =
39    "B0B1730ECBC7FF4505142C49F1295E6EDA6BCAED7E2C68C5BE91B5A11001F024";
40/// SHA-256 fingerprint of Apple Root CA - G2.
41pub const APPLE_ROOT_CA_G2_SHA256: &str =
42    "C2B9B042DD57830E7D117DAC55AC8AE19407D38E41D88F3215BC3A890444A050";
43/// SHA-256 fingerprint of Apple Root CA - G3.
44pub const APPLE_ROOT_CA_G3_SHA256: &str =
45    "63343ABFB89A6A03EBB57E9B3F5FA7BE7C4F5C756F3017B3A8C488C3653E9179";
46
47const APPLE_DEVELOPMENT_PREFIX: &str = "Apple Development: ";
48const IPHONE_DEVELOPER_PREFIX: &str = "iPhone Developer: ";
49const MAX_CERTIFICATE_CHAIN_DEPTH: i32 = 8;
50
51const APPLE_ROOT_CA_PEM: &[u8] = include_bytes!("../assets/apple-root-ca.crt");
52const APPLE_ROOT_CA_G2_PEM: &[u8] = include_bytes!("../assets/apple-root-ca-g2.crt");
53const APPLE_ROOT_CA_G3_PEM: &[u8] = include_bytes!("../assets/apple-root-ca-g3.crt");
54
55const PRODUCTION_TRUST_ANCHORS: [TrustAnchor<'static>; 3] = [
56    TrustAnchor {
57        pem: APPLE_ROOT_CA_PEM,
58        sha256: APPLE_ROOT_CA_SHA256,
59    },
60    TrustAnchor {
61        pem: APPLE_ROOT_CA_G2_PEM,
62        sha256: APPLE_ROOT_CA_G2_SHA256,
63    },
64    TrustAnchor {
65        pem: APPLE_ROOT_CA_G3_PEM,
66        sha256: APPLE_ROOT_CA_G3_SHA256,
67    },
68];
69
70/// One secret input field rejected before cryptographic parsing.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum ManualSigningAssetField {
73    /// PKCS#12 archive.
74    CertificatePkcs12,
75    /// PKCS#12 password.
76    CertificatePassword,
77    /// CMS provisioning profile.
78    ProvisioningProfile,
79}
80
81impl fmt::Display for ManualSigningAssetField {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        formatter.write_str(match self {
84            Self::CertificatePkcs12 => "certificate PKCS#12",
85            Self::CertificatePassword => "certificate password",
86            Self::ProvisioningProfile => "provisioning profile",
87        })
88    }
89}
90
91/// Structural reason an input was rejected before cryptographic parsing.
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum ManualSigningAssetInputError {
94    /// Required binary input was empty.
95    Empty,
96    /// Input exceeded its fixed byte limit.
97    TooLarge,
98    /// Password was not valid UTF-8.
99    PasswordEncoding,
100    /// Password contained a NUL byte unsupported by OpenSSL's string API.
101    PasswordContainsNul,
102}
103
104impl fmt::Display for ManualSigningAssetInputError {
105    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        formatter.write_str(match self {
107            Self::Empty => "is empty",
108            Self::TooLarge => "exceeds its byte limit",
109            Self::PasswordEncoding => "is not UTF-8",
110            Self::PasswordContainsNul => "contains a NUL byte",
111        })
112    }
113}
114
115/// Secret, bounded inputs for one manual-signing identity and profile.
116///
117/// This type deliberately implements neither `Clone`, `Debug`, serialization,
118/// nor field accessors. All three byte buffers are overwritten by
119/// [`SecretBytes`] when cleared or dropped.
120pub struct ManualSigningAssetsInput {
121    certificate_pkcs12: SecretBytes,
122    certificate_password: SecretBytes,
123    provisioning_profile: SecretBytes,
124}
125
126/// Secret, bounded PKCS#12 identity input retained for one remote upload.
127///
128/// This separates the identity from target profiles so multi-target validation
129/// never duplicates the private key or password in memory.
130pub struct ManualSigningIdentityInput {
131    certificate_pkcs12: SecretBytes,
132    certificate_password: SecretBytes,
133}
134
135impl ManualSigningIdentityInput {
136    /// Validate structural bounds and take ownership of one signing identity.
137    ///
138    /// # Errors
139    ///
140    /// Returns a field-only error without retaining caller-controlled content.
141    pub fn new(
142        certificate_pkcs12: SecretBytes,
143        certificate_password: SecretBytes,
144    ) -> Result<Self, ManualSigningAssetError> {
145        validate_identity_input(&certificate_pkcs12, &certificate_password)?;
146        Ok(Self {
147            certificate_pkcs12,
148            certificate_password,
149        })
150    }
151
152    /// Consume the identity into its exact secret buffers.
153    pub fn into_parts(self) -> (SecretBytes, SecretBytes) {
154        (self.certificate_pkcs12, self.certificate_password)
155    }
156}
157
158impl ManualSigningAssetsInput {
159    /// Validate input bounds and take ownership of all secret buffers.
160    ///
161    /// Empty PKCS#12 passwords are supported.
162    ///
163    /// # Errors
164    ///
165    /// Returns a field-only error for empty, oversized, non-UTF-8, or
166    /// NUL-containing input. No input content is retained in the error.
167    pub fn new(
168        certificate_pkcs12: SecretBytes,
169        certificate_password: SecretBytes,
170        provisioning_profile: SecretBytes,
171    ) -> Result<Self, ManualSigningAssetError> {
172        validate_identity_input(&certificate_pkcs12, &certificate_password)?;
173        validate_nonempty_size(
174            &provisioning_profile,
175            MAX_MANUAL_SIGNING_PROFILE_BYTES,
176            ManualSigningAssetField::ProvisioningProfile,
177        )?;
178        Ok(Self {
179            certificate_pkcs12,
180            certificate_password,
181            provisioning_profile,
182        })
183    }
184
185    /// Consume the input into the exact secret buffers supplied by the caller.
186    ///
187    /// This is intended for forwarding already-validated assets to a remote
188    /// worker without rereading or duplicating them. Each returned buffer
189    /// remains protected by [`SecretBytes`] and is overwritten when dropped.
190    pub fn into_parts(self) -> (SecretBytes, SecretBytes, SecretBytes) {
191        (
192            self.certificate_pkcs12,
193            self.certificate_password,
194            self.provisioning_profile,
195        )
196    }
197}
198
199fn validate_identity_input(
200    certificate_pkcs12: &SecretBytes,
201    certificate_password: &SecretBytes,
202) -> Result<(), ManualSigningAssetError> {
203    validate_nonempty_size(
204        certificate_pkcs12,
205        MAX_MANUAL_SIGNING_PKCS12_BYTES,
206        ManualSigningAssetField::CertificatePkcs12,
207    )?;
208    if certificate_password.len() > MAX_MANUAL_SIGNING_PASSWORD_BYTES {
209        return Err(ManualSigningAssetError::InvalidInput {
210            field: ManualSigningAssetField::CertificatePassword,
211            reason: ManualSigningAssetInputError::TooLarge,
212        });
213    }
214    if str::from_utf8(certificate_password.expose_secret_bytes()).is_err() {
215        return Err(ManualSigningAssetError::InvalidInput {
216            field: ManualSigningAssetField::CertificatePassword,
217            reason: ManualSigningAssetInputError::PasswordEncoding,
218        });
219    }
220    if certificate_password.expose_secret_bytes().contains(&0) {
221        return Err(ManualSigningAssetError::InvalidInput {
222            field: ManualSigningAssetField::CertificatePassword,
223            reason: ManualSigningAssetInputError::PasswordContainsNul,
224        });
225    }
226    Ok(())
227}
228
229/// Public metadata proven by PKCS#12, X.509, CMS, and profile validation.
230#[derive(Clone, Debug, Eq, PartialEq)]
231pub struct ValidatedManualSigningAssets {
232    /// Public signing-certificate metadata. No private key is retained.
233    pub certificate: SigningCertificate,
234    /// Public provisioning-profile metadata. Raw profile bytes are not retained.
235    pub profile: ProvisioningProfile,
236}
237
238/// Secret-free manual-signing asset validation failure.
239#[derive(Clone, Debug, Eq, PartialEq)]
240pub enum ManualSigningAssetError {
241    /// Input failed a fixed structural bound.
242    InvalidInput {
243        /// Input category; never caller data.
244        field: ManualSigningAssetField,
245        /// Structural reason; never caller data.
246        reason: ManualSigningAssetInputError,
247    },
248    /// The system clock cannot supply a supported validation time.
249    Clock,
250    /// Required cryptographic functionality or pinned trust data was unavailable.
251    CryptoUnavailable,
252    /// PKCS#12 bytes were malformed.
253    Pkcs12Malformed,
254    /// PKCS#12 decryption or integrity verification failed.
255    Pkcs12Decrypt,
256    /// PKCS#12 archive contained no private key.
257    MissingPrivateKey,
258    /// PKCS#12 archive contained no certificate corresponding to its private key.
259    MissingCertificate,
260    /// Certificate public key did not match the PKCS#12 private key.
261    PrivateKeyMismatch,
262    /// Certificate subject did not describe an Apple development identity.
263    CertificateIdentity,
264    /// Certificate is not yet valid.
265    CertificateNotYetValid,
266    /// Certificate is expired at validation time.
267    CertificateExpired,
268    /// Certificate did not chain to a pinned Apple root for code signing.
269    CertificateUntrusted,
270    /// CMS provisioning-profile bytes were malformed.
271    ProvisioningProfileCmsMalformed,
272    /// CMS signature, signed content, attributes, or signer trust failed.
273    ProvisioningProfileSignature,
274    /// Verified CMS content was not a valid bounded provisioning-profile plist.
275    ProvisioningProfileParse(ProvisioningProfileParseError),
276    /// Provisioning profile creation time is in the future.
277    ProvisioningProfileNotYetValid,
278    /// Provisioning profile is expired at validation time.
279    ProvisioningProfileExpired,
280    /// Provisioning profile belongs to another development team.
281    ProvisioningProfileTeamMismatch,
282    /// Provisioning profile does not include the supplied signing certificate.
283    ProvisioningProfileCertificateMismatch,
284}
285
286impl fmt::Display for ManualSigningAssetError {
287    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288        match self {
289            Self::InvalidInput { field, reason } => write!(formatter, "{field} {reason}"),
290            Self::Clock => formatter.write_str("validation clock is unavailable"),
291            Self::CryptoUnavailable => {
292                formatter.write_str("required cryptographic validation is unavailable")
293            }
294            Self::Pkcs12Malformed => formatter.write_str("certificate PKCS#12 is malformed"),
295            Self::Pkcs12Decrypt => {
296                formatter.write_str("certificate PKCS#12 integrity or decryption failed")
297            }
298            Self::MissingPrivateKey => {
299                formatter.write_str("certificate PKCS#12 has no private key")
300            }
301            Self::MissingCertificate => formatter
302                .write_str("certificate PKCS#12 has no certificate matching its private key"),
303            Self::PrivateKeyMismatch => {
304                formatter.write_str("certificate and private key do not match")
305            }
306            Self::CertificateIdentity => {
307                formatter.write_str("certificate is not an Apple development identity")
308            }
309            Self::CertificateNotYetValid => formatter.write_str("certificate is not yet valid"),
310            Self::CertificateExpired => formatter.write_str("certificate is expired"),
311            Self::CertificateUntrusted => {
312                formatter.write_str("certificate is not trusted for Apple code signing")
313            }
314            Self::ProvisioningProfileCmsMalformed => {
315                formatter.write_str("provisioning profile CMS is malformed")
316            }
317            Self::ProvisioningProfileSignature => {
318                formatter.write_str("provisioning profile CMS signature or signer trust is invalid")
319            }
320            Self::ProvisioningProfileParse(source) => write!(formatter, "{source}"),
321            Self::ProvisioningProfileNotYetValid => {
322                formatter.write_str("provisioning profile is not yet valid")
323            }
324            Self::ProvisioningProfileExpired => {
325                formatter.write_str("provisioning profile is expired")
326            }
327            Self::ProvisioningProfileTeamMismatch => {
328                formatter.write_str("certificate and provisioning profile teams differ")
329            }
330            Self::ProvisioningProfileCertificateMismatch => {
331                formatter.write_str("provisioning profile does not include the signing certificate")
332            }
333        }
334    }
335}
336
337impl Error for ManualSigningAssetError {
338    fn source(&self) -> Option<&(dyn Error + 'static)> {
339        match self {
340            Self::ProvisioningProfileParse(source) => Some(source),
341            _ => None,
342        }
343    }
344}
345
346/// Validate a PKCS#12 signing identity and Apple CMS provisioning profile.
347///
348/// Validation uses the current wall-clock time and exactly three pinned Apple
349/// roots. It verifies the PKCS#12 integrity/decryption, key-to-certificate
350/// match, code-signing certificate chain and identity, CMS signer chain and
351/// signature, decoded plist bounds, expiration, team, and profile certificate
352/// membership. On success it returns public metadata plus ownership of the
353/// exact validated secret buffers for forwarding to a worker. On failure the
354/// input is dropped and overwritten.
355///
356/// # Errors
357///
358/// Returns [`ManualSigningAssetError`] without preserving OpenSSL diagnostics
359/// or caller-controlled secret content.
360pub fn validate_manual_signing_assets(
361    input: ManualSigningAssetsInput,
362) -> Result<(ValidatedManualSigningAssets, ManualSigningAssetsInput), ManualSigningAssetError> {
363    let now_unix_seconds = SystemTime::now()
364        .duration_since(UNIX_EPOCH)
365        .map_err(|_| ManualSigningAssetError::Clock)?
366        .as_secs();
367    validate_manual_signing_assets_with_policy(
368        input,
369        &SigningAssetPolicy {
370            trust_anchors: &PRODUCTION_TRUST_ANCHORS,
371            accepted_common_name_prefixes: &[APPLE_DEVELOPMENT_PREFIX, IPHONE_DEVELOPER_PREFIX],
372            now_unix_seconds,
373        },
374    )
375}
376
377/// Validate one PKCS#12 identity without provisioning-profile bytes.
378///
379/// The exact input buffers are returned on success for one later upload.
380///
381/// # Errors
382///
383/// Returns a secret-free identity-validation error.
384pub fn validate_manual_signing_identity(
385    input: ManualSigningIdentityInput,
386) -> Result<(SigningCertificate, ManualSigningIdentityInput), ManualSigningAssetError> {
387    let now_unix_seconds = SystemTime::now()
388        .duration_since(UNIX_EPOCH)
389        .map_err(|_| ManualSigningAssetError::Clock)?
390        .as_secs();
391    let certificate = validate_manual_signing_identity_with_policy(
392        &input.certificate_pkcs12,
393        &input.certificate_password,
394        &SigningAssetPolicy {
395            trust_anchors: &PRODUCTION_TRUST_ANCHORS,
396            accepted_common_name_prefixes: &[APPLE_DEVELOPMENT_PREFIX, IPHONE_DEVELOPER_PREFIX],
397            now_unix_seconds,
398        },
399    )?;
400    Ok((certificate, input))
401}
402
403/// Validate one CMS provisioning profile against an already validated identity.
404///
405/// The exact profile buffer is returned on success for one later upload.
406///
407/// # Errors
408///
409/// Returns a secret-free CMS, metadata, team, or certificate-membership error.
410pub fn validate_manual_signing_profile(
411    profile: SecretBytes,
412    certificate: &SigningCertificate,
413) -> Result<(ProvisioningProfile, SecretBytes), ManualSigningAssetError> {
414    validate_nonempty_size(
415        &profile,
416        MAX_MANUAL_SIGNING_PROFILE_BYTES,
417        ManualSigningAssetField::ProvisioningProfile,
418    )?;
419    let now_unix_seconds = SystemTime::now()
420        .duration_since(UNIX_EPOCH)
421        .map_err(|_| ManualSigningAssetError::Clock)?
422        .as_secs();
423    let parsed = validate_manual_signing_profile_with_policy(
424        &profile,
425        certificate,
426        &SigningAssetPolicy {
427            trust_anchors: &PRODUCTION_TRUST_ANCHORS,
428            accepted_common_name_prefixes: &[APPLE_DEVELOPMENT_PREFIX, IPHONE_DEVELOPER_PREFIX],
429            now_unix_seconds,
430        },
431    )?;
432    Ok((parsed, profile))
433}
434
435#[derive(Clone, Copy)]
436struct TrustAnchor<'a> {
437    pem: &'a [u8],
438    sha256: &'a str,
439}
440
441struct SigningAssetPolicy<'a> {
442    trust_anchors: &'a [TrustAnchor<'a>],
443    accepted_common_name_prefixes: &'a [&'a str],
444    now_unix_seconds: u64,
445}
446
447fn validate_manual_signing_assets_with_policy(
448    input: ManualSigningAssetsInput,
449    policy: &SigningAssetPolicy<'_>,
450) -> Result<(ValidatedManualSigningAssets, ManualSigningAssetsInput), ManualSigningAssetError> {
451    let certificate_metadata = validate_manual_signing_identity_with_policy(
452        &input.certificate_pkcs12,
453        &input.certificate_password,
454        policy,
455    )?;
456    let profile = validate_manual_signing_profile_with_policy(
457        &input.provisioning_profile,
458        &certificate_metadata,
459        policy,
460    )?;
461    Ok((
462        ValidatedManualSigningAssets {
463            certificate: certificate_metadata,
464            profile,
465        },
466        input,
467    ))
468}
469
470fn validate_manual_signing_identity_with_policy(
471    certificate_pkcs12: &SecretBytes,
472    certificate_password: &SecretBytes,
473    policy: &SigningAssetPolicy<'_>,
474) -> Result<SigningCertificate, ManualSigningAssetError> {
475    if !is_exact_der_sequence(certificate_pkcs12.expose_secret_bytes()) {
476        return Err(ManualSigningAssetError::Pkcs12Malformed);
477    }
478    let pkcs12 = Pkcs12::from_der(certificate_pkcs12.expose_secret_bytes())
479        .map_err(|_| ManualSigningAssetError::Pkcs12Malformed)?;
480
481    // Legacy algorithms are available only for the bounded import operation.
482    // `retain_fallbacks` preserves the default provider for modern archives.
483    let legacy_provider = Provider::try_load(None, "legacy", true)
484        .map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
485    let parse_result = {
486        let password = str::from_utf8(certificate_password.expose_secret_bytes())
487            .map_err(|_| ManualSigningAssetError::Pkcs12Decrypt)?;
488        pkcs12.parse2(password)
489    };
490    drop(legacy_provider);
491    let parsed = parse_result.map_err(|_| ManualSigningAssetError::Pkcs12Decrypt)?;
492    let private_key = parsed
493        .pkey
494        .ok_or(ManualSigningAssetError::MissingPrivateKey)?;
495    let certificate = parsed
496        .cert
497        .ok_or(ManualSigningAssetError::MissingCertificate)?;
498    validate_private_key_match(&certificate, &private_key)?;
499
500    validate_certificate(&certificate, parsed.ca.as_ref(), policy)
501}
502
503fn validate_manual_signing_profile_with_policy(
504    provisioning_profile: &SecretBytes,
505    certificate: &SigningCertificate,
506    policy: &SigningAssetPolicy<'_>,
507) -> Result<ProvisioningProfile, ManualSigningAssetError> {
508    if !is_exact_der_sequence(provisioning_profile.expose_secret_bytes()) {
509        return Err(ManualSigningAssetError::ProvisioningProfileCmsMalformed);
510    }
511    let mut cms = CmsContentInfo::from_der(provisioning_profile.expose_secret_bytes())
512        .map_err(|_| ManualSigningAssetError::ProvisioningProfileCmsMalformed)?;
513    let cms_store = build_trust_store(policy, None)?;
514    let mut decoded_profile = Vec::new();
515    let verification = cms.verify(
516        None,
517        Some(&cms_store),
518        None,
519        Some(&mut decoded_profile),
520        CMSOptions::BINARY,
521    );
522    if verification.is_err() {
523        decoded_profile.fill(0);
524        return Err(ManualSigningAssetError::ProvisioningProfileSignature);
525    }
526
527    let profile_result = parse_decoded_provisioning_profile(&decoded_profile);
528    decoded_profile.fill(0);
529    let profile = profile_result.map_err(ManualSigningAssetError::ProvisioningProfileParse)?;
530    if profile.created_at_unix_seconds > policy.now_unix_seconds {
531        return Err(ManualSigningAssetError::ProvisioningProfileNotYetValid);
532    }
533    if profile.expires_at_unix_seconds <= policy.now_unix_seconds {
534        return Err(ManualSigningAssetError::ProvisioningProfileExpired);
535    }
536    if profile.team.id() != certificate.team.id() {
537        return Err(ManualSigningAssetError::ProvisioningProfileTeamMismatch);
538    }
539    if !profile
540        .certificate_fingerprints
541        .iter()
542        .any(|fingerprint| fingerprint == &certificate.sha256_fingerprint)
543    {
544        return Err(ManualSigningAssetError::ProvisioningProfileCertificateMismatch);
545    }
546    Ok(profile)
547}
548
549fn validate_private_key_match(
550    certificate: &X509,
551    private_key: &PKeyRef<Private>,
552) -> Result<(), ManualSigningAssetError> {
553    let public_key = certificate
554        .public_key()
555        .map_err(|_| ManualSigningAssetError::CertificateIdentity)?;
556    if !public_key.public_eq(private_key) {
557        return Err(ManualSigningAssetError::PrivateKeyMismatch);
558    }
559    Ok(())
560}
561
562fn validate_certificate(
563    certificate: &X509,
564    supplied_chain: Option<&Stack<X509>>,
565    policy: &SigningAssetPolicy<'_>,
566) -> Result<SigningCertificate, ManualSigningAssetError> {
567    let now = Asn1Time::from_unix(
568        policy
569            .now_unix_seconds
570            .try_into()
571            .map_err(|_| ManualSigningAssetError::Clock)?,
572    )
573    .map_err(|_| ManualSigningAssetError::Clock)?;
574    if certificate
575        .not_before()
576        .compare(&now)
577        .map_err(|_| ManualSigningAssetError::CertificateIdentity)?
578        == Ordering::Greater
579    {
580        return Err(ManualSigningAssetError::CertificateNotYetValid);
581    }
582    if certificate
583        .not_after()
584        .compare(&now)
585        .map_err(|_| ManualSigningAssetError::CertificateIdentity)?
586        != Ordering::Greater
587    {
588        return Err(ManualSigningAssetError::CertificateExpired);
589    }
590
591    let common_name = unique_subject_value(certificate, Nid::COMMONNAME)?;
592    let accepted_name = policy.accepted_common_name_prefixes.iter().any(|prefix| {
593        common_name
594            .strip_prefix(prefix)
595            .is_some_and(|remainder| !remainder.is_empty())
596    });
597    if !accepted_name {
598        return Err(ManualSigningAssetError::CertificateIdentity);
599    }
600    let team_id = unique_subject_value(certificate, Nid::ORGANIZATIONALUNITNAME)?;
601    let team = DevelopmentTeam::new(team_id, None)
602        .map_err(|_| ManualSigningAssetError::CertificateIdentity)?;
603
604    let empty_chain = Stack::new().map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
605    let chain: &StackRef<X509> = match supplied_chain {
606        Some(chain) => chain.as_ref(),
607        None => empty_chain.as_ref(),
608    };
609    let store = build_trust_store(policy, Some(X509PurposeId::CODE_SIGN))?;
610    let mut context =
611        X509StoreContext::new().map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
612    let verified = context
613        .init(
614            &store,
615            certificate,
616            chain,
617            openssl::x509::X509StoreContextRef::verify_cert,
618        )
619        .map_err(|_| ManualSigningAssetError::CertificateUntrusted)?;
620    if !verified {
621        return Err(ManualSigningAssetError::CertificateUntrusted);
622    }
623
624    let fingerprint = certificate
625        .digest(MessageDigest::sha256())
626        .map_err(|_| ManualSigningAssetError::CertificateIdentity)?;
627    let metadata = SigningCertificate {
628        common_name,
629        sha256_fingerprint: uppercase_hex(fingerprint.as_ref()),
630        team,
631        expires_at_unix_seconds: asn1_time_to_unix(certificate.not_after())?,
632    };
633    metadata
634        .validate()
635        .map_err(|_| ManualSigningAssetError::CertificateIdentity)?;
636    Ok(metadata)
637}
638
639fn unique_subject_value(certificate: &X509, nid: Nid) -> Result<String, ManualSigningAssetError> {
640    let mut entries = certificate.subject_name().entries_by_nid(nid);
641    let entry = entries
642        .next()
643        .ok_or(ManualSigningAssetError::CertificateIdentity)?;
644    if entries.next().is_some() {
645        return Err(ManualSigningAssetError::CertificateIdentity);
646    }
647    let value = entry
648        .data()
649        .to_string()
650        .map_err(|_| ManualSigningAssetError::CertificateIdentity)?;
651    if value.contains('\u{fffd}') {
652        return Err(ManualSigningAssetError::CertificateIdentity);
653    }
654    Ok(value)
655}
656
657fn build_trust_store(
658    policy: &SigningAssetPolicy<'_>,
659    purpose: Option<X509PurposeId>,
660) -> Result<X509Store, ManualSigningAssetError> {
661    if policy.trust_anchors.is_empty() {
662        return Err(ManualSigningAssetError::CryptoUnavailable);
663    }
664    let mut builder =
665        X509StoreBuilder::new().map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
666    for anchor in policy.trust_anchors {
667        builder
668            .add_cert(load_trust_anchor(*anchor)?)
669            .map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
670    }
671    let mut parameters =
672        X509VerifyParam::new().map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
673    parameters.set_time(
674        policy
675            .now_unix_seconds
676            .try_into()
677            .map_err(|_| ManualSigningAssetError::Clock)?,
678    );
679    parameters.set_depth(MAX_CERTIFICATE_CHAIN_DEPTH);
680    builder
681        .set_param(&parameters)
682        .map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
683    if let Some(purpose) = purpose {
684        builder
685            .set_purpose(purpose)
686            .map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
687    }
688    Ok(builder.build())
689}
690
691fn load_trust_anchor(anchor: TrustAnchor<'_>) -> Result<X509, ManualSigningAssetError> {
692    let certificate =
693        X509::from_pem(anchor.pem).map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
694    let fingerprint = certificate
695        .digest(MessageDigest::sha256())
696        .map_err(|_| ManualSigningAssetError::CryptoUnavailable)?;
697    if uppercase_hex(fingerprint.as_ref()) != anchor.sha256 {
698        return Err(ManualSigningAssetError::CryptoUnavailable);
699    }
700    Ok(certificate)
701}
702
703fn asn1_time_to_unix(time: &Asn1TimeRef) -> Result<u64, ManualSigningAssetError> {
704    let epoch = Asn1Time::from_unix(0).map_err(|_| ManualSigningAssetError::CertificateIdentity)?;
705    let difference = epoch
706        .diff(time)
707        .map_err(|_| ManualSigningAssetError::CertificateIdentity)?;
708    let seconds = i64::from(difference.days)
709        .checked_mul(86_400)
710        .and_then(|days| days.checked_add(i64::from(difference.secs)))
711        .ok_or(ManualSigningAssetError::CertificateIdentity)?;
712    u64::try_from(seconds).map_err(|_| ManualSigningAssetError::CertificateIdentity)
713}
714
715fn validate_nonempty_size(
716    bytes: &SecretBytes,
717    maximum: usize,
718    field: ManualSigningAssetField,
719) -> Result<(), ManualSigningAssetError> {
720    if bytes.is_empty() {
721        return Err(ManualSigningAssetError::InvalidInput {
722            field,
723            reason: ManualSigningAssetInputError::Empty,
724        });
725    }
726    if bytes.len() > maximum {
727        return Err(ManualSigningAssetError::InvalidInput {
728            field,
729            reason: ManualSigningAssetInputError::TooLarge,
730        });
731    }
732    Ok(())
733}
734
735fn is_exact_der_sequence(bytes: &[u8]) -> bool {
736    // OpenSSL's DER constructors accept one valid object prefix and ignore any
737    // trailing bytes, so validate the complete top-level encoding separately.
738    let Some((&0x30, remainder)) = bytes.split_first() else {
739        return false;
740    };
741    let Some((&length_octet, remainder)) = remainder.split_first() else {
742        return false;
743    };
744    let (header_length, content_length) = if length_octet & 0x80 == 0 {
745        (2_usize, usize::from(length_octet))
746    } else {
747        let length_bytes = usize::from(length_octet & 0x7f);
748        if length_bytes == 0
749            || length_bytes > std::mem::size_of::<usize>()
750            || remainder.len() < length_bytes
751        {
752            return false;
753        }
754        let encoded_length = &remainder[..length_bytes];
755        if encoded_length[0] == 0 {
756            return false;
757        }
758        let Some(content_length) = encoded_length.iter().try_fold(0_usize, |length, byte| {
759            length
760                .checked_mul(256)
761                .and_then(|length| length.checked_add(usize::from(*byte)))
762        }) else {
763            return false;
764        };
765        if content_length < 128 {
766            return false;
767        }
768        (2 + length_bytes, content_length)
769    };
770    header_length
771        .checked_add(content_length)
772        .is_some_and(|encoded_length| encoded_length == bytes.len())
773}
774
775fn uppercase_hex(bytes: &[u8]) -> String {
776    const HEX: &[u8; 16] = b"0123456789ABCDEF";
777    let mut output = String::with_capacity(bytes.len() * 2);
778    for byte in bytes {
779        output.push(char::from(HEX[usize::from(byte >> 4)]));
780        output.push(char::from(HEX[usize::from(byte & 0x0f)]));
781    }
782    output
783}
784
785#[cfg(test)]
786mod tests {
787    use std::time::{Duration, UNIX_EPOCH};
788
789    use openssl::{
790        asn1::{Asn1Integer, Asn1Time},
791        bn::BigNum,
792        ec::{EcGroup, EcKey},
793        pkey::{PKey, Private},
794        x509::{
795            X509Builder, X509NameBuilder,
796            extension::{BasicConstraints, ExtendedKeyUsage, KeyUsage},
797        },
798    };
799    use plist::{Dictionary, Value as PlistValue};
800
801    use super::*;
802
803    const TEAM_ID: &str = "TESTTEAM01";
804    const TEST_NOW_UNIX_SECONDS: u64 = 1_900_000_000;
805    const PASSWORD: &str = "fixture-password";
806    const DEVICE_UDID: &str = "00008110-001234567890801E";
807
808    struct Fixture {
809        root_pem: Vec<u8>,
810        root_fingerprint: String,
811        pkcs12: Vec<u8>,
812        profile_cms: Vec<u8>,
813        certificate_fingerprint: String,
814        signing_certificate: X509,
815    }
816
817    #[test]
818    fn validates_synthetic_assets_and_returns_the_exact_owned_inputs() {
819        let fixture = fixture(false, LeafUsage::CodeSigning);
820        let result = validate_fixture(
821            &fixture,
822            fixture.pkcs12.clone(),
823            PASSWORD,
824            fixture.profile_cms.clone(),
825        );
826        let (validated, input) = result.expect("synthetic signing assets should validate");
827
828        assert_eq!(validated.certificate.team.id(), TEAM_ID);
829        assert_eq!(
830            validated.certificate.sha256_fingerprint,
831            fixture.certificate_fingerprint
832        );
833        assert_eq!(validated.profile.team.id(), TEAM_ID);
834        assert_eq!(
835            validated.profile.uuid,
836            "12345678-1234-1234-1234-123456789ABC"
837        );
838
839        let (pkcs12, password, profile) = input.into_parts();
840        assert_eq!(pkcs12.expose_secret_bytes(), fixture.pkcs12);
841        assert_eq!(password.expose_secret_bytes(), PASSWORD.as_bytes());
842        assert_eq!(profile.expose_secret_bytes(), fixture.profile_cms);
843    }
844
845    #[test]
846    fn split_identity_and_multiple_profile_validation_retains_each_secret_once() {
847        let fixture = fixture(false, LeafUsage::CodeSigning);
848        let anchors = [TrustAnchor {
849            pem: &fixture.root_pem,
850            sha256: &fixture.root_fingerprint,
851        }];
852        let policy = SigningAssetPolicy {
853            trust_anchors: &anchors,
854            accepted_common_name_prefixes: &[APPLE_DEVELOPMENT_PREFIX, IPHONE_DEVELOPER_PREFIX],
855            now_unix_seconds: TEST_NOW_UNIX_SECONDS,
856        };
857        let identity = ManualSigningIdentityInput::new(
858            SecretBytes::new(fixture.pkcs12.clone()),
859            SecretBytes::new(PASSWORD.as_bytes().to_vec()),
860        )
861        .expect("bounded identity");
862        let certificate = validate_manual_signing_identity_with_policy(
863            &identity.certificate_pkcs12,
864            &identity.certificate_password,
865            &policy,
866        )
867        .expect("identity validates once");
868
869        let first = SecretBytes::new(fixture.profile_cms.clone());
870        let second = SecretBytes::new(fixture.profile_cms.clone());
871        let first_metadata =
872            validate_manual_signing_profile_with_policy(&first, &certificate, &policy)
873                .expect("first profile");
874        let second_metadata =
875            validate_manual_signing_profile_with_policy(&second, &certificate, &policy)
876                .expect("second profile");
877        assert_eq!(first_metadata, second_metadata);
878        assert_eq!(first.expose_secret_bytes(), fixture.profile_cms);
879        assert_eq!(second.expose_secret_bytes(), fixture.profile_cms);
880
881        let (pkcs12, password) = identity.into_parts();
882        assert_eq!(pkcs12.expose_secret_bytes(), fixture.pkcs12);
883        assert_eq!(password.expose_secret_bytes(), PASSWORD.as_bytes());
884
885        let mut wrong_team = certificate.clone();
886        wrong_team.team = DevelopmentTeam::new("OTHERTEAM1", None).expect("other team");
887        assert_eq!(
888            validate_manual_signing_profile_with_policy(&first, &wrong_team, &policy)
889                .expect_err("wrong team"),
890            ManualSigningAssetError::ProvisioningProfileTeamMismatch
891        );
892        let mut wrong_certificate = certificate.clone();
893        wrong_certificate.sha256_fingerprint = "F".repeat(64);
894        assert_eq!(
895            validate_manual_signing_profile_with_policy(&first, &wrong_certificate, &policy)
896                .expect_err("wrong certificate"),
897            ManualSigningAssetError::ProvisioningProfileCertificateMismatch
898        );
899
900        let expired = fixture_with_profile_window(
901            false,
902            LeafUsage::CodeSigning,
903            TEST_NOW_UNIX_SECONDS - 3_600,
904            TEST_NOW_UNIX_SECONDS,
905        );
906        let expired_anchors = [TrustAnchor {
907            pem: &expired.root_pem,
908            sha256: &expired.root_fingerprint,
909        }];
910        let expired_policy = SigningAssetPolicy {
911            trust_anchors: &expired_anchors,
912            accepted_common_name_prefixes: &[APPLE_DEVELOPMENT_PREFIX, IPHONE_DEVELOPER_PREFIX],
913            now_unix_seconds: TEST_NOW_UNIX_SECONDS,
914        };
915        let expired_certificate = validate_manual_signing_identity_with_policy(
916            &SecretBytes::new(expired.pkcs12.clone()),
917            &SecretBytes::new(PASSWORD.as_bytes().to_vec()),
918            &expired_policy,
919        )
920        .expect("expired fixture identity remains valid");
921        let expired_error = validate_manual_signing_profile_with_policy(
922            &SecretBytes::new(expired.profile_cms),
923            &expired_certificate,
924            &expired_policy,
925        )
926        .expect_err("expired profile");
927        assert_eq!(
928            expired_error,
929            ManualSigningAssetError::ProvisioningProfileExpired
930        );
931        let malformed = validate_manual_signing_profile_with_policy(
932            &SecretBytes::new(b"canary-profile".to_vec()),
933            &certificate,
934            &policy,
935        )
936        .expect_err("malformed profile");
937        assert_eq!(
938            malformed,
939            ManualSigningAssetError::ProvisioningProfileCmsMalformed
940        );
941        assert!(!malformed.to_string().contains("canary"));
942    }
943
944    #[test]
945    fn rejects_wrong_pkcs12_password_without_secret_diagnostics() {
946        let fixture = fixture(false, LeafUsage::CodeSigning);
947        let error = validation_error(validate_fixture(
948            &fixture,
949            fixture.pkcs12.clone(),
950            "wrong-password",
951            fixture.profile_cms.clone(),
952        ));
953
954        assert_eq!(error, ManualSigningAssetError::Pkcs12Decrypt);
955        assert!(!error.to_string().contains("wrong-password"));
956    }
957
958    #[test]
959    fn rejects_certificate_and_private_key_mismatch() {
960        let fixture = fixture(false, LeafUsage::CodeSigning);
961        let error = validate_private_key_match(&fixture.signing_certificate, &generate_key())
962            .expect_err("unrelated private key should be rejected");
963
964        assert_eq!(error, ManualSigningAssetError::PrivateKeyMismatch);
965    }
966
967    #[test]
968    fn rejects_expired_certificate_before_profile_use() {
969        let fixture = fixture(true, LeafUsage::CodeSigning);
970        let error = validation_error(validate_fixture(
971            &fixture,
972            fixture.pkcs12.clone(),
973            PASSWORD,
974            fixture.profile_cms.clone(),
975        ));
976
977        assert_eq!(error, ManualSigningAssetError::CertificateExpired);
978    }
979
980    #[test]
981    fn profile_validity_boundaries_are_enforced_after_cms_verification() {
982        let future = fixture_with_profile_window(
983            false,
984            LeafUsage::CodeSigning,
985            TEST_NOW_UNIX_SECONDS + 1,
986            TEST_NOW_UNIX_SECONDS + 3_600,
987        );
988        assert_eq!(
989            validation_error(validate_fixture(
990                &future,
991                future.pkcs12.clone(),
992                PASSWORD,
993                future.profile_cms.clone(),
994            )),
995            ManualSigningAssetError::ProvisioningProfileNotYetValid
996        );
997
998        let expired = fixture_with_profile_window(
999            false,
1000            LeafUsage::CodeSigning,
1001            TEST_NOW_UNIX_SECONDS - 3_600,
1002            TEST_NOW_UNIX_SECONDS,
1003        );
1004        assert_eq!(
1005            validation_error(validate_fixture(
1006                &expired,
1007                expired.pkcs12.clone(),
1008                PASSWORD,
1009                expired.profile_cms.clone(),
1010            )),
1011            ManualSigningAssetError::ProvisioningProfileExpired
1012        );
1013
1014        let boundary = fixture_with_profile_window(
1015            false,
1016            LeafUsage::CodeSigning,
1017            TEST_NOW_UNIX_SECONDS,
1018            TEST_NOW_UNIX_SECONDS + 3_600,
1019        );
1020        validate_fixture(
1021            &boundary,
1022            boundary.pkcs12.clone(),
1023            PASSWORD,
1024            boundary.profile_cms.clone(),
1025        )
1026        .expect("profile created at validation time should be valid");
1027    }
1028
1029    #[test]
1030    fn rejects_certificate_without_code_signing_usage() {
1031        let fixture = fixture(false, LeafUsage::EmailProtection);
1032        let error = validation_error(validate_fixture(
1033            &fixture,
1034            fixture.pkcs12.clone(),
1035            PASSWORD,
1036            fixture.profile_cms.clone(),
1037        ));
1038
1039        assert_eq!(error, ManualSigningAssetError::CertificateUntrusted);
1040    }
1041
1042    #[test]
1043    fn rejects_tampered_cms_signed_content() {
1044        let fixture = fixture(false, LeafUsage::CodeSigning);
1045        let mut tampered = fixture.profile_cms.clone();
1046        let marker = b"RustFerry Development";
1047        let marker_offset = tampered
1048            .windows(marker.len())
1049            .position(|window| window == marker)
1050            .expect("CMS fixture should contain its attached plist content");
1051        tampered[marker_offset] ^= 1;
1052
1053        let error = validation_error(validate_fixture(
1054            &fixture,
1055            fixture.pkcs12.clone(),
1056            PASSWORD,
1057            tampered,
1058        ));
1059        assert_eq!(error, ManualSigningAssetError::ProvisioningProfileSignature);
1060    }
1061
1062    #[test]
1063    fn rejects_unvalidated_bytes_appended_to_der_inputs() {
1064        let fixture = fixture(false, LeafUsage::CodeSigning);
1065        let mut pkcs12 = fixture.pkcs12.clone();
1066        pkcs12.push(0);
1067        let error = validation_error(validate_fixture(
1068            &fixture,
1069            pkcs12,
1070            PASSWORD,
1071            fixture.profile_cms.clone(),
1072        ));
1073        assert_eq!(error, ManualSigningAssetError::Pkcs12Malformed);
1074
1075        let mut profile_cms = fixture.profile_cms.clone();
1076        profile_cms.push(0);
1077        let error = validation_error(validate_fixture(
1078            &fixture,
1079            fixture.pkcs12.clone(),
1080            PASSWORD,
1081            profile_cms,
1082        ));
1083        assert_eq!(
1084            error,
1085            ManualSigningAssetError::ProvisioningProfileCmsMalformed
1086        );
1087    }
1088
1089    #[test]
1090    fn embedded_apple_roots_match_all_runtime_fingerprint_constants() {
1091        assert_eq!(
1092            PRODUCTION_TRUST_ANCHORS.map(|anchor| anchor.sha256),
1093            [
1094                APPLE_ROOT_CA_SHA256,
1095                APPLE_ROOT_CA_G2_SHA256,
1096                APPLE_ROOT_CA_G3_SHA256,
1097            ]
1098        );
1099        for anchor in PRODUCTION_TRUST_ANCHORS {
1100            let certificate = load_trust_anchor(anchor)
1101                .expect("embedded Apple root should match its pinned fingerprint");
1102            let fingerprint = certificate
1103                .digest(MessageDigest::sha256())
1104                .expect("embedded Apple root should be hashable");
1105            assert_eq!(uppercase_hex(fingerprint.as_ref()), anchor.sha256);
1106        }
1107
1108        assert!(matches!(
1109            load_trust_anchor(TrustAnchor {
1110                pem: APPLE_ROOT_CA_PEM,
1111                sha256: APPLE_ROOT_CA_G2_SHA256,
1112            }),
1113            Err(ManualSigningAssetError::CryptoUnavailable)
1114        ));
1115    }
1116
1117    fn validation_error(
1118        result: Result<
1119            (ValidatedManualSigningAssets, ManualSigningAssetsInput),
1120            ManualSigningAssetError,
1121        >,
1122    ) -> ManualSigningAssetError {
1123        match result {
1124            Ok(_) => panic!("invalid synthetic assets unexpectedly validated"),
1125            Err(error) => error,
1126        }
1127    }
1128
1129    fn validate_fixture(
1130        fixture: &Fixture,
1131        pkcs12: Vec<u8>,
1132        password: &str,
1133        profile_cms: Vec<u8>,
1134    ) -> Result<(ValidatedManualSigningAssets, ManualSigningAssetsInput), ManualSigningAssetError>
1135    {
1136        let anchors = [TrustAnchor {
1137            pem: &fixture.root_pem,
1138            sha256: &fixture.root_fingerprint,
1139        }];
1140        let prefixes = [APPLE_DEVELOPMENT_PREFIX, IPHONE_DEVELOPER_PREFIX];
1141        let input = ManualSigningAssetsInput::new(
1142            SecretBytes::new(pkcs12),
1143            SecretBytes::new(password.as_bytes().to_vec()),
1144            SecretBytes::new(profile_cms),
1145        )?;
1146        validate_manual_signing_assets_with_policy(
1147            input,
1148            &SigningAssetPolicy {
1149                trust_anchors: &anchors,
1150                accepted_common_name_prefixes: &prefixes,
1151                now_unix_seconds: TEST_NOW_UNIX_SECONDS,
1152            },
1153        )
1154    }
1155
1156    #[derive(Clone, Copy)]
1157    enum LeafUsage {
1158        CodeSigning,
1159        EmailProtection,
1160    }
1161
1162    fn fixture(expired: bool, leaf_usage: LeafUsage) -> Fixture {
1163        fixture_with_profile_window(
1164            expired,
1165            leaf_usage,
1166            TEST_NOW_UNIX_SECONDS - 3_600,
1167            TEST_NOW_UNIX_SECONDS + 3_600,
1168        )
1169    }
1170
1171    fn fixture_with_profile_window(
1172        expired: bool,
1173        leaf_usage: LeafUsage,
1174        profile_created_at: u64,
1175        profile_expires_at: u64,
1176    ) -> Fixture {
1177        let root_key = generate_key();
1178        let root = build_root(&root_key);
1179        let signing_key = generate_key();
1180        let signing_certificate = build_leaf(
1181            &signing_key,
1182            &root,
1183            &root_key,
1184            "Apple Development: RustFerry Fixture",
1185            leaf_usage,
1186            expired,
1187            2,
1188        );
1189        let profile_signer_key = generate_key();
1190        let profile_signer = build_leaf(
1191            &profile_signer_key,
1192            &root,
1193            &root_key,
1194            "RustFerry Profile Signer",
1195            LeafUsage::EmailProtection,
1196            false,
1197            3,
1198        );
1199
1200        let certificate_der = signing_certificate
1201            .to_der()
1202            .expect("fixture certificate should serialize");
1203        let certificate_fingerprint = uppercase_hex(
1204            signing_certificate
1205                .digest(MessageDigest::sha256())
1206                .expect("fixture certificate should be hashable")
1207                .as_ref(),
1208        );
1209        let profile_plist = profile_plist(&certificate_der, profile_created_at, profile_expires_at);
1210        let profile_cms = CmsContentInfo::sign(
1211            Some(&profile_signer),
1212            Some(&profile_signer_key),
1213            None,
1214            Some(&profile_plist),
1215            CMSOptions::BINARY,
1216        )
1217        .expect("fixture profile should sign")
1218        .to_der()
1219        .expect("fixture CMS should serialize");
1220        let pkcs12_bytes = pkcs12(&signing_key, &signing_certificate);
1221        let root_fingerprint = uppercase_hex(
1222            root.digest(MessageDigest::sha256())
1223                .expect("fixture root should be hashable")
1224                .as_ref(),
1225        );
1226
1227        Fixture {
1228            root_pem: root.to_pem().expect("fixture root should serialize"),
1229            root_fingerprint,
1230            pkcs12: pkcs12_bytes,
1231            profile_cms,
1232            certificate_fingerprint,
1233            signing_certificate,
1234        }
1235    }
1236
1237    fn generate_key() -> PKey<Private> {
1238        let group =
1239            EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).expect("fixture curve should exist");
1240        let key = EcKey::generate(&group).expect("fixture key should generate");
1241        PKey::from_ec_key(key).expect("fixture key should convert")
1242    }
1243
1244    fn build_root(key: &PKey<Private>) -> X509 {
1245        let mut name = X509NameBuilder::new().expect("fixture name builder should initialize");
1246        name.append_entry_by_nid(Nid::COMMONNAME, "RustFerry Test Root")
1247            .expect("fixture root name should be valid");
1248        let name = name.build();
1249
1250        let mut builder = certificate_builder(1, key, &name, &name, false);
1251        builder
1252            .append_extension(
1253                BasicConstraints::new()
1254                    .critical()
1255                    .ca()
1256                    .build()
1257                    .expect("root constraints should build"),
1258            )
1259            .expect("root constraints should append");
1260        builder
1261            .append_extension(
1262                KeyUsage::new()
1263                    .critical()
1264                    .key_cert_sign()
1265                    .crl_sign()
1266                    .build()
1267                    .expect("root key usage should build"),
1268            )
1269            .expect("root key usage should append");
1270        builder
1271            .sign(key, MessageDigest::sha256())
1272            .expect("fixture root should sign");
1273        builder.build()
1274    }
1275
1276    #[allow(clippy::too_many_arguments)]
1277    fn build_leaf(
1278        key: &PKey<Private>,
1279        issuer: &X509,
1280        issuer_key: &PKey<Private>,
1281        common_name: &str,
1282        usage: LeafUsage,
1283        expired: bool,
1284        serial: u32,
1285    ) -> X509 {
1286        let mut name = X509NameBuilder::new().expect("fixture name builder should initialize");
1287        name.append_entry_by_nid(Nid::COMMONNAME, common_name)
1288            .expect("fixture common name should be valid");
1289        name.append_entry_by_nid(Nid::ORGANIZATIONALUNITNAME, TEAM_ID)
1290            .expect("fixture team should be valid");
1291        let name = name.build();
1292
1293        let mut builder = certificate_builder(serial, key, &name, issuer.subject_name(), expired);
1294        builder
1295            .append_extension(
1296                BasicConstraints::new()
1297                    .critical()
1298                    .build()
1299                    .expect("leaf constraints should build"),
1300            )
1301            .expect("leaf constraints should append");
1302        builder
1303            .append_extension(
1304                KeyUsage::new()
1305                    .critical()
1306                    .digital_signature()
1307                    .build()
1308                    .expect("leaf key usage should build"),
1309            )
1310            .expect("leaf key usage should append");
1311        let mut extended_usage = ExtendedKeyUsage::new();
1312        match usage {
1313            LeafUsage::CodeSigning => {
1314                extended_usage.code_signing();
1315            }
1316            LeafUsage::EmailProtection => {
1317                extended_usage.email_protection();
1318            }
1319        }
1320        builder
1321            .append_extension(
1322                extended_usage
1323                    .critical()
1324                    .build()
1325                    .expect("leaf extended key usage should build"),
1326            )
1327            .expect("leaf extended key usage should append");
1328        builder
1329            .sign(issuer_key, MessageDigest::sha256())
1330            .expect("fixture leaf should sign");
1331        builder.build()
1332    }
1333
1334    fn certificate_builder(
1335        serial: u32,
1336        key: &PKey<Private>,
1337        subject: &openssl::x509::X509NameRef,
1338        issuer: &openssl::x509::X509NameRef,
1339        expired: bool,
1340    ) -> X509Builder {
1341        let mut builder = X509::builder().expect("fixture certificate builder should initialize");
1342        builder
1343            .set_version(2)
1344            .expect("fixture certificate version should set");
1345        let serial_number = BigNum::from_u32(serial).expect("fixture serial should build");
1346        let serial_number =
1347            Asn1Integer::from_bn(&serial_number).expect("fixture serial should convert");
1348        builder
1349            .set_serial_number(&serial_number)
1350            .expect("fixture serial should set");
1351        builder
1352            .set_subject_name(subject)
1353            .expect("fixture subject should set");
1354        builder
1355            .set_issuer_name(issuer)
1356            .expect("fixture issuer should set");
1357        builder
1358            .set_pubkey(key)
1359            .expect("fixture public key should set");
1360        let not_before = Asn1Time::from_unix(
1361            (TEST_NOW_UNIX_SECONDS - 86_400)
1362                .try_into()
1363                .expect("fixture time should fit"),
1364        )
1365        .expect("fixture not-before should build");
1366        let not_after_seconds = if expired {
1367            TEST_NOW_UNIX_SECONDS - 1
1368        } else {
1369            TEST_NOW_UNIX_SECONDS + 31_536_000
1370        };
1371        let not_after = Asn1Time::from_unix(
1372            not_after_seconds
1373                .try_into()
1374                .expect("fixture time should fit"),
1375        )
1376        .expect("fixture not-after should build");
1377        builder
1378            .set_not_before(&not_before)
1379            .expect("fixture not-before should set");
1380        builder
1381            .set_not_after(&not_after)
1382            .expect("fixture not-after should set");
1383        builder
1384    }
1385
1386    fn pkcs12(key: &PKey<Private>, certificate: &X509) -> Vec<u8> {
1387        let mut builder = Pkcs12::builder();
1388        builder
1389            .name("RustFerry Fixture")
1390            .pkey(key)
1391            .cert(certificate);
1392        builder
1393            .build2(PASSWORD)
1394            .expect("fixture PKCS#12 should build")
1395            .to_der()
1396            .expect("fixture PKCS#12 should serialize")
1397    }
1398
1399    fn profile_plist(
1400        certificate_der: &[u8],
1401        profile_created_at: u64,
1402        profile_expires_at: u64,
1403    ) -> Vec<u8> {
1404        let mut entitlements = Dictionary::new();
1405        entitlements.insert(
1406            "application-identifier".to_owned(),
1407            PlistValue::String(format!("{TEAM_ID}.com.example.rustferry")),
1408        );
1409        entitlements.insert(
1410            "com.apple.developer.team-identifier".to_owned(),
1411            PlistValue::String(TEAM_ID.to_owned()),
1412        );
1413        entitlements.insert("get-task-allow".to_owned(), PlistValue::Boolean(true));
1414
1415        let mut profile = Dictionary::new();
1416        profile.insert(
1417            "UUID".to_owned(),
1418            PlistValue::String("12345678-1234-1234-1234-123456789ABC".to_owned()),
1419        );
1420        profile.insert(
1421            "Name".to_owned(),
1422            PlistValue::String("RustFerry Development".to_owned()),
1423        );
1424        profile.insert(
1425            "TeamIdentifier".to_owned(),
1426            PlistValue::Array(vec![PlistValue::String(TEAM_ID.to_owned())]),
1427        );
1428        profile.insert(
1429            "TeamName".to_owned(),
1430            PlistValue::String("RustFerry Fixture Team".to_owned()),
1431        );
1432        profile.insert(
1433            "CreationDate".to_owned(),
1434            PlistValue::Date((UNIX_EPOCH + Duration::from_secs(profile_created_at)).into()),
1435        );
1436        profile.insert(
1437            "ExpirationDate".to_owned(),
1438            PlistValue::Date((UNIX_EPOCH + Duration::from_secs(profile_expires_at)).into()),
1439        );
1440        profile.insert(
1441            "Platform".to_owned(),
1442            PlistValue::Array(vec![PlistValue::String("iOS".to_owned())]),
1443        );
1444        profile.insert(
1445            "ProvisionedDevices".to_owned(),
1446            PlistValue::Array(vec![PlistValue::String(DEVICE_UDID.to_owned())]),
1447        );
1448        profile.insert(
1449            "DeveloperCertificates".to_owned(),
1450            PlistValue::Array(vec![PlistValue::Data(certificate_der.to_vec())]),
1451        );
1452        profile.insert(
1453            "Entitlements".to_owned(),
1454            PlistValue::Dictionary(entitlements),
1455        );
1456
1457        let mut plist = Vec::new();
1458        PlistValue::Dictionary(profile)
1459            .to_writer_xml(&mut plist)
1460            .expect("fixture profile should serialize");
1461        plist
1462    }
1463}