snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 17:00:42 +05:00
commit feda81bf2f
1178 changed files with 156404 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
# Changelog
## \[2.3.4]
### Enhancements
- [`eacd36a4e`](https://www.github.com/tauri-apps/tauri/commit/eacd36a4ea4d6a14a73f414981fb5a8af7dfdafe) ([#15038](https://www.github.com/tauri-apps/tauri/pull/15038)) Do not rely on system base64 CLI to decode certificates.
## \[2.3.3]
### Dependencies
- [`268bb339f`](https://www.github.com/tauri-apps/tauri/commit/268bb339f0c512f021cc94e102573432cf2696d0) ([#14766](https://www.github.com/tauri-apps/tauri/pull/14766) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Remove once-cell-regex from direct dependencies.
## \[2.3.2]
### Dependencies
- [`514cf21e1`](https://www.github.com/tauri-apps/tauri/commit/514cf21e1417c7a78a0db494f891ba79d948b73d) ([#14591](https://www.github.com/tauri-apps/tauri/pull/14591) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) Update num-bigint-dig from 0.8.4 to 0.8.6
## \[2.3.1]
### Performance Improvements
- [`ee3cc4a91`](https://www.github.com/tauri-apps/tauri/commit/ee3cc4a91bf1315ecaefe90f423ffd55ef6c40db) ([#14475](https://www.github.com/tauri-apps/tauri/pull/14475) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) perf: remove needless clones in various files for improved performance. No user facing changes.
## \[2.3.0]
### Enhancements
- [`f59bf9d53`](https://www.github.com/tauri-apps/tauri/commit/f59bf9d5392ffd209e26ce5259c26d1acc31c4ba) ([#14337](https://www.github.com/tauri-apps/tauri/pull/14337) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) **Potentially breaking change:** Export custom Error enum instead of using anyhow. The changes happened in https://github.com/tauri-apps/tauri/pull/14126.
## \[2.2.0]
### New Features
- [`a9ec12843`](https://www.github.com/tauri-apps/tauri/commit/a9ec12843aa7d0eb774bd3a53e2e63da12cfa77b) ([#13521](https://www.github.com/tauri-apps/tauri/pull/13521) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added a `--skip-stapling` option to make `tauri build|bundle` *not* wait for notarization to finish on macOS.
## \[2.1.0]
### Enhancements
- [`d6520a21c`](https://www.github.com/tauri-apps/tauri/commit/d6520a21ce02c3e2be2955999946c2cb7bdb07aa) ([#12541](https://www.github.com/tauri-apps/tauri/pull/12541) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Updated `wry` to 0.50, `windows` to 0.60, `webview2-com` to 0.36, and `objc2` to 0.6. This can be a **breaking change** if you use the `with_webview` API!
## \[2.0.1]
### What's Changed
- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
## \[2.0.0]
### What's Changed
- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78.
- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable!
## \[0.1.1-rc.1]
### Enhancements
- [`f5d61822b`](https://www.github.com/tauri-apps/tauri/commit/f5d61822bf5988827776dd58bed75c19364e86bd) ([#11184](https://www.github.com/tauri-apps/tauri/pull/11184) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Added `Keychain::with_certificate_file` and `certificate::generate_self_signed`.
## \[0.1.1-rc.0]
### Bug Fixes
- [`1b0c447fc`](https://www.github.com/tauri-apps/tauri/commit/1b0c447fcbc424e08e4260277ec178df86f45d1d) ([#10654](https://www.github.com/tauri-apps/tauri/pull/10654) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fixes output not visible when running on Node.js via NAPI.
## \[0.1.0-beta.0]
### New Features
- [`7c7fa0964`](https://www.github.com/tauri-apps/tauri/commit/7c7fa0964db3403037fdb9a34de2b877ddb8df1c) ([#9963](https://www.github.com/tauri-apps/tauri/pull/9963) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Initial release.

View File

@@ -0,0 +1,32 @@
[package]
name = "tauri-macos-sign"
version = "2.3.4"
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 = "2024"
rust-version = "1.90"
[dependencies]
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tempfile = "3"
# This must match the version apple-codesign uses
x509-certificate = "0.24"
once_cell = "1"
regex = "1"
os_pipe = "1"
plist = "1"
rand = "0.10"
dirs = "7"
log = { version = "0.4.21", features = ["kv"] }
apple-codesign = { version = "0.29", default-features = false }
chrono = "0.4"
p12 = "0.6"
base64 = "0.22"
[lints]
workspace = true

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,71 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use apple_codesign::create_self_signed_code_signing_certificate;
use x509_certificate::{EcdsaCurve, KeyAlgorithm};
pub use apple_codesign::CertificateProfile;
use crate::{Error, Result};
/// 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,
)
.map_err(|error| Error::FailedToCreateSelfSignedCertificate {
error: Box::new(error),
})?;
let pfx = p12::PFX::new(
&cert
.encode_der()
.map_err(|error| Error::FailedToEncodeDER { error })?,
&key_pair.to_pkcs8_one_asymmetric_key_der(),
None,
&request.password,
"code-signing",
)
.ok_or(Error::FailedToCreatePFX)?;
let der = pfx.to_der();
Ok(der)
}

View File

@@ -0,0 +1,254 @@
// 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::{CommandExt, Error, Result, assert_command};
use rand::distr::{Alphanumeric, SampleString};
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)
.piped();
}
}
}
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 tmp_dir = tempfile::tempdir().map_err(Error::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::home_dir().ok_or(Error::ResolveHomeDir)?;
let keychain_path = home_dir.join("Library").join("Keychains").join(format!(
"{}.keychain-db",
Alphanumeric.sample_string(&mut rand::rng(), 16)
));
let keychain_password = Alphanumeric.sample_string(&mut rand::rng(), 16);
let keychain_list_output = Command::new("security")
.args(["list-keychain", "-d", "user"])
.output()
.map_err(|e| Error::CommandFailed {
command: "security list-keychain -d user".to_string(),
error: e,
})?;
assert_command(
Command::new("security")
.args(["create-keychain", "-p", &keychain_password])
.arg(&keychain_path)
.piped(),
"failed to create keychain",
)
.map_err(|error| Error::CommandFailed {
command: "security create-Keychain".to_string(),
error,
})?;
assert_command(
Command::new("security")
.args(["unlock-keychain", "-p", &keychain_password])
.arg(&keychain_path)
.piped(),
"failed to set unlock keychain",
)
.map_err(|error| Error::CommandFailed {
command: "security unlock-keychain".to_string(),
error,
})?;
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)
.piped(),
"failed to import keychain certificate",
)
.map_err(|error| Error::CommandFailed {
command: "security import".to_string(),
error,
})?;
assert_command(
Command::new("security")
.args(["set-keychain-settings", "-t", "3600", "-u"])
.arg(&keychain_path)
.piped(),
"failed to set keychain settings",
)
.map_err(|error| Error::CommandFailed {
command: "security set-keychain-settings".to_string(),
error,
})?;
assert_command(
Command::new("security")
.args([
"set-key-partition-list",
"-S",
"apple-tool:,apple:,codesign:",
"-s",
"-k",
&keychain_password,
])
.arg(&keychain_path)
.piped(),
"failed to set keychain settings",
)
.map_err(|error| Error::CommandFailed {
command: "security set-key-partition-list".to_string(),
error,
})?;
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)
.piped(),
"failed to list keychain",
)
.map_err(|error| Error::CommandFailed {
command: "security list-keychain".to_string(),
error,
})?;
let signing_identity = identity::list(&keychain_path)
.map(|l| l.first().cloned())?
.ok_or(Error::ResolveSigningIdentity)?;
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.piped(), "failed to sign app").map_err(|error| {
Error::CommandFailed {
command: "codesign".to_string(),
error,
}
})?;
Ok(())
}
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
}

