feat(cli): iOS signing for CI usage (#9963)

* feat(cli): iOS signing for CI usage

* license headers

* change file

* chore: support more cert types

* xplicit method arg

* keep keychain alive

* fix early keychano drop

* set team id

* use common name as cert name
This commit is contained in:
Lucas Fernandes Nogueira 2024-07-12 11:08:55 -03:00 committed by GitHub
parent 532b3b1c03
commit 7c7fa0964d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1836 additions and 384 deletions

View File

@ -188,10 +188,14 @@
"path": "./core/tauri-utils",
"manager": "rust"
},
"tauri-macos-sign": {
"path": "./tooling/macos-sign",
"manager": "rust"
},
"tauri-bundler": {
"path": "./tooling/bundler",
"manager": "rust",
"dependencies": ["tauri-utils"]
"dependencies": ["tauri-utils", "tauri-macos-sign"]
},
"tauri-runtime": {
"path": "./core/tauri-runtime",
@ -262,7 +266,7 @@
"tauri-cli": {
"path": "./tooling/cli",
"manager": "rust",
"dependencies": ["tauri-bundler", "tauri-utils"],
"dependencies": ["tauri-bundler", "tauri-utils", "tauri-macos-sign"],
"postversion": [
"cargo check",
"cargo build --manifest-path ../../core/tauri-config-schema/Cargo.toml"

View File

@ -0,0 +1,6 @@
---
"tauri-cli": patch:feat
"@tauri-apps/cli": patch:feat
---
Added `--method` argument for `ios build` to select the export options' method.

6
.changes/ios-signing.md Normal file
View File

@ -0,0 +1,6 @@
---
"tauri-cli": patch:feat
"@tauri-apps/cli": patch:feat
---
Setup iOS signing by reading `IOS_CERTIFICATE`, `IOS_CERTIFICATE_PASSWORD` and `IOS_MOBILE_PROVISION` environment variables.

View File

@ -0,0 +1,5 @@
---
"tauri-macos-sign": minor:feat
---
Initial release.

View File

@ -58,6 +58,7 @@ glob = "0.3"
icns = { package = "tauri-icns", version = "0.1" }
time = { version = "0.3", features = [ "formatting" ] }
plist = "1"
tauri-macos-sign = { version = "0.0.0", path = "../macos-sign" }
[target."cfg(any(target_os = \"macos\", target_os = \"windows\"))".dependencies]
regex = "1"

View File

@ -118,12 +118,12 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
remove_extra_attr(&app_bundle_path)?;
// sign application
sign(sign_paths, identity, settings)?;
let keychain = sign(sign_paths, identity, settings)?;
// notarization is required for distribution
match notarize_auth() {
Ok(auth) => {
notarize(app_bundle_path.clone(), auth, settings)?;
notarize(&keychain, app_bundle_path.clone(), &auth)?;
}
Err(e) => {
if matches!(e, NotarizeAuthError::MissingTeamId) {

View File

@ -6,390 +6,53 @@
use std::{
env::{var, var_os},
ffi::OsString,
fs::File,
io::prelude::*,
path::PathBuf,
process::Command,
path::{Path, PathBuf},
};
use crate::{bundle::common::CommandExt, Settings};
use anyhow::Context;
use serde::Deserialize;
const KEYCHAIN_ID: &str = "tauri-build.keychain";
const KEYCHAIN_PWD: &str = "tauri-build";
// Import certificate from ENV variables.
// APPLE_CERTIFICATE is the p12 certificate base64 encoded.
// By example you can use; openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt
// Then use the value of the base64 in APPLE_CERTIFICATE env variable.
// You need to set APPLE_CERTIFICATE_PASSWORD to the password you set when you exported your certificate.
// https://help.apple.com/xcode/mac/current/#/dev154b28f09 see: `Export a signing certificate`
pub fn setup_keychain(
certificate_encoded: OsString,
certificate_password: OsString,
) -> crate::Result<()> {
// we delete any previous version of our keychain if present
delete_keychain();
log::info!("setup keychain from environment variables...");
let keychain_list_output = Command::new("security")
.args(["list-keychain", "-d", "user"])
.output()?;
let tmp_dir = tempfile::tempdir()?;
let cert_path = tmp_dir
.path()
.join("cert.p12")
.to_string_lossy()
.to_string();
let cert_path_tmp = tmp_dir
.path()
.join("cert.p12.tmp")
.to_string_lossy()
.to_string();
let certificate_encoded = certificate_encoded
.to_str()
.expect("failed to convert APPLE_CERTIFICATE to string")
.as_bytes();
let certificate_password = certificate_password
.to_str()
.expect("failed to convert APPLE_CERTIFICATE_PASSWORD to string")
.to_string();
// as certificate contain whitespace decoding may be broken
// https://github.com/marshallpierce/rust-base64/issues/105
// we'll use builtin base64 command from the OS
let mut tmp_cert = File::create(cert_path_tmp.clone())?;
tmp_cert.write_all(certificate_encoded)?;
Command::new("base64")
.args(["--decode", "-i", &cert_path_tmp, "-o", &cert_path])
.output_ok()
.context("failed to decode certificate")?;
Command::new("security")
.args(["create-keychain", "-p", KEYCHAIN_PWD, KEYCHAIN_ID])
.output_ok()
.context("failed to create keychain")?;
Command::new("security")
.args(["unlock-keychain", "-p", KEYCHAIN_PWD, KEYCHAIN_ID])
.output_ok()
.context("failed to set unlock keychain")?;
Command::new("security")
.args([
"import",
&cert_path,
"-k",
KEYCHAIN_ID,
"-P",
&certificate_password,
"-T",
"/usr/bin/codesign",
"-T",
"/usr/bin/pkgbuild",
"-T",
"/usr/bin/productbuild",
])
.output_ok()
.context("failed to import keychain certificate")?;
Command::new("security")
.args(["set-keychain-settings", "-t", "3600", "-u", KEYCHAIN_ID])
.output_ok()
.context("failed to set keychain settings")?;
Command::new("security")
.args([
"set-key-partition-list",
"-S",
"apple-tool:,apple:,codesign:",
"-s",
"-k",
KEYCHAIN_PWD,
KEYCHAIN_ID,
])
.output_ok()
.context("failed to set keychain settings")?;
let current_keychains = String::from_utf8_lossy(&keychain_list_output.stdout)
.split('\n')
.map(|line| {
line
.trim_matches(|c: char| c.is_whitespace() || c == '"')
.to_string()
})
.filter(|l| !l.is_empty())
.collect::<Vec<String>>();
Command::new("security")
.args(["list-keychain", "-d", "user", "-s"])
.args(current_keychains)
.arg(KEYCHAIN_ID)
.output_ok()
.context("failed to list keychain")?;
Ok(())
}
pub fn delete_keychain() {
// delete keychain if needed and skip any error
let _ = Command::new("security")
.arg("delete-keychain")
.arg(KEYCHAIN_ID)
.output_ok();
}
use crate::Settings;
pub struct SignTarget {
pub path: PathBuf,
pub is_an_executable: bool,
}
pub fn sign(targets: Vec<SignTarget>, identity: &str, settings: &Settings) -> crate::Result<()> {
pub fn sign(
targets: Vec<SignTarget>,
identity: &str,
settings: &Settings,
) -> crate::Result<tauri_macos_sign::Keychain> {
log::info!(action = "Signing"; "with identity \"{}\"", identity);
let setup_keychain = if let (Some(certificate_encoded), Some(certificate_password)) = (
let keychain = if let (Some(certificate_encoded), Some(certificate_password)) = (
var_os("APPLE_CERTIFICATE"),
var_os("APPLE_CERTIFICATE_PASSWORD"),
) {
// setup keychain allow you to import your certificate
// for CI build
setup_keychain(certificate_encoded, certificate_password)?;
true
tauri_macos_sign::Keychain::with_certificate(&certificate_encoded, &certificate_password)?
} else {
false
tauri_macos_sign::Keychain::with_signing_identity(identity)
};
log::info!("Signing app bundle...");
for target in targets {
try_sign(
target.path,
identity,
settings,
target.is_an_executable,
setup_keychain,
keychain.sign(
&target.path,
settings.macos().entitlements.as_ref().map(Path::new),
target.is_an_executable && settings.macos().hardened_runtime,
)?;
}
if setup_keychain {
// delete the keychain again after signing
delete_keychain();
}
Ok(())
}
fn try_sign(
path_to_sign: PathBuf,
identity: &str,
settings: &Settings,
is_an_executable: bool,
tauri_keychain: bool,
) -> crate::Result<()> {
log::info!(action = "Signing"; "{}", path_to_sign.display());
let mut args = vec!["--force", "-s", identity];
if tauri_keychain {
args.push("--keychain");
args.push(KEYCHAIN_ID);
}
if let Some(entitlements_path) = &settings.macos().entitlements {
log::info!("using entitlements file at {}", entitlements_path);
args.push("--entitlements");
args.push(entitlements_path);
}
// add runtime flag by default
if is_an_executable && settings.macos().hardened_runtime {
args.push("--options");
args.push("runtime");
}
Command::new("codesign")
.args(args)
.arg(path_to_sign)
.output_ok()
.context("failed to sign app")?;
Ok(())
}
#[derive(Deserialize)]
struct NotarytoolSubmitOutput {
id: String,
status: String,
message: String,
Ok(keychain)
}
pub fn notarize(
keychain: &tauri_macos_sign::Keychain,
app_bundle_path: PathBuf,
auth: NotarizeAuth,
settings: &Settings,
credentials: &tauri_macos_sign::AppleNotarizationCredentials,
) -> crate::Result<()> {
let bundle_stem = app_bundle_path
.file_stem()
.expect("failed to get bundle filename");
let tmp_dir = tempfile::tempdir()?;
let zip_path = tmp_dir
.path()
.join(format!("{}.zip", bundle_stem.to_string_lossy()));
let zip_args = vec![
"-c",
"-k",
"--keepParent",
"--sequesterRsrc",
app_bundle_path
.to_str()
.expect("failed to convert bundle_path to string"),
zip_path
.to_str()
.expect("failed to convert zip_path to string"),
];
// use ditto to create a PKZip almost identical to Finder
// this remove almost 99% of false alarm in notarization
Command::new("ditto")
.args(zip_args)
.output_ok()
.context("failed to zip app with ditto")?;
// sign the zip file
if let Some(identity) = &settings.macos().signing_identity {
sign(
vec![SignTarget {
path: zip_path.clone(),
is_an_executable: false,
}],
identity,
settings,
)?;
};
let notarize_args = vec![
"notarytool",
"submit",
zip_path
.to_str()
.expect("failed to convert zip_path to string"),
"--wait",
"--output-format",
"json",
];
log::info!(action = "Notarizing"; "{}", app_bundle_path.display());
let output = Command::new("xcrun")
.args(notarize_args)
.notarytool_args(&auth)
.output_ok()
.context("failed to upload app to Apple's notarization servers.")?;
if !output.status.success() {
return Err(anyhow::anyhow!("failed to notarize app").into());
}
let output_str = String::from_utf8_lossy(&output.stdout);
if let Ok(submit_output) = serde_json::from_str::<NotarytoolSubmitOutput>(&output_str) {
let log_message = format!(
"Finished with status {} for id {} ({})",
submit_output.status, submit_output.id, submit_output.message
);
if submit_output.status == "Accepted" {
log::info!(action = "Notarizing"; "{}", log_message);
staple_app(app_bundle_path)?;
Ok(())
} else if let Ok(output) = Command::new("xcrun")
.args(["notarytool", "log"])
.arg(&submit_output.id)
.notarytool_args(&auth)
.output_ok()
{
Err(
anyhow::anyhow!(
"{log_message}\nLog:\n{}",
String::from_utf8_lossy(&output.stdout)
)
.into(),
)
} else {
Err(anyhow::anyhow!("{log_message}").into())
}
} else {
Err(anyhow::anyhow!("failed to parse notarytool output as JSON: `{output_str}`").into())
}
}
fn staple_app(mut app_bundle_path: PathBuf) -> crate::Result<()> {
let app_bundle_path_clone = app_bundle_path.clone();
let filename = app_bundle_path_clone
.file_name()
.expect("failed to get bundle filename")
.to_str()
.expect("failed to convert bundle filename to string");
app_bundle_path.pop();
Command::new("xcrun")
.args(vec!["stapler", "staple", "-v", filename])
.current_dir(app_bundle_path)
.output_ok()
.context("failed to staple app.")?;
Ok(())
}
pub enum NotarizeAuth {
AppleId {
apple_id: OsString,
password: OsString,
team_id: OsString,
},
ApiKey {
key: OsString,
key_path: PathBuf,
issuer: OsString,
},
}
pub trait NotarytoolCmdExt {
fn notarytool_args(&mut self, auth: &NotarizeAuth) -> &mut Self;
}
impl NotarytoolCmdExt for Command {
fn notarytool_args(&mut self, auth: &NotarizeAuth) -> &mut Self {
match auth {
NotarizeAuth::AppleId {
apple_id,
password,
team_id,
} => self
.arg("--apple-id")
.arg(apple_id)
.arg("--password")
.arg(password)
.arg("--team-id")
.arg(team_id),
NotarizeAuth::ApiKey {
key,
key_path,
issuer,
} => self
.arg("--key-id")
.arg(key)
.arg("--key")
.arg(key_path)
.arg("--issuer")
.arg(issuer),
}
}
tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials).map_err(Into::into)
}
#[derive(Debug, thiserror::Error)]
@ -402,26 +65,29 @@ pub enum NotarizeAuthError {
Anyhow(#[from] anyhow::Error),
}
pub fn notarize_auth() -> Result<NotarizeAuth, NotarizeAuthError> {
pub fn notarize_auth() -> Result<tauri_macos_sign::AppleNotarizationCredentials, NotarizeAuthError>
{
match (
var_os("APPLE_ID"),
var_os("APPLE_PASSWORD"),
var_os("APPLE_TEAM_ID"),
) {
(Some(apple_id), Some(password), Some(team_id)) => Ok(NotarizeAuth::AppleId {
apple_id,
password,
team_id,
}),
(Some(apple_id), Some(password), Some(team_id)) => {
Ok(tauri_macos_sign::AppleNotarizationCredentials::AppleId {
apple_id,
password,
team_id,
})
}
(Some(_apple_id), Some(_password), None) => Err(NotarizeAuthError::MissingTeamId),
_ => {
match (var_os("APPLE_API_KEY"), var_os("APPLE_API_ISSUER"), var("APPLE_API_KEY_PATH")) {
(Some(key), Some(issuer), Ok(key_path)) => {
Ok(NotarizeAuth::ApiKey { key, key_path: key_path.into(), issuer })
(Some(key_id), Some(issuer), Ok(key_path)) => {
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey { key_id, key: tauri_macos_sign::ApiKey::Path( key_path.into()), issuer })
},
(Some(key), Some(issuer), Err(_)) => {
(Some(key_id), Some(issuer), Err(_)) => {
let mut api_key_file_name = OsString::from("AuthKey_");
api_key_file_name.push(&key);
api_key_file_name.push(&key_id);
api_key_file_name.push(".p8");
let mut key_path = None;
@ -440,7 +106,7 @@ pub fn notarize_auth() -> Result<NotarizeAuth, NotarizeAuthError> {
}
if let Some(key_path) = key_path {
Ok(NotarizeAuth::ApiKey { key, key_path, issuer })
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey { key_id, key: tauri_macos_sign::ApiKey::Path(key_path), issuer })
} else {
Err(anyhow::anyhow!("could not find API key file. Please set the APPLE_API_KEY_PATH environment variables to the path to the {api_key_file_name:?} file").into())
}

39
tooling/cli/Cargo.lock generated
View File

@ -1161,6 +1161,16 @@ dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
@ -1173,6 +1183,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dsa"
version = "0.6.3"
@ -5081,6 +5102,7 @@ dependencies = [
"strsim 0.11.0",
"tar",
"tauri-icns",
"tauri-macos-sign",
"tauri-utils 2.0.0-beta.18",
"tempfile",
"thiserror",
@ -5151,6 +5173,7 @@ dependencies = [
"sublime_fuzzy",
"tauri-bundler",
"tauri-icns",
"tauri-macos-sign",
"tauri-utils 1.5.4",
"tauri-utils 2.0.0-beta.18",
"tokio",
@ -5182,6 +5205,22 @@ dependencies = [
"png",
]
[[package]]
name = "tauri-macos-sign"
version = "0.0.0"
dependencies = [
"anyhow",
"dirs-next",
"once-cell-regex",
"os_pipe",
"plist",
"rand 0.8.5",
"serde",
"serde_json",
"tempfile",
"x509-certificate",
]
[[package]]
name = "tauri-utils"
version = "1.5.4"

View File

@ -112,6 +112,7 @@ libc = "0.2"
[target."cfg(target_os = \"macos\")".dependencies]
plist = "1"
tauri-macos-sign = { version = "0.0.0", path = "../macos-sign" }
[features]
default = [ "rustls" ]

View File

@ -24,6 +24,7 @@ use cargo_mobile2::{
use handlebars::{
Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, RenderErrorReason,
};
use serde::Serialize;
use std::{env::var_os, path::PathBuf};
@ -35,8 +36,23 @@ pub fn command(
) -> Result<()> {
let wrapper = TextWrapper::default();
exec(target, &wrapper, ci, reinstall_deps, skip_targets_install)
.map_err(|e| anyhow::anyhow!("{:#}", e))?;
let tauri_init_config = TauriInitConfig {
#[cfg(target_os = "macos")]
ios: {
let (keychain, provisioning_profile) = super::ios::signing_from_env()?;
super::ios::init_config(keychain.as_ref(), provisioning_profile.as_ref())?
},
};
exec(
target,
&wrapper,
&tauri_init_config,
ci,
reinstall_deps,
skip_targets_install,
)
.map_err(|e| anyhow::anyhow!("{:#}", e))?;
Ok(())
}
@ -77,9 +93,33 @@ pub fn configure_cargo(
dot_cargo.write(app).map_err(Into::into)
}
#[cfg(target_os = "macos")]
#[derive(Serialize)]
pub enum CodeSignStyle {
Manual,
Automatic,
}
#[cfg(target_os = "macos")]
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct IosInitConfig {
pub code_sign_style: CodeSignStyle,
pub code_sign_identity: Option<String>,
pub team_id: Option<String>,
pub provisioning_profile_uuid: Option<String>,
}
#[derive(Serialize)]
pub struct TauriInitConfig {
#[cfg(target_os = "macos")]
ios: IosInitConfig,
}
pub fn exec(
target: Target,
wrapper: &TextWrapper,
tauri_init_config: &TauriInitConfig,
#[allow(unused_variables)] non_interactive: bool,
#[allow(unused_variables)] reinstall_deps: bool,
skip_targets_install: bool,
@ -93,6 +133,8 @@ pub fn exec(
let (handlebars, mut map) = handlebars(&app);
map.insert("tauri", tauri_init_config);
let mut args = std::env::args_os();
let (binary, mut build_args) = args

View File

@ -17,7 +17,7 @@ use crate::{
mobile::{write_options, CliOptions},
ConfigValue, Result,
};
use clap::{ArgAction, Parser};
use clap::{ArgAction, Parser, ValueEnum};
use anyhow::Context;
use cargo_mobile2::{
@ -63,6 +63,41 @@ pub struct Options {
/// Skip prompting for values
#[clap(long, env = "CI")]
pub ci: bool,
/// Describes how Xcode should export the archive.
///
/// Use this to create a package ready for the App Store (app-store-connect option) or TestFlight (release-testing option).
#[clap(long, value_enum)]
pub export_method: Option<ExportMethod>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ExportMethod {
AppStoreConnect,
ReleaseTesting,
Debugging,
}
impl std::fmt::Display for ExportMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AppStoreConnect => write!(f, "app-store-connect"),
Self::ReleaseTesting => write!(f, "release-testing"),
Self::Debugging => write!(f, "debugging"),
}
}
}
impl std::str::FromStr for ExportMethod {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"app-store-connect" => Ok(Self::AppStoreConnect),
"release-testing" => Ok(Self::ReleaseTesting),
"debugging" => Ok(Self::Debugging),
_ => Err("unknown ios target"),
}
}
}
impl From<Options> for BuildOptions {
@ -129,9 +164,9 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
.join(config.scheme())
.join("Info.plist");
merge_plist(
&[
tauri_path.join("Info.plist"),
tauri_path.join("Info.ios.plist"),
vec![
tauri_path.join("Info.plist").into(),
tauri_path.join("Info.ios.plist").into(),
],
&info_plist_path,
)?;
@ -139,6 +174,22 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
let mut env = env()?;
configure_cargo(&app, None)?;
let (keychain, provisioning_profile) = super::signing_from_env()?;
let init_config = super::init_config(keychain.as_ref(), provisioning_profile.as_ref())?;
if let Some(export_options_plist) =
create_export_options(&app, &init_config, options.export_method)
{
let export_options_plist_path = config.project_dir().join("ExportOptions.plist");
merge_plist(
vec![
export_options_plist_path.clone().into(),
export_options_plist.into(),
],
&export_options_plist_path,
)?;
}
let open = options.open;
let _handle = run_build(
interface,
@ -157,6 +208,41 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
Ok(())
}
fn create_export_options(
app: &cargo_mobile2::config::app::App,
config: &super::super::init::IosInitConfig,
export_method: Option<ExportMethod>,
) -> Option<plist::Value> {
let mut plist = plist::Dictionary::new();
if let Some(method) = export_method {
plist.insert("method".to_string(), method.to_string().into());
}
if config.code_sign_identity.is_some() || config.provisioning_profile_uuid.is_some() {
plist.insert("signingStyle".to_string(), "manual".into());
}
if let Some(identity) = &config.code_sign_identity {
plist.insert("signingCertificate".to_string(), identity.clone().into());
}
if let Some(id) = &config.team_id {
plist.insert("teamID".to_string(), id.clone().into());
}
if let Some(profile_uuid) = &config.provisioning_profile_uuid {
let mut provisioning_profiles = plist::Dictionary::new();
provisioning_profiles.insert(app.reverse_identifier(), profile_uuid.clone().into());
plist.insert(
"provisioningProfiles".to_string(),
provisioning_profiles.into(),
);
}
(!plist.is_empty()).then(|| plist.into())
}
fn run_build(
interface: AppInterface,
options: Options,

View File

@ -148,9 +148,9 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> {
.join(config.scheme())
.join("Info.plist");
merge_plist(
&[
tauri_path.join("Info.plist"),
tauri_path.join("Info.ios.plist"),
vec![
tauri_path.join("Info.plist").into(),
tauri_path.join("Info.ios.plist").into(),
],
&info_plist_path,
)?;

View File

@ -30,7 +30,7 @@ use super::{
use crate::{helpers::config::Config as TauriConfig, Result};
use std::{
env::set_var,
env::{set_var, var_os},
fs::create_dir_all,
path::{Path, PathBuf},
thread::sleep,
@ -275,16 +275,36 @@ fn inject_assets(config: &AppleConfig) -> Result<()> {
Ok(())
}
fn merge_plist(src: &[PathBuf], dest: &Path) -> Result<()> {
enum PlistKind {
Path(PathBuf),
Plist(plist::Value),
}
impl From<PathBuf> for PlistKind {
fn from(p: PathBuf) -> Self {
Self::Path(p)
}
}
impl From<plist::Value> for PlistKind {
fn from(p: plist::Value) -> Self {
Self::Plist(p)
}
}
fn merge_plist(src: Vec<PlistKind>, dest: &Path) -> Result<()> {
let mut dest_plist = None;
for src_path in src {
if let Ok(src_plist) = plist::Value::from_file(src_path) {
for plist_kind in src {
let plist = match plist_kind {
PlistKind::Path(p) => plist::Value::from_file(p),
PlistKind::Plist(v) => Ok(v),
};
if let Ok(src_plist) = plist {
if dest_plist.is_none() {
dest_plist.replace(plist::Value::from_file(dest)?);
}
let plist = dest_plist.as_mut().expect("Info.plist not loaded");
let plist = dest_plist.as_mut().expect("plist not loaded");
if let Some(plist) = plist.as_dictionary_mut() {
if let Some(dict) = src_plist.into_dictionary() {
for (key, value) in dict {
@ -301,3 +321,40 @@ fn merge_plist(src: &[PathBuf], dest: &Path) -> Result<()> {
Ok(())
}
pub fn signing_from_env() -> Result<(
Option<tauri_macos_sign::Keychain>,
Option<tauri_macos_sign::ProvisioningProfile>,
)> {
let keychain = if let (Some(certificate), Some(certificate_password)) = (
var_os("IOS_CERTIFICATE"),
var_os("IOS_CERTIFICATE_PASSWORD"),
) {
tauri_macos_sign::Keychain::with_certificate(&certificate, &certificate_password).map(Some)?
} else {
None
};
let provisioning_profile = if let Some(provisioning_profile) = var_os("IOS_MOBILE_PROVISION") {
tauri_macos_sign::ProvisioningProfile::from_base64(&provisioning_profile).map(Some)?
} else {
None
};
Ok((keychain, provisioning_profile))
}
pub fn init_config(
keychain: Option<&tauri_macos_sign::Keychain>,
provisioning_profile: Option<&tauri_macos_sign::ProvisioningProfile>,
) -> Result<super::init::IosInitConfig> {
Ok(super::init::IosInitConfig {
code_sign_style: if keychain.is_some() && provisioning_profile.is_some() {
super::init::CodeSignStyle::Manual
} else {
super::init::CodeSignStyle::Automatic
},
code_sign_identity: keychain.map(|k| k.signing_identity()),
team_id: keychain.and_then(|k| k.team_id().map(ToString::to_string)),
provisioning_profile_uuid: provisioning_profile.and_then(|p| p.uuid().ok()),
})
}

View File

@ -15,6 +15,13 @@ settingGroups:
{{#if apple.development-team}}
DEVELOPMENT_TEAM: {{apple.development-team}}
{{/if}}
CODE_SIGN_STYLE: {{tauri.ios.code-sign-style}}
{{#if tauri.ios.code-sign-identity}}
CODE_SIGN_IDENTITY: "{{tauri.ios.code-sign-identity}}"
{{/if}}
{{#if tauri.ios.provisioning-profile-uuid}}
PROVISIONING_PROFILE_SPECIFIER: "{{tauri.ios.provisioning-profile-uuid}}"
{{/if}}
targetTemplates:
app:
type: application

869
tooling/macos-sign/Cargo.lock generated Normal file
View File

@ -0,0 +1,869 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bcder"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c627747a6774aab38beb35990d88309481378558875a41da1a4b2e373c906ef0"
dependencies = [
"bytes",
"smallvec",
]
[[package]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytes"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cc"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "line-wrap"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "memchr"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once-cell-regex"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3de7e389a5043420c8f2b95ed03f3f104ad6f4c41f7d7e27298f033abc253e8"
dependencies = [
"once_cell",
"regex",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "os_pipe"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "pem"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
dependencies = [
"base64 0.22.1",
"serde",
]
[[package]]
name = "plist"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9"
dependencies = [
"base64 0.21.7",
"indexmap",
"line-wrap",
"quick-xml",
"serde",
"time",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_users"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "regex"
version = "1.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"spin",
"untrusted",
"windows-sys",
]
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "syn"
version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tauri-macos-sign"
version = "0.0.0"
dependencies = [
"anyhow",
"dirs-next",
"once-cell-regex",
"os_pipe",
"plist",
"rand",
"serde",
"serde_json",
"tempfile",
"x509-certificate",
]
[[package]]
name = "tempfile"
version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "x509-certificate"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66534846dec7a11d7c50a74b7cdb208b9a581cad890b7866430d438455847c85"
dependencies = [
"bcder",
"bytes",
"chrono",
"der",
"hex",
"pem",
"ring",
"signature",
"spki",
"thiserror",
"zeroize",
]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -0,0 +1,24 @@
workspace = {}
[package]
name = "tauri-macos-sign"
version = "0.0.0"
authors = ["Tauri Programme within The Commons Conservancy"]
license = "Apache-2.0 OR MIT"
keywords = ["codesign", "signing", "macos", "ios", "tauri"]
repository = "https://github.com/tauri-apps/tauri"
description = "Code signing utilities for macOS and iOS apps"
edition = "2021"
rust-version = "1.70"
[dependencies]
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tempfile = "3"
x509-certificate = "0.23"
once-cell-regex = "0.2"
os_pipe = "1"
plist = "1"
rand = "0.8"
dirs-next = "2"

View File

@ -0,0 +1,3 @@
# Tauri MacOS Sign
Utilities for setting up macOS certificates, code signing and notarization for macOS and iOS apps.

View File

@ -0,0 +1,216 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
ffi::OsString,
path::{Path, PathBuf},
process::Command,
};
use crate::assert_command;
use anyhow::Result;
use rand::distributions::{Alphanumeric, DistString};
mod identity;
pub use identity::Team;
pub enum SigningIdentity {
Team(Team),
Identifier(String),
}
pub struct Keychain {
// none means the default keychain must be used
path: Option<PathBuf>,
signing_identity: SigningIdentity,
}
impl Drop for Keychain {
fn drop(&mut self) {
if let Some(path) = &self.path {
let _ = Command::new("security")
.arg("delete-keychain")
.arg(path)
.status();
}
}
}
impl Keychain {
/// Use a certificate in the default keychain.
pub fn with_signing_identity(identity: impl Into<String>) -> Self {
Self {
path: None,
signing_identity: SigningIdentity::Identifier(identity.into()),
}
}
/// Import certificate from base64 string.
/// certificate_encoded is the p12 certificate base64 encoded.
/// By example you can use; openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt
/// Then use the value of the base64 as `certificate_encoded`.
/// You need to set certificate_password to the password you set when you exported your certificate.
/// https://help.apple.com/xcode/mac/current/#/dev154b28f09 see: `Export a signing certificate`
pub fn with_certificate(
certificate_encoded: &OsString,
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!(
"{}.keychain-db",
Alphanumeric.sample_string(&mut rand::thread_rng(), 16)
));
let keychain_password = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
let keychain_list_output = Command::new("security")
.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])
.arg(&keychain_path)
.status(),
"failed to create keychain",
)?;
assert_command(
Command::new("security")
.args(["unlock-keychain", "-p", &keychain_password])
.arg(&keychain_path)
.status(),
"failed to set unlock keychain",
)?;
assert_command(
Command::new("security")
.arg("import")
.arg(&cert_path)
.arg("-P")
.arg(certificate_password)
.args([
"-T",
"/usr/bin/codesign",
"-T",
"/usr/bin/pkgbuild",
"-T",
"/usr/bin/productbuild",
])
.arg("-k")
.arg(&keychain_path)
.status(),
"failed to import keychain certificate",
)?;
assert_command(
Command::new("security")
.args(["set-keychain-settings", "-t", "3600", "-u"])
.arg(&keychain_path)
.status(),
"failed to set keychain settings",
)?;
assert_command(
Command::new("security")
.args([
"set-key-partition-list",
"-S",
"apple-tool:,apple:,codesign:",
"-s",
"-k",
&keychain_password,
])
.arg(&keychain_path)
.status(),
"failed to set keychain settings",
)?;
let current_keychains = String::from_utf8_lossy(&keychain_list_output.stdout)
.split('\n')
.map(|line| {
line
.trim_matches(|c: char| c.is_whitespace() || c == '"')
.to_string()
})
.filter(|l| !l.is_empty())
.collect::<Vec<String>>();
assert_command(
Command::new("security")
.args(["list-keychain", "-d", "user", "-s"])
.args(current_keychains)
.arg(&keychain_path)
.status(),
"failed to list keychain",
)?;
let signing_identity = identity::list(&keychain_path)
.map(|l| l.first().cloned())?
.ok_or_else(|| anyhow::anyhow!("failed to resolve signing identity"))?;
Ok(Self {
path: Some(keychain_path),
signing_identity: SigningIdentity::Team(signing_identity),
})
}
pub fn signing_identity(&self) -> String {
match &self.signing_identity {
SigningIdentity::Team(t) => t.certificate_name(),
SigningIdentity::Identifier(i) => i.to_string(),
}
}
pub fn team_id(&self) -> Option<&str> {
match &self.signing_identity {
SigningIdentity::Team(t) => Some(&t.id),
SigningIdentity::Identifier(_) => None,
}
}
pub fn sign(
&self,
path: &Path,
entitlements_path: Option<&Path>,
hardened_runtime: bool,
) -> Result<()> {
let identity = match &self.signing_identity {
SigningIdentity::Team(t) => t.certificate_name(),
SigningIdentity::Identifier(i) => i.clone(),
};
println!("Signing with identity \"{}\"", identity);
println!("Signing {}", path.display());
let mut args = vec!["--force", "-s", &identity];
if hardened_runtime {
args.push("--options");
args.push("runtime");
}
let mut codesign = Command::new("codesign");
codesign.args(args);
if let Some(p) = &self.path {
codesign.arg("--keychain").arg(p);
}
if let Some(entitlements_path) = entitlements_path {
codesign.arg("--entitlements");
codesign.arg(entitlements_path);
}
codesign.arg(path);
assert_command(codesign.status(), "failed to sign app")?;
Ok(())
}
}

View File

@ -0,0 +1,117 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use anyhow::Context;
use once_cell_regex::regex;
use std::{collections::BTreeSet, path::Path, process::Command};
use x509_certificate::certificate::X509Certificate;
use crate::Result;
fn get_pem_list(keychain_path: &Path, name_substr: &str) -> std::io::Result<std::process::Output> {
Command::new("security")
.arg("find-certificate")
.args(["-p", "-a"])
.arg("-c")
.arg(name_substr)
.arg(keychain_path)
.stderr(os_pipe::dup_stderr().unwrap())
.output()
}
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Team {
pub name: String,
pub certificate_name: String,
pub id: String,
pub cert_prefix: &'static str,
}
impl Team {
fn from_x509(cert_prefix: &'static str, cert: X509Certificate) -> Result<Self> {
let common_name = cert
.subject_common_name()
.ok_or_else(|| anyhow::anyhow!("skipping cert, missing common name"))?;
let organization = cert
.subject_name()
.iter_organization()
.next()
.and_then(|v| v.to_string().ok());
let name = if let Some(organization) = organization {
println!(
"found cert {:?} with organization {:?}",
common_name, organization
);
organization
} else {
println!(
"found cert {:?} but failed to get organization; falling back to displaying common name",
common_name
);
regex!(r"Apple Develop\w+: (.*) \(.+\)")
.captures(&common_name)
.map(|caps| caps[1].to_owned())
.unwrap_or_else(|| {
println!("regex failed to capture nice part of name in cert {:?}; falling back to displaying full name", common_name);
common_name.clone()
})
};
let id = cert
.subject_name()
.iter_organizational_unit()
.next()
.and_then(|v| v.to_string().ok())
.ok_or_else(|| anyhow::anyhow!("skipping cert {common_name}: missing Organization Unit"))?;
Ok(Self {
name,
certificate_name: common_name,
id,
cert_prefix,
})
}
pub fn certificate_name(&self) -> String {
self.certificate_name.clone()
}
}
pub fn list(keychain_path: &Path) -> Result<Vec<Team>> {
let certs = {
let mut certs = Vec::new();
for cert_prefix in [
"iOS Distribution:",
"Apple Distribution:",
"Developer ID Application:",
"Mac App Distribution:",
"Apple Development:",
"iOS App Development:",
"Mac Development:",
] {
let pem_list_out =
get_pem_list(keychain_path, cert_prefix).context("Failed to call `security` command")?;
let cert_list = X509Certificate::from_pem_multiple(pem_list_out.stdout)
.context("Failed to parse X509 cert")?;
certs.extend(cert_list.into_iter().map(|cert| (cert_prefix, cert)));
}
certs
};
Ok(
certs
.into_iter()
.flat_map(|(cert_prefix, cert)| {
Team::from_x509(cert_prefix, cert).map_err(|err| {
eprintln!("{}", err);
err
})
})
// Silly way to sort this and ensure no dupes
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
)
}

View File

@ -0,0 +1,251 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
ffi::{OsStr, OsString},
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, Result};
use serde::Deserialize;
mod keychain;
mod provisioning_profile;
pub use keychain::{Keychain, Team};
pub use provisioning_profile::ProvisioningProfile;
pub enum ApiKey {
Path(PathBuf),
Raw(Vec<u8>),
}
pub enum AppleNotarizationCredentials {
AppleId {
apple_id: OsString,
password: OsString,
team_id: OsString,
},
ApiKey {
issuer: OsString,
key_id: OsString,
key: ApiKey,
},
}
#[derive(Deserialize)]
struct NotarytoolSubmitOutput {
id: String,
status: String,
message: String,
}
pub fn notarize(
keychain: &Keychain,
app_bundle_path: &Path,
auth: &AppleNotarizationCredentials,
) -> Result<()> {
let bundle_stem = app_bundle_path
.file_stem()
.expect("failed to get bundle filename");
let tmp_dir = tempfile::tempdir()?;
let zip_path = tmp_dir
.path()
.join(format!("{}.zip", bundle_stem.to_string_lossy()));
let zip_args = vec![
"-c",
"-k",
"--keepParent",
"--sequesterRsrc",
app_bundle_path
.to_str()
.expect("failed to convert bundle_path to string"),
zip_path
.to_str()
.expect("failed to convert zip_path to string"),
];
// use ditto to create a PKZip almost identical to Finder
// this remove almost 99% of false alarm in notarization
assert_command(
Command::new("ditto").args(zip_args).status(),
"failed to zip app with ditto",
)?;
// sign the zip file
keychain.sign(&zip_path, None, false)?;
let notarize_args = vec![
"notarytool",
"submit",
zip_path
.to_str()
.expect("failed to convert zip_path to string"),
"--wait",
"--output-format",
"json",
];
println!("Notarizing {}", app_bundle_path.display());
let output = Command::new("xcrun")
.args(notarize_args)
.notarytool_args(auth, tmp_dir.path())?
.output()
.context("failed to upload app to Apple's notarization servers.")?;
if !output.status.success() {
return Err(anyhow::anyhow!("failed to notarize app"));
}
let output_str = String::from_utf8_lossy(&output.stdout);
if let Ok(submit_output) = serde_json::from_str::<NotarytoolSubmitOutput>(&output_str) {
let log_message = format!(
"Finished with status {} for id {} ({})",
submit_output.status, submit_output.id, submit_output.message
);
if submit_output.status == "Accepted" {
println!("Notarizing {}", log_message);
staple_app(app_bundle_path.to_path_buf())?;
Ok(())
} else if let Ok(output) = Command::new("xcrun")
.args(["notarytool", "log"])
.arg(&submit_output.id)
.notarytool_args(auth, tmp_dir.path())?
.output()
{
Err(anyhow::anyhow!(
"{log_message}\nLog:\n{}",
String::from_utf8_lossy(&output.stdout)
))
} else {
Err(anyhow::anyhow!("{log_message}"))
}
} else {
Err(anyhow::anyhow!(
"failed to parse notarytool output as JSON: `{output_str}`"
))
}
}
fn staple_app(mut app_bundle_path: PathBuf) -> Result<()> {
let app_bundle_path_clone = app_bundle_path.clone();
let filename = app_bundle_path_clone
.file_name()
.expect("failed to get bundle filename")
.to_str()
.expect("failed to convert bundle filename to string");
app_bundle_path.pop();
Command::new("xcrun")
.args(vec!["stapler", "staple", "-v", filename])
.current_dir(app_bundle_path)
.output()
.context("failed to staple app.")?;
Ok(())
}
pub trait NotarytoolCmdExt {
fn notarytool_args(
&mut self,
auth: &AppleNotarizationCredentials,
temp_dir: &Path,
) -> Result<&mut Self>;
}
impl NotarytoolCmdExt for Command {
fn notarytool_args(
&mut self,
auth: &AppleNotarizationCredentials,
temp_dir: &Path,
) -> Result<&mut Self> {
match auth {
AppleNotarizationCredentials::AppleId {
apple_id,
password,
team_id,
} => Ok(
self
.arg("--apple-id")
.arg(apple_id)
.arg("--password")
.arg(password)
.arg("--team-id")
.arg(team_id),
),
AppleNotarizationCredentials::ApiKey {
key,
key_id,
issuer,
} => {
let key_path = match key {
ApiKey::Raw(k) => {
let key_path = temp_dir.join("AuthKey.p8");
std::fs::write(&key_path, k)?;
key_path
}
ApiKey::Path(p) => p.to_owned(),
};
Ok(
self
.arg("--key-id")
.arg(key_id)
.arg("--key")
.arg(key_path)
.arg("--issuer")
.arg(issuer),
)
}
}
}
}
fn decode_base64(base64: &OsStr, out_path: &Path) -> Result<()> {
let tmp_dir = tempfile::tempdir()?;
let src_path = tmp_dir.path().join("src");
let base64 = base64
.to_str()
.expect("failed to convert base64 to string")
.as_bytes();
// as base64 contain whitespace decoding may be broken
// https://github.com/marshallpierce/rust-base64/issues/105
// we'll use builtin base64 command from the OS
std::fs::write(&src_path, base64)?;
assert_command(
std::process::Command::new("base64")
.arg("--decode")
.arg("-i")
.arg(&src_path)
.arg("-o")
.arg(out_path)
.status(),
"failed to decode certificate",
)?;
Ok(())
}
fn assert_command(
response: Result<std::process::ExitStatus, std::io::Error>,
error_message: &str,
) -> std::io::Result<()> {
let status =
response.map_err(|e| std::io::Error::new(e.kind(), format!("{error_message}: {e}")))?;
if !status.success() {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
error_message,
))
} else {
Ok(())
}
}

View File

@ -0,0 +1,52 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{ffi::OsStr, path::PathBuf, process::Command};
use anyhow::{Context, Result};
use rand::distributions::{Alphanumeric, DistString};
pub struct ProvisioningProfile {
path: PathBuf,
}
impl ProvisioningProfile {
pub fn from_base64(base64: &OsStr) -> Result<Self> {
let home_dir = dirs_next::home_dir().unwrap();
let provisioning_profiles_folder = home_dir
.join("Library")
.join("MobileDevice")
.join("Provisioning Profiles");
std::fs::create_dir_all(&provisioning_profiles_folder).unwrap();
let provisioning_profile_path = provisioning_profiles_folder.join(format!(
"{}.mobileprovision",
Alphanumeric.sample_string(&mut rand::thread_rng(), 16)
));
super::decode_base64(base64, &provisioning_profile_path)?;
Ok(Self {
path: provisioning_profile_path,
})
}
pub fn uuid(&self) -> Result<String> {
let output = Command::new("security")
.args(["cms", "-D", "-i"])
.arg(&self.path)
.output()?;
if !output.status.success() {
return Err(anyhow::anyhow!("failed to decode provisioning profile"));
}
let plist = plist::from_bytes::<plist::Dictionary>(&output.stdout)
.context("failed to decode provisioning profile as plist")?;
plist
.get("UUID")
.and_then(|v| v.as_string().map(ToString::to_string))
.ok_or_else(|| anyhow::anyhow!("could not find provisioning profile UUID"))
}
}