From 906eac152bf736897aac47e9b51f70602f699b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Fri, 20 Sep 2024 17:00:24 +0300 Subject: [PATCH] Fix `keychain add-certificates` on macOS 15 (#428) --- CHANGELOG.md | 6 +++ pyproject.toml | 2 +- src/codemagic/__version__.py | 2 +- src/codemagic/tools/keychain.py | 66 ++++++++++++++++++++++++++------- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6faeec45..f5d09564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Version 0.53.8 +------------- + +**Bugfixes** +- Fix action `keychain add-certificates` on macOS 15.0. [PR #428](https://github.com/codemagic-ci-cd/cli-tools/pull/428) + Version 0.53.7 ------------- diff --git a/pyproject.toml b/pyproject.toml index 135e3c56..baa681df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codemagic-cli-tools" -version = "0.53.7" +version = "0.53.8" description = "CLI tools used in Codemagic builds" readme = "README.md" authors = [ diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py index 53ae081d..b400f924 100644 --- a/src/codemagic/__version__.py +++ b/src/codemagic/__version__.py @@ -1,5 +1,5 @@ __title__ = "codemagic-cli-tools" __description__ = "CLI tools used in Codemagic builds" -__version__ = "0.53.7.dev" +__version__ = "0.53.8.dev" __url__ = "https://github.com/codemagic-ci-cd/cli-tools" __licence__ = "GNU General Public License v3.0" diff --git a/src/codemagic/tools/keychain.py b/src/codemagic/tools/keychain.py index ee48de65..1f7a76a6 100755 --- a/src/codemagic/tools/keychain.py +++ b/src/codemagic/tools/keychain.py @@ -7,6 +7,7 @@ import shutil from datetime import datetime from tempfile import NamedTemporaryFile +from typing import TYPE_CHECKING from typing import Iterable from typing import List from typing import Optional @@ -18,6 +19,9 @@ from codemagic.mixins import PathFinderMixin from codemagic.models import Certificate +if TYPE_CHECKING: + from typing_extensions import Literal + class Seconds(int): pass @@ -33,6 +37,10 @@ class KeychainError(cli.CliAppException): pass +class _CertificateDataDecodeError(IOError): + pass + + class KeychainArgument(cli.Argument): PATH = cli.ArgumentProperties( flags=("-p", "--path"), @@ -375,6 +383,34 @@ def _add_certificate( allowed_applications: Sequence[str] = tuple(), ): self.logger.info(f"Add certificate {certificate_path} to keychain {self.path}") + + try: + self._run_add_certificate_process( + certificate_path=certificate_path, + certificate_password=certificate_password, + allow_for_all_apps=allow_for_all_apps, + allowed_applications=allowed_applications, + import_format="pkcs12", + ) + except _CertificateDataDecodeError: + # Attempt import again, but now using different format specifier. + self._run_add_certificate_process( + certificate_path=certificate_path, + certificate_password=certificate_password, + allow_for_all_apps=allow_for_all_apps, + allowed_applications=allowed_applications, + import_format="openssl", + ) + + def _run_add_certificate_process( + self, + *, + certificate_path: pathlib.Path, + certificate_password: Optional[str] = None, + allow_for_all_apps: bool = False, + allowed_applications: Sequence[str] = tuple(), + import_format: Literal["pkcs12", "openssl"] = "pkcs12", + ): # If case of no password, we need to explicitly set -P '' flag. Otherwise, # security tries to open an interactive dialog to prompt the user for a password, # which fails in non-interactive CI environment. @@ -385,15 +421,10 @@ def _add_certificate( obfuscate_patterns = [] import_cmd = [ - "security", - "import", - certificate_path, - "-f", - "pkcs12", - "-k", - self.path, - "-P", - certificate_password, + *("security", "import", certificate_path), + *("-f", import_format), + *("-k", self.path), + *("-P", certificate_password), ] if allow_for_all_apps: import_cmd.append("-A") @@ -402,11 +433,18 @@ def _add_certificate( process = self.execute(import_cmd, obfuscate_patterns=obfuscate_patterns) - if process.returncode != 0: - if "The specified item already exists in the keychain" in process.stderr: - pass # It is fine that the certificate is already in keychain - else: - raise KeychainError(f"Unable to add certificate {certificate_path} to keychain {self.path}", process) + if process.returncode == 0: + return + elif "The specified item already exists in the keychain" in process.stderr: + # It is fine that the certificate is already in keychain + pass + elif import_format == "pkcs12" and "Unable to decode the provided data" in process.stderr: + # MacOS has not been very compliant with unencrypted PEM-formatted PKCS#12 + # containers generated by OpenSSL. But starting from macOS 15.0 security + # just rejects them with error message "Unable to decode the provided data". + raise _CertificateDataDecodeError() + else: + raise KeychainError(f"Unable to add certificate {certificate_path} to keychain {self.path}", process) def _find_certificates(self): process = self.execute(("security", "find-certificate", "-a", "-p", self.path), show_output=False)