View File

@@ -0,0 +1,120 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use once_cell::sync::OnceCell;
use regex::Regex;
use std::{collections::BTreeSet, path::Path, process::Command};
use x509_certificate::certificate::X509Certificate;
use crate::{Error, Result};
fn get_pem_list(keychain_path: &Path, name_substr: &str) -> Result<std::process::Output> {
Command::new("security")
.arg("find-certificate")
.args(["-p", "-a"])
.arg("-c")
.arg(name_substr)
.arg(keychain_path)
.stdin(os_pipe::dup_stdin().unwrap())
.stderr(os_pipe::dup_stderr().unwrap())
.output()
.map_err(|error| Error::CommandFailed {
command: "security find-certificate".to_string(),
error,
})
}
#[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(Error::CertificateMissingCommonName)?;
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 {common_name:?} with organization {organization:?}");
organization
} else {
println!(
"found cert {common_name:?} but failed to get organization; falling back to displaying common name"
);
static APPLE_DEV: OnceCell<Regex> = OnceCell::new();
APPLE_DEV.get_or_init(|| Regex::new(r"Apple Develop\w+: (.*) \(.+\)").unwrap())
.captures(&common_name)
.map(|caps| caps[1].to_owned())
.unwrap_or_else(|| {
println!("regex failed to capture nice part of name in cert {common_name:?}; falling back to displaying full name");
common_name.clone()
})
};
let id = cert
.subject_name()
.iter_organizational_unit()
.next()
.and_then(|v| v.to_string().ok())
.ok_or_else(|| Error::CertificateMissingOrganizationUnit {
common_name: common_name.clone(),
})?;
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)?;
let cert_list = X509Certificate::from_pem_multiple(pem_list_out.stdout)
.map_err(|error| Error::X509Certificate { error })?;
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| {
log::error!("{err}");
err
})
})
// Silly way to sort this and ensure no dupes
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
)
}

