1use 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
30pub const MAX_MANUAL_SIGNING_PKCS12_BYTES: usize = 64 * 1024 * 1024;
32pub const MAX_MANUAL_SIGNING_PASSWORD_BYTES: usize = 4 * 1024;
34pub const MAX_MANUAL_SIGNING_PROFILE_BYTES: usize = 8 * 1024 * 1024;
36
37pub const APPLE_ROOT_CA_SHA256: &str =
39 "B0B1730ECBC7FF4505142C49F1295E6EDA6BCAED7E2C68C5BE91B5A11001F024";
40pub const APPLE_ROOT_CA_G2_SHA256: &str =
42 "C2B9B042DD57830E7D117DAC55AC8AE19407D38E41D88F3215BC3A890444A050";
43pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum ManualSigningAssetField {
73 CertificatePkcs12,
75 CertificatePassword,
77 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum ManualSigningAssetInputError {
94 Empty,
96 TooLarge,
98 PasswordEncoding,
100 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
115pub struct ManualSigningAssetsInput {
121 certificate_pkcs12: SecretBytes,
122 certificate_password: SecretBytes,
123 provisioning_profile: SecretBytes,
124}
125
126pub struct ManualSigningIdentityInput {
131 certificate_pkcs12: SecretBytes,
132 certificate_password: SecretBytes,
133}
134
135impl ManualSigningIdentityInput {
136 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 pub fn into_parts(self) -> (SecretBytes, SecretBytes) {
154 (self.certificate_pkcs12, self.certificate_password)
155 }
156}
157
158impl ManualSigningAssetsInput {
159 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 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#[derive(Clone, Debug, Eq, PartialEq)]
231pub struct ValidatedManualSigningAssets {
232 pub certificate: SigningCertificate,
234 pub profile: ProvisioningProfile,
236}
237
238#[derive(Clone, Debug, Eq, PartialEq)]
240pub enum ManualSigningAssetError {
241 InvalidInput {
243 field: ManualSigningAssetField,
245 reason: ManualSigningAssetInputError,
247 },
248 Clock,
250 CryptoUnavailable,
252 Pkcs12Malformed,
254 Pkcs12Decrypt,
256 MissingPrivateKey,
258 MissingCertificate,
260 PrivateKeyMismatch,
262 CertificateIdentity,
264 CertificateNotYetValid,
266 CertificateExpired,
268 CertificateUntrusted,
270 ProvisioningProfileCmsMalformed,
272 ProvisioningProfileSignature,
274 ProvisioningProfileParse(ProvisioningProfileParseError),
276 ProvisioningProfileNotYetValid,
278 ProvisioningProfileExpired,
280 ProvisioningProfileTeamMismatch,
282 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
346pub 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
377pub 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
403pub 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 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(¶meters)
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 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(¬_before)
1379 .expect("fixture not-before should set");
1380 builder
1381 .set_not_after(¬_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}