Browse Source

fix(cli): iOS app signature not retaining entitlements, closes #11089 (#11184)

* fix(cli): iOS app signature not retaining entitlements, closes #11089

The IPA does not retain the entitlements as a regression from #10854 which removed the signing step from the build() and archive(), deferring to the export() call

To retain the entitlements we need to force sign one of the files in the app bundle. The most reliable way to do this is to use a self signed certificate as a dummy signature - it is replaced by the export() call so we do not rely on any user provided certificate

Additionally the export options are incorrectly configuring a manual signing, preventing Xcode from properly managing provisioning profiles, which is also part of the fix

* fix header
Lucas Fernandes Nogueira 10 months ago
parent
commit
f5d61822bf

+ 6 - 0
.changes/fix-ios-app-export.md

@@ -0,0 +1,6 @@
+---
+"tauri-cli": patch:bug
+"@tauri-apps/cli": patch:bug
+---
+
+Fix iOS application not including the provided capabilities (entitlements).

+ 5 - 0
.changes/self-signed-cert.md

@@ -0,0 +1,5 @@
+---
+"tauri-macos-sign": patch:enhance
+---
+
+Added `Keychain::with_certificate_file` and `certificate::generate_self_signed`.

File diff suppressed because it is too large
+ 785 - 28
Cargo.lock


+ 1 - 0
crates/tauri-cli/Cargo.toml

@@ -112,6 +112,7 @@ elf = "0.7"
 memchr = "2"
 tempfile = "3"
 uuid = { version = "1", features = ["v5"] }
+rand = "0.8"
 
 [dev-dependencies]
 insta = "1"

+ 59 - 12
crates/tauri-cli/src/mobile/ios/build.rs

@@ -30,6 +30,7 @@ use cargo_mobile2::{
   opts::{NoiseLevel, Profile},
   target::{call_for_targets_with_fallback, TargetInvalid, TargetTrait},
 };
+use rand::distributions::{Alphanumeric, DistString};
 
 use std::{
   env::{set_current_dir, var, var_os},
@@ -240,7 +241,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
     options,
     build_options,
     tauri_config,
-    &config,
+    &mut config,
     &mut env,
     noise_level,
   )?;