View File

@@ -0,0 +1,368 @@
// 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, ExitStatus},
};
use serde::Deserialize;
pub mod certificate;
mod keychain;
mod provisioning_profile;
pub use keychain::{Keychain, Team};
pub use provisioning_profile::ProvisioningProfile;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to create temp directory: {0}")]
TempDir(std::io::Error),
#[error("failed to resolve home dir")]
ResolveHomeDir,
#[error("failed to resolve signing identity")]
ResolveSigningIdentity,
#[error("failed to decode provisioning profile")]
FailedToDecodeProvisioningProfile,
#[error("could not find provisioning profile UUID")]
FailedToFindProvisioningProfileUuid,
#[error("{context} {path}: {error}")]
Plist {
context: &'static str,
path: PathBuf,
error: plist::Error,
},
#[error("failed to upload app to Apple's notarization servers: {error}")]
FailedToUploadApp { error: std::io::Error },
#[error("failed to notarize app: {0}")]
Notarize(String),
#[error("failed to parse notarytool output as JSON: {output}")]
ParseNotarytoolOutput { output: String },
#[error("failed to run command {command}: {error}")]
CommandFailed {
command: String,
error: std::io::Error,
},
#[error("{context} {path}: {error}")]
Fs {
context: &'static str,
path: PathBuf,
error: std::io::Error,
},
#[error("failed to parse X509 certificate: {error}")]
X509Certificate {
error: x509_certificate::X509CertificateError,
},
#[error("failed to create PFX from self signed certificate")]
FailedToCreatePFX,
#[error("failed to create self signed certificate: {error}")]
FailedToCreateSelfSignedCertificate {
error: Box<apple_codesign::AppleCodesignError>,
},
#[error("failed to encode DER: {error}")]
FailedToEncodeDER { error: std::io::Error },
#[error("failed to decode base64 certificate: {0}")]
Base64Decode(base64::DecodeError),
#[error("certificate missing common name")]
CertificateMissingCommonName,
#[error("certificate missing organization unit for common name {common_name}")]
CertificateMissingOrganizationUnit { common_name: String },
}
pub type Result<T> = std::result::Result<T, Error>;
trait CommandExt {
// The `pipe` function sets the stdout and stderr to properly
// show the command output in the Node.js wrapper.
fn piped(&mut self) -> std::io::Result<ExitStatus>;
}
impl CommandExt for Command {
fn piped(&mut self) -> std::io::Result<ExitStatus> {
self.stdin(os_pipe::dup_stdin()?);
self.stdout(os_pipe::dup_stdout()?);
self.stderr(os_pipe::dup_stderr()?);
let program = self.get_program().to_string_lossy().into_owned();
log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
self.status()
}
}
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,
#[serde(default)]
status: Option<String>,
message: String,
}
pub fn notarize(
keychain: &Keychain,
app_bundle_path: &Path,
auth: &AppleNotarizationCredentials,
) -> Result<()> {
notarize_inner(keychain, app_bundle_path, auth, true)
}
pub fn notarize_without_stapling(
keychain: &Keychain,
app_bundle_path: &Path,
auth: &AppleNotarizationCredentials,
) -> Result<()> {
notarize_inner(keychain, app_bundle_path, auth, false)
}
fn notarize_inner(
keychain: &Keychain,
app_bundle_path: &Path,
auth: &AppleNotarizationCredentials,
wait: bool,
) -> Result<()> {
let bundle_stem = app_bundle_path
.file_stem()
.expect("failed to get bundle filename");
let tmp_dir = tempfile::tempdir().map_err(Error::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).piped(),
"failed to zip app with ditto",
)
.map_err(|error| Error::CommandFailed {
command: "ditto".to_string(),
error,
})?;
// sign the zip file
keychain.sign(&zip_path, None, false)?;
let mut notarize_args = vec![
"notarytool",
"submit",
zip_path
.to_str()
.expect("failed to convert zip_path to string"),
"--output-format",
"json",
];
if wait {
notarize_args.push("--wait");
}
let notarize_args = notarize_args;
println!("Notarizing {}", app_bundle_path.display());
let output = Command::new("xcrun")
.args(notarize_args)
.notarytool_args(auth, tmp_dir.path())?
.output()
.map_err(|error| Error::FailedToUploadApp { error })?;
if !output.status.success() {
return Err(Error::Notarize(
String::from_utf8_lossy(&output.stderr).into_owned(),
));
}
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!(
"{} with status {} for id {} ({})",
if wait { "Finished" } else { "Submitted" },
submit_output.status.as_deref().unwrap_or("Pending"),
submit_output.id,
submit_output.message
);
// status is empty when not waiting for the notarization to finish
if submit_output.status.map_or(!wait, |s| s == "Accepted") {
println!("Notarizing {log_message}");
if wait {
println!("Stapling app...");
staple_app(app_bundle_path.to_path_buf())?;
} else {
println!("Not waiting for notarization to finish.");
println!("You can use `xcrun notarytool log` to check the notarization progress.");
println!(
"When it's done you can optionally staple your app via `xcrun stapler staple {}`",
app_bundle_path.display()
);
}
Ok(())
} else if let Ok(output) = Command::new("xcrun")
.args(["notarytool", "log"])
.arg(&submit_output.id)
.notarytool_args(auth, tmp_dir.path())?
.output()
{
Err(Error::Notarize(format!(
"{log_message}\nLog:\n{}",
String::from_utf8_lossy(&output.stdout)
)))
} else {
Err(Error::Notarize(log_message))
}
} else {
Err(Error::ParseNotarytoolOutput {
output: output_str.into_owned(),
})
}
}
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()
.map_err(|error| Error::CommandFailed {
command: "xcrun stapler staple".to_string(),
error,
})?;
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).map_err(|error| Error::Fs {
context: "failed to write notarization API key to temp file",
path: key_path.clone(),
error,
})?;
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_input: &OsStr, out_path: &Path) -> Result<()> {
use base64::Engine;
let input = base64_input
.to_str()
.expect("failed to convert base64 to string");
// strip whitespace before decoding
let cleaned: String = input.chars().filter(|c| !c.is_ascii_whitespace()).collect();
let decoded = base64::engine::general_purpose::STANDARD
.decode(&cleaned)
.map_err(Error::Base64Decode)?;
std::fs::write(out_path, &decoded).map_err(|error| Error::Fs {
context: "failed to write decoded certificate",
path: out_path.to_path_buf(),
error,
})?;
Ok(())
}
fn assert_command(
response: std::result::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::other(error_message))
} else {
Ok(())
}
}

