lib.rs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. ffi::{OsStr, OsString},
  6. path::{Path, PathBuf},
  7. process::{Command, ExitStatus},
  8. };
  9. use anyhow::{Context, Result};
  10. use serde::Deserialize;
  11. pub mod certificate;
  12. mod keychain;
  13. mod provisioning_profile;
  14. pub use keychain::{Keychain, Team};
  15. pub use provisioning_profile::ProvisioningProfile;
  16. trait CommandExt {
  17. // The `pipe` function sets the stdout and stderr to properly
  18. // show the command output in the Node.js wrapper.
  19. fn piped(&mut self) -> std::io::Result<ExitStatus>;
  20. }
  21. impl CommandExt for Command {
  22. fn piped(&mut self) -> std::io::Result<ExitStatus> {
  23. self.stdin(os_pipe::dup_stdin()?);
  24. self.stdout(os_pipe::dup_stdout()?);
  25. self.stderr(os_pipe::dup_stderr()?);
  26. let program = self.get_program().to_string_lossy().into_owned();
  27. log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
  28. self.status().map_err(Into::into)
  29. }
  30. }
  31. pub enum ApiKey {
  32. Path(PathBuf),
  33. Raw(Vec<u8>),
  34. }
  35. pub enum AppleNotarizationCredentials {
  36. AppleId {
  37. apple_id: OsString,
  38. password: OsString,
  39. team_id: OsString,
  40. },
  41. ApiKey {
  42. issuer: OsString,
  43. key_id: OsString,
  44. key: ApiKey,
  45. },
  46. }
  47. #[derive(Deserialize)]
  48. struct NotarytoolSubmitOutput {
  49. id: String,
  50. status: String,
  51. message: String,
  52. }
  53. pub fn notarize(
  54. keychain: &Keychain,
  55. app_bundle_path: &Path,
  56. auth: &AppleNotarizationCredentials,
  57. ) -> Result<()> {
  58. let bundle_stem = app_bundle_path
  59. .file_stem()
  60. .expect("failed to get bundle filename");
  61. let tmp_dir = tempfile::tempdir()?;
  62. let zip_path = tmp_dir
  63. .path()
  64. .join(format!("{}.zip", bundle_stem.to_string_lossy()));
  65. let zip_args = vec![
  66. "-c",
  67. "-k",
  68. "--keepParent",
  69. "--sequesterRsrc",
  70. app_bundle_path
  71. .to_str()
  72. .expect("failed to convert bundle_path to string"),
  73. zip_path
  74. .to_str()
  75. .expect("failed to convert zip_path to string"),
  76. ];
  77. // use ditto to create a PKZip almost identical to Finder
  78. // this remove almost 99% of false alarm in notarization
  79. assert_command(
  80. Command::new("ditto").args(zip_args).piped(),
  81. "failed to zip app with ditto",
  82. )?;
  83. // sign the zip file
  84. keychain.sign(&zip_path, None, false)?;
  85. let notarize_args = vec![
  86. "notarytool",
  87. "submit",
  88. zip_path
  89. .to_str()
  90. .expect("failed to convert zip_path to string"),
  91. "--wait",
  92. "--output-format",
  93. "json",
  94. ];
  95. println!("Notarizing {}", app_bundle_path.display());
  96. let output = Command::new("xcrun")
  97. .args(notarize_args)
  98. .notarytool_args(auth, tmp_dir.path())?
  99. .output()
  100. .context("failed to upload app to Apple's notarization servers.")?;
  101. if !output.status.success() {
  102. return Err(
  103. anyhow::anyhow!("failed to notarize app")
  104. .context(String::from_utf8_lossy(&output.stderr).into_owned()),
  105. );
  106. }
  107. let output_str = String::from_utf8_lossy(&output.stdout);
  108. if let Ok(submit_output) = serde_json::from_str::<NotarytoolSubmitOutput>(&output_str) {
  109. let log_message = format!(
  110. "Finished with status {} for id {} ({})",
  111. submit_output.status, submit_output.id, submit_output.message
  112. );
  113. if submit_output.status == "Accepted" {
  114. println!("Notarizing {}", log_message);
  115. staple_app(app_bundle_path.to_path_buf())?;
  116. Ok(())
  117. } else if let Ok(output) = Command::new("xcrun")
  118. .args(["notarytool", "log"])
  119. .arg(&submit_output.id)
  120. .notarytool_args(auth, tmp_dir.path())?
  121. .output()
  122. {
  123. Err(anyhow::anyhow!(
  124. "{log_message}\nLog:\n{}",
  125. String::from_utf8_lossy(&output.stdout)
  126. ))
  127. } else {
  128. Err(anyhow::anyhow!("{log_message}"))
  129. }
  130. } else {
  131. Err(anyhow::anyhow!(
  132. "failed to parse notarytool output as JSON: `{output_str}`"
  133. ))
  134. }
  135. }
  136. fn staple_app(mut app_bundle_path: PathBuf) -> Result<()> {
  137. let app_bundle_path_clone = app_bundle_path.clone();
  138. let filename = app_bundle_path_clone
  139. .file_name()
  140. .expect("failed to get bundle filename")
  141. .to_str()
  142. .expect("failed to convert bundle filename to string");
  143. app_bundle_path.pop();
  144. Command::new("xcrun")
  145. .args(vec!["stapler", "staple", "-v", filename])
  146. .current_dir(app_bundle_path)
  147. .output()
  148. .context("failed to staple app.")?;
  149. Ok(())
  150. }
  151. pub trait NotarytoolCmdExt {
  152. fn notarytool_args(
  153. &mut self,
  154. auth: &AppleNotarizationCredentials,
  155. temp_dir: &Path,
  156. ) -> Result<&mut Self>;
  157. }
  158. impl NotarytoolCmdExt for Command {
  159. fn notarytool_args(
  160. &mut self,
  161. auth: &AppleNotarizationCredentials,
  162. temp_dir: &Path,
  163. ) -> Result<&mut Self> {
  164. match auth {
  165. AppleNotarizationCredentials::AppleId {
  166. apple_id,
  167. password,
  168. team_id,
  169. } => Ok(
  170. self
  171. .arg("--apple-id")
  172. .arg(apple_id)
  173. .arg("--password")
  174. .arg(password)
  175. .arg("--team-id")
  176. .arg(team_id),
  177. ),
  178. AppleNotarizationCredentials::ApiKey {
  179. key,
  180. key_id,
  181. issuer,
  182. } => {
  183. let key_path = match key {
  184. ApiKey::Raw(k) => {
  185. let key_path = temp_dir.join("AuthKey.p8");
  186. std::fs::write(&key_path, k)?;
  187. key_path
  188. }
  189. ApiKey::Path(p) => p.to_owned(),
  190. };
  191. Ok(
  192. self
  193. .arg("--key-id")
  194. .arg(key_id)
  195. .arg("--key")
  196. .arg(key_path)
  197. .arg("--issuer")
  198. .arg(issuer),
  199. )
  200. }
  201. }
  202. }
  203. }
  204. fn decode_base64(base64: &OsStr, out_path: &Path) -> Result<()> {
  205. let tmp_dir = tempfile::tempdir()?;
  206. let src_path = tmp_dir.path().join("src");
  207. let base64 = base64
  208. .to_str()
  209. .expect("failed to convert base64 to string")
  210. .as_bytes();
  211. // as base64 contain whitespace decoding may be broken
  212. // https://github.com/marshallpierce/rust-base64/issues/105
  213. // we'll use builtin base64 command from the OS
  214. std::fs::write(&src_path, base64)?;
  215. assert_command(
  216. std::process::Command::new("base64")
  217. .arg("--decode")
  218. .arg("-i")
  219. .arg(&src_path)
  220. .arg("-o")
  221. .arg(out_path)
  222. .piped(),
  223. "failed to decode certificate",
  224. )?;
  225. Ok(())
  226. }
  227. fn assert_command(
  228. response: Result<std::process::ExitStatus, std::io::Error>,
  229. error_message: &str,
  230. ) -> std::io::Result<()> {
  231. let status =
  232. response.map_err(|e| std::io::Error::new(e.kind(), format!("{error_message}: {e}")))?;
  233. if !status.success() {
  234. Err(std::io::Error::new(
  235. std::io::ErrorKind::Other,
  236. error_message,
  237. ))
  238. } else {
  239. Ok(())
  240. }
  241. }