From 34515b70287df0b6e02b624ef84f70af3721102c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Tue, 2 Jul 2024 12:35:53 +0300 Subject: [PATCH] Fix exporting code signing certificates with OpenSSL 3.2.+ (#411) --- CHANGELOG.md | 12 ++ poetry.lock | 68 +++---- pyproject.toml | 4 +- src/codemagic/__version__.py | 2 +- .../models/certificate_p12_exporter.py | 179 ++++++++++++------ 5 files changed, 168 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83145200..ccb29fb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +Version 0.53.1 +------------- + +This bugfix release contains changes from [PR #411](https://github.com/codemagic-ci-cd/cli-tools/pull/411). + +**Bugfixes** +- Fix saving code signing certificates fetched from App Store Connect if the certificate subject line contains non-ascii characters and export is done using OpenSSL version 3.2+. + +**Dependencies**: +- Set lower bound version limit `>= 38.0.0` to [`cryptography`](https://cryptography.io/) dependency. + + Version 0.53.0 ------------- diff --git a/poetry.lock b/poetry.lock index 5e647700..cfe7e44e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -372,43 +372,43 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "42.0.5" +version = "42.0.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, - {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, - {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, - {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, - {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, - {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, - {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, - {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, ] [package.dependencies] @@ -1361,4 +1361,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "d2d6f58692d7850a79367d9b272238edd7127c7edf16d3bcdcda853c1562fe76" +content-hash = "78c58185f177a57381887346a6f7917b20a721a5b2a2ad924ab172117d8ac19e" diff --git a/pyproject.toml b/pyproject.toml index a4116d00..fda79c44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codemagic-cli-tools" -version = "0.53.0" +version = "0.53.1" description = "CLI tools used in Codemagic builds" readme = "README.md" authors = [ @@ -36,7 +36,7 @@ xcode-project = "codemagic.tools:XcodeProject.invoke_cli" [tool.poetry.dependencies] python = "^3.7" -cryptography = ">= 3.3, != 37.0.0" +cryptography = ">= 38.0.0" google-api-python-client = ">= 2.84.0" httplib2 = ">= 0.19.0" oauth2client = ">= 4.1.3" diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py index bdb502e1..8edf9152 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.0.dev" +__version__ = "0.53.1.dev" __url__ = "https://github.com/codemagic-ci-cd/cli-tools" __licence__ = "GNU General Public License v3.0" diff --git a/src/codemagic/models/certificate_p12_exporter.py b/src/codemagic/models/certificate_p12_exporter.py index 1099dd2a..d7371171 100644 --- a/src/codemagic/models/certificate_p12_exporter.py +++ b/src/codemagic/models/certificate_p12_exporter.py @@ -1,15 +1,24 @@ from __future__ import annotations -import os +import contextlib import pathlib +import re import shutil import subprocess import tempfile from typing import TYPE_CHECKING +from typing import AnyStr +from typing import Generator +from typing import Literal from typing import Optional from typing import Sequence from typing import Union +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives._serialization import PrivateFormat +from cryptography.hazmat.primitives.serialization import pkcs12 +from packaging.version import Version + from codemagic.mixins import RunningCliAppMixin from codemagic.mixins import StringConverterMixin @@ -18,22 +27,58 @@ from .private_key import PrivateKey -class P12Exporter(RunningCliAppMixin, StringConverterMixin): - def __init__(self, certificate: Certificate, private_key: PrivateKey, container_password: str): - self.container_password = container_password - self._temp_pem_certificate_path = self._save_to_disk("cert", certificate.as_pem()) - self._temp_private_key_path = self._save_to_disk("key", private_key.as_pem()) +class _OpenSsl: + def __init__(self): + self._executable = shutil.which("openssl") - @classmethod - def _save_to_disk(cls, prefix: str, pem: str): - with tempfile.NamedTemporaryFile(mode="w", prefix=f"{prefix}_", suffix=".pem", delete=False) as tf: - tf.write(pem) - return pathlib.Path(tf.name) + def ensure_installed(self): + if self._executable is None: + raise IOError("OpenSSL executable is not present on system") + + def get_version(self) -> Optional[Version]: + if not self._executable: + return None + + try: + version_output = subprocess.check_output([self._executable, "version"]) + except subprocess.CalledProcessError: + version_output = b"" + + version_match = re.search(r"\d+\.\d+\.\d+", version_output.decode(errors="ignore")) + if not version_match: + return None + + return Version(version_match.group()) + + @property + def no_encryption_flag(self) -> Literal["-nodes", "-noenc"]: + # Starting from OpenSSL version 3.0.0 `-nodes` is deprecated for disabling encryption + # when invoking `openssl pkcs12`. It is replaced with `-noenc`. + # See https://www.openssl.org/docs/man3.0/man1/openssl-pkcs12.html + + openssl_version = self.get_version() + if openssl_version and openssl_version < Version("3.0.0"): + # Use legacy flag only if we are sure that the version is earlier than 3.0.0 + return "-nodes" + return "-noenc" + + +class P12Exporter(RunningCliAppMixin, StringConverterMixin): + def __init__( + self, + certificate: Certificate, + private_key: PrivateKey, + container_password: Optional[str], + ): + self._password = container_password + self._certificate = certificate + self._private_key = private_key + self._openssl = _OpenSsl() - def _cleanup(self): - for path in (self._temp_pem_certificate_path, self._temp_private_key_path): - if path and path.exists(): - os.remove(str(path)) + @contextlib.contextmanager + def _temp_container(self) -> Generator[pathlib.Path, None, None]: + with tempfile.NamedTemporaryFile(prefix="certificate_", suffix=".p12") as tf: + yield pathlib.Path(tf.name) @classmethod def _get_export_path(cls, export_path: Optional[pathlib.Path]) -> pathlib.Path: @@ -42,17 +87,13 @@ def _get_export_path(cls, export_path: Optional[pathlib.Path]) -> pathlib.Path: with tempfile.NamedTemporaryFile(prefix="certificate", suffix=".p12", delete=False) as tf: return pathlib.Path(tf.name) - @classmethod - def _ensure_openssl(cls): - if shutil.which("openssl") is None: - raise IOError("OpenSSL executable is not present on system") - def _run_openssl_command(self, command: Sequence[Union[str, pathlib.Path]]): process = None cli_app = self.get_current_cli_app() try: if cli_app: - process = cli_app.execute(command, [command[-1]]) + obfuscate_patterns = [arg for arg in command if str(arg).startswith("pass:")] + process = cli_app.execute(command, obfuscate_patterns) process.raise_for_returncode() else: subprocess.check_output(command, stderr=subprocess.PIPE) @@ -63,52 +104,70 @@ def _run_openssl_command(self, command: Sequence[Union[str, pathlib.Path]]): error = "Unable to export certificate: Failed to create PKCS12 container" raise IOError(error, process) - def _create_pkcs12_container(self, pkcs12: pathlib.Path, password: str): + def create_encrypted_pkcs12_container(self, password: str) -> bytes: + # With OpenSSL 3.0.0+ the defaults for encryption when serializing PKCS12 + # have changed and some versions of Windows and macOS will not be able to + # read the new format. Maximum compatibility can be achieved by using SHA1 + # for MAC algorithm and PBESv1SHA1And3KeyTripleDESCBC for encryption algorithm. + if not password: - raise ValueError("Cannot export PKCS12 container without password") - - export_args = ( - "openssl", - "pkcs12", - "-export", - "-out", - pkcs12.expanduser(), - "-in", - self._temp_pem_certificate_path, - "-inkey", - self._temp_private_key_path, - "-passout", - f"pass:{password}", + raise ValueError("Cannot create encrypted PKCS12 container without password") + + encryption = ( + PrivateFormat.PKCS12.encryption_builder() + .kdf_rounds(50000) + .key_cert_algorithm(pkcs12.PBES.PBESv1SHA1And3KeyTripleDESCBC) + .hmac_hash(hashes.SHA1()) + .build(self._bytes(password)) + ) + + return pkcs12.serialize_key_and_certificates( + None, + self._private_key.cryptography_private_key, + self._certificate.certificate, + None, + encryption, ) - self._run_openssl_command(export_args) - def _decrypt_pkcs12_container(self, pkcs12: pathlib.Path, password: str): + def create_decrypted_pkcs12_container(self) -> bytes: + # Create an encrypted container with temporary password first as macOS keychain + # is not capable of handling non-encrypted PKCS#12 containers created by cryptography + # as those use SHA256 for HMAC hash function. + encrypted_pkcs12_container = self.create_encrypted_pkcs12_container("temporary-password") + + # Strip temporary password from the encrypted container to decrypt it + return self._decrypt_pkcs12_container(encrypted_pkcs12_container, "temporary-password") + + def _decrypt_pkcs12_container(self, pkcs12_container: bytes, password: AnyStr) -> bytes: if not password: raise ValueError("Cannot decrypt PKCS12 container without password") - decrypted_pkcs12 = pkcs12.parent / f"{pkcs12.stem}_decrypted{pkcs12.suffix}" - decrypt_args = ( - "openssl", - "pkcs12", - "-nodes", - "-passin", - f"pass:{password}", - "-in", - pkcs12, - "-out", - decrypted_pkcs12, - ) - self._run_openssl_command(decrypt_args) - decrypted_pkcs12.rename(pkcs12) + with self._temp_container() as encrypted, self._temp_container() as decrypted: + encrypted.write_bytes(pkcs12_container) + decrypt_args = ( + *("openssl", "pkcs12", self._openssl.no_encryption_flag), + *("-passin", f"pass:{self._str(password)}"), + *("-in", encrypted), # type: ignore + *("-out", decrypted), # type: ignore + ) + self._run_openssl_command(decrypt_args) + decrypted_container = decrypted.read_bytes() + + # Decrypted container is an ASCII text file that contains the PEM encoded + # certificate and key along with their bag attributes, which are optional. + # Remove subject line from bag attributes, which is not required but can + # contain unicode characters that are not supported by macOS keychain. + lines = decrypted_container.splitlines(keepends=True) + return b"".join(line for line in lines if not line.startswith(b"subject=")) def export(self, export_path: Optional[pathlib.Path] = None) -> pathlib.Path: - self._ensure_openssl() + self._openssl.ensure_installed() + + if self._password: + pkcs12_container = self.create_encrypted_pkcs12_container(self._password) + else: + pkcs12_container = self.create_decrypted_pkcs12_container() + p12_path = self._get_export_path(export_path) - password = self.container_password or "temporary-password" - try: - self._create_pkcs12_container(p12_path, password) - if not self.container_password: - self._decrypt_pkcs12_container(p12_path, password) - finally: - self._cleanup() + p12_path.write_bytes(pkcs12_container) return p12_path