Skip to content

Commit

Permalink
Fix keychain add-certificates on macOS 15 (#428)
Browse files Browse the repository at this point in the history
  • Loading branch information
priitlatt authored Sep 20, 2024
1 parent ff87078 commit 906eac1
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 16 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
-------------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion src/codemagic/__version__.py
Original file line number Diff line number Diff line change
@@ -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"
66 changes: 52 additions & 14 deletions src/codemagic/tools/keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -33,6 +37,10 @@ class KeychainError(cli.CliAppException):
pass


class _CertificateDataDecodeError(IOError):
pass


class KeychainArgument(cli.Argument):
PATH = cli.ArgumentProperties(
flags=("-p", "--path"),
Expand Down Expand Up @@ -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.
Expand All @@ -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")
Expand All @@ -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)
Expand Down

0 comments on commit 906eac1

Please sign in to comment.