View File

@@ -0,0 +1,64 @@
// 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 crate::{Error, Result};
use rand::distr::{Alphanumeric, SampleString};
pub struct ProvisioningProfile {
path: PathBuf,
}
impl ProvisioningProfile {
pub fn from_base64(base64: &OsStr) -> Result<Self> {
let home_dir = dirs::home_dir().ok_or(Error::ResolveHomeDir)?;
let provisioning_profiles_folder = home_dir
.join("Library")
.join("MobileDevice")
.join("Provisioning Profiles");
std::fs::create_dir_all(&provisioning_profiles_folder).map_err(|error| Error::Fs {
context: "failed to create provisioning profiles folder",
path: provisioning_profiles_folder.clone(),
error,
})?;
let provisioning_profile_path = provisioning_profiles_folder.join(format!(
"{}.mobileprovision",
Alphanumeric.sample_string(&mut rand::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()
.map_err(|error| Error::CommandFailed {
command: "security cms -D -i".to_string(),
error,
})?;
if !output.status.success() {
return Err(Error::FailedToDecodeProvisioningProfile);
}
let plist =
plist::from_bytes::<plist::Dictionary>(&output.stdout).map_err(|error| Error::Plist {
context: "failed to parse provisioning profile as plist",
path: self.path.clone(),
error,
})?;
plist
.get("UUID")
.and_then(|v| v.as_string().map(ToString::to_string))
.ok_or(Error::FailedToFindProvisioningProfileUuid)
}
}