@@ -258,7 +259,7 @@ fn run_build(
   options: Options,
   mut build_options: BuildOptions,
   tauri_config: ConfigHandle,
-  config: &AppleConfig,
+  config: &mut AppleConfig,
   env: &mut Env,
   noise_level: NoiseLevel,
 ) -> Result<OptionsHandle> {
@@ -305,30 +306,31 @@ fn run_build(
       }
 
       let credentials = auth_credentials_from_env()?;
+      let skip_signing = credentials.is_some();
 
-      let mut build_config = BuildConfig::new()
-        .allow_provisioning_updates()
-        .skip_codesign();
+      let mut build_config = BuildConfig::new().allow_provisioning_updates();
       if let Some(credentials) = &credentials {
-        build_config = build_config.authentication_credentials(credentials.clone());
+        build_config = build_config
+          .authentication_credentials(credentials.clone())
+          .skip_codesign();
       }
 
       target.build(config, env, noise_level, profile, build_config)?;
 
+      let mut archive_config = ArchiveConfig::new();
+      if skip_signing {
+        archive_config = archive_config.skip_codesign();
+      }
+
       target.archive(
         config,
         env,
         noise_level,
         profile,
         Some(app_version),
-        ArchiveConfig::new().skip_codesign(),
+        archive_config,
       )?;
 
-      let mut export_config = ExportConfig::new().allow_provisioning_updates();
-      if let Some(credentials) = credentials {
-        export_config = export_config.authentication_credentials(credentials);
-      }
-
       let out_dir = config.export_dir().join(target.arch);
 
       if target.sdk == "iphonesimulator" {
@@ -346,6 +348,51 @@ fn run_build(
         fs::rename(&app_path, &path)?;
         out_files.push(path);
       } else {
+        // if we skipped code signing, we do not have the entitlements applied to our exported IPA
+        // we must force sign the app binary with a dummy certificate just to preserve the entitlements
+        // target.export() will sign it with an actual certificate for us
+        if skip_signing {
+          let password = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
+          let certificate = tauri_macos_sign::certificate::generate_self_signed(
+            tauri_macos_sign::certificate::SelfSignedCertificateRequest {
+              algorithm: "rsa".to_string(),
+              profile: tauri_macos_sign::certificate::CertificateProfile::AppleDistribution,
+              team_id: "unset".to_string(),
+              person_name: "Tauri".to_string(),
+              country_name: "NL".to_string(),
+              validity_days: 365,
+              password: password.clone(),
+            },
+          )?;
+          let tmp_dir = tempfile::tempdir()?;
+          let cert_path = tmp_dir.path().join("cert.p12");
+          std::fs::write(&cert_path, certificate)?;
+          let self_signed_cert_keychain =
+            tauri_macos_sign::Keychain::with_certificate_file(&cert_path, &password.into())?;
+
+          let app_dir = config
+            .export_dir()
+            .join(format!("{}.xcarchive", config.scheme()))
+            .join("Products/Applications")
+            .join(format!("{}.app", config.app().stylized_name()));
+
+          self_signed_cert_keychain.sign(
+            &app_dir.join(config.app().stylized_name()),
+            Some(
+              &config
+                .project_dir()
+                .join(config.scheme())
+                .join(format!("{}.entitlements", config.scheme())),
+            ),
+            false,
+          )?;
+        }
+
+        let mut export_config = ExportConfig::new().allow_provisioning_updates();
+        if let Some(credentials) = &credentials {
+          export_config = export_config.authentication_credentials(credentials.clone());
+        }
+
         target.export(config, env, noise_level, export_config)?;
 
         if let Ok(ipa_path) = config.ipa_path() {

+ 30 - 26
crates/tauri-cli/src/mobile/ios/mod.rs

@@ -526,16 +526,38 @@ pub fn synchronize_project_config(
         "signingStyle".to_string(),
         style.value.to_lowercase().into(),
       );
+    } else {
+      export_options_plist.insert("signingStyle".to_string(), "automatic".into());
     }
 
-    if let Some(identity) = build_configuration
-      .get_build_setting("\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"")
-      .or_else(|| build_configuration.get_build_setting("CODE_SIGN_IDENTITY"))
-    {
-      export_options_plist.insert(
-        "signingCertificate".to_string(),
-        identity.value.trim_matches('"').into(),
-      );
+    if manual_signing {
+      if let Some(identity) = build_configuration
+        .get_build_setting("\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"")
+        .or_else(|| build_configuration.get_build_setting("CODE_SIGN_IDENTITY"))
+      {
+        export_options_plist.insert(
+          "signingCertificate".to_string(),
+          identity.value.trim_matches('"').into(),
+        );
+      }
+
+      let profile_uuid = project_config
+        .provisioning_profile_uuid
+        .clone()
+        .or_else(|| {
+          build_configuration
+            .get_build_setting("\"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]\"")
+            .or_else(|| build_configuration.get_build_setting("PROVISIONING_PROFILE_SPECIFIER"))
+            .map(|setting| setting.value.trim_matches('"').to_string())
+        });
+      if let Some(profile_uuid) = profile_uuid {
+        let mut provisioning_profiles = plist::Dictionary::new();
+        provisioning_profiles.insert(config.app().identifier().to_string(), profile_uuid.into());
+        export_options_plist.insert(
+          "provisioningProfiles".to_string(),
+          provisioning_profiles.into(),
+        );
+      }
     }
 
     if let Some(id) = build_configuration
@@ -544,24 +566,6 @@ pub fn synchronize_project_config(
     {
       export_options_plist.insert("teamID".to_string(), id.value.trim_matches('"').into());
     }
-
-    let profile_uuid = project_config
-      .provisioning_profile_uuid
-      .clone()
-      .or_else(|| {
-        build_configuration
-          .get_build_setting("\"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]\"")
-          .or_else(|| build_configuration.get_build_setting("PROVISIONING_PROFILE_SPECIFIER"))
-          .map(|setting| setting.value.trim_matches('"').to_string())
-      });
-    if let Some(profile_uuid) = profile_uuid {
-      let mut provisioning_profiles = plist::Dictionary::new();
-      provisioning_profiles.insert(config.app().identifier().to_string(), profile_uuid.into());
-      export_options_plist.insert(
-        "provisioningProfiles".to_string(),
-        provisioning_profiles.into(),
-      );
-    }
   }
 
   Ok(())

+ 3 - 0
crates/tauri-macos-sign/Cargo.toml

@@ -21,3 +21,6 @@ plist = "1"
 rand = "0.8"
 dirs-next = "2"
 log = { version = "0.4.21", features = ["kv"] }
+apple-codesign = "0.27"
+chrono = "0.4.38"
+p12 = "0.6"

+ 65 - 0
crates/tauri-macos-sign/src/certificate.rs

@@ -0,0 +1,65 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use anyhow::{Context, Result};
+use apple_codesign::create_self_signed_code_signing_certificate;
+use x509_certificate::{EcdsaCurve, KeyAlgorithm};
+
+pub use apple_codesign::CertificateProfile;
+
+/// Self signed certificate options.
+pub struct SelfSignedCertificateRequest {
+  /// Which key type to use
+  pub algorithm: String,
+
+  /// Profile
+  pub profile: CertificateProfile,
+
+  /// Team ID (this is a short string attached to your Apple Developer account)
+  pub team_id: String,
+
+  /// The name of the person this certificate is for
+  pub person_name: String,
+
+  /// Country Name (C) value for certificate identifier
+  pub country_name: String,
+
+  /// How many days the certificate should be valid for
+  pub validity_days: i64,
+
+  /// Certificate password.
+  pub password: String,
+}
+
+pub fn generate_self_signed(request: SelfSignedCertificateRequest) -> Result<Vec<u8>> {
+  let algorithm = match request.algorithm.as_str() {
+    "ecdsa" => KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1),
+    "ed25519" => KeyAlgorithm::Ed25519,
+    "rsa" => KeyAlgorithm::Rsa,
+    value => panic!("algorithm values should have been validated by arg parser: {value}"),
+  };
+
+  let validity_duration = chrono::Duration::days(request.validity_days);
+
+  let (cert, key_pair) = create_self_signed_code_signing_certificate(
+    algorithm,
+    request.profile,
+    &request.team_id,
+    &request.person_name,
+    &request.country_name,
+    validity_duration,
+  )?;
+
+  let pfx = p12::PFX::new(
+    &cert.encode_der()?,
+    &key_pair.to_pkcs8_one_asymmetric_key_der(),
+    None,
+    &request.password,
+    "code-signing",
+  )
+  .context("failed to create PFX structure")?;
+  let der = pfx.to_der();
+
+  Ok(der)
+}

+ 12 - 5
crates/tauri-macos-sign/src/keychain.rs

@@ -57,6 +57,13 @@ impl Keychain {
     certificate_encoded: &OsString,
     certificate_password: &OsString,
   ) -> Result<Self> {
+    let tmp_dir = tempfile::tempdir()?;
+    let cert_path = tmp_dir.path().join("cert.p12");
+    super::decode_base64(certificate_encoded, &cert_path)?;
+    Self::with_certificate_file(&cert_path, certificate_password)
+  }
+
+  pub fn with_certificate_file(cert_path: &Path, certificate_password: &OsString) -> Result<Self> {
     let home_dir =
       dirs_next::home_dir().ok_or_else(|| anyhow::anyhow!("failed to resolve home dir"))?;
     let keychain_path = home_dir.join("Library").join("Keychains").join(format!(
@@ -69,10 +76,6 @@ impl Keychain {
       .args(["list-keychain", "-d", "user"])
       .output()?;
 
-    let tmp_dir = tempfile::tempdir()?;
-    let cert_path = tmp_dir.path().join("cert.p12");
-    super::decode_base64(certificate_encoded, &cert_path)?;
-
     assert_command(
       Command::new("security")
         .args(["create-keychain", "-p", &keychain_password])
@@ -92,7 +95,7 @@ impl Keychain {
     assert_command(
       Command::new("security")
         .arg("import")
-        .arg(&cert_path)
+        .arg(cert_path)
         .arg("-P")
         .arg(certificate_password)
         .args([
@@ -213,4 +216,8 @@ impl Keychain {
 
     Ok(())
   }
+
+  pub fn path(&self) -> Option<&Path> {
+    self.path.as_deref()
+  }
 }

+ 1 - 0
crates/tauri-macos-sign/src/lib.rs

@@ -11,6 +11,7 @@ use std::{
 use anyhow::{Context, Result};
 use serde::Deserialize;
 
+pub mod certificate;
 mod keychain;
 mod provisioning_profile;
 

Some files were not shown because too many files changed in this diff