From 16cfd7880310103479e38ef4e181e04835adffd2 Mon Sep 17 00:00:00 2001 From: Kain Date: Sun, 12 Jul 2026 15:14:58 -0700 Subject: [PATCH] fix: normalize Apple .p8 EC key PEM header/footer on load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple provides EC private keys in PKCS#8 PEM format (-----BEGIN PRIVATE KEY-----). The raw key bytes passed directly to jwt.encode() can fail in some PyJWT or cryptography releases due to sensitivity to the exact PEM header/footer wording or encoding. Add a _load_ec_key() helper that loads the key through cryptography's load_pem_private_key() — which handles both PKCS#8 and SEC1/EC PEM formats transparently — and re-serialises it to a consistent PKCS#8 PEM byte string before handing it to PyJWT. --- pyaxm/auth.py | 26 ++++++++++++++++++++++++-- pyproject.toml | 1 + requirements.txt | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pyaxm/auth.py b/pyaxm/auth.py index 738a19c..5a5b028 100644 --- a/pyaxm/auth.py +++ b/pyaxm/auth.py @@ -5,6 +5,8 @@ from dataclasses import dataclass import jwt +from cryptography.hazmat.primitives import serialization as crypto_serialization +from cryptography.hazmat.primitives.serialization import load_pem_private_key @dataclass @@ -35,6 +37,27 @@ def __init__( self.token_path = token_path self.access_token = self._get_or_refresh_token() + def _load_ec_key(self) -> bytes: + """Load and normalize an Apple .p8 EC private key. + + Apple provides EC private keys in PKCS#8 PEM format + (``-----BEGIN PRIVATE KEY-----``). Some PyJWT or + ``cryptography`` releases can be sensitive to the exact PEM + header / footer wording or encoding. To avoid any such issues, + this helper loads the key with ``cryptography`` (which + transparently handles both PKCS#8 and SEC1 / EC PEM formats) + and re-serialises it back to a consistent PKCS#8 PEM byte + string. + """ + with open(self.key_path, "rb") as f: + key_data = f.read() + private_key = load_pem_private_key(key_data, password=None) + return private_key.private_bytes( + encoding=crypto_serialization.Encoding.PEM, + format=crypto_serialization.PrivateFormat.PKCS8, + encryption_algorithm=crypto_serialization.NoEncryption(), + ) + def _generate_assertion(self) -> str: issued_at = int(dt.datetime.now(dt.timezone.utc).timestamp()) expires_at = issued_at + 60 @@ -50,8 +73,7 @@ def _generate_assertion(self) -> str: "jti": str(uuid.uuid4()), "iss": self.axm_client_id, } - with open(self.key_path, "rb") as f: - key = f.read() + key = self._load_ec_key() return jwt.encode(payload, key, algorithm="ES256", headers=headers) def _get_or_refresh_token(self) -> AccessToken: diff --git a/pyproject.toml b/pyproject.toml index 5baf51d..20fc9f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ version = "2026.5.18" description = "Query Apple Business Manager using Python" requires-python = ">=3.10" dependencies = [ + "cryptography", "pydantic", "requests", "pyjwt", diff --git a/requirements.txt b/requirements.txt index 20a8eb7..0a7aa00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +cryptography pydantic requests pyjwt