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
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:
74
crates/tauri-macos-sign/CHANGELOG.md
Normal file
74
crates/tauri-macos-sign/CHANGELOG.md
Normal 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.
|
||||
32
crates/tauri-macos-sign/Cargo.toml
Normal file
32
crates/tauri-macos-sign/Cargo.toml
Normal 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
|
||||
3
crates/tauri-macos-sign/README.md
Normal file
3
crates/tauri-macos-sign/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Tauri MacOS Sign
|
||||
|
||||
Utilities for setting up macOS certificates, code signing and notarization for macOS and iOS apps.
|
||||
71
crates/tauri-macos-sign/src/certificate.rs
Normal file
71
crates/tauri-macos-sign/src/certificate.rs
Normal 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)
|
||||
}
|
||||
254
crates/tauri-macos-sign/src/keychain.rs
Normal file
254
crates/tauri-macos-sign/src/keychain.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
120
crates/tauri-macos-sign/src/keychain/identity.rs
Normal file
120
crates/tauri-macos-sign/src/keychain/identity.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
368
crates/tauri-macos-sign/src/lib.rs
Normal file
368
crates/tauri-macos-sign/src/lib.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
64
crates/tauri-macos-sign/src/provisioning_profile.rs
Normal file
64
crates/tauri-macos-sign/src/provisioning_profile.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user