From 915a51ded1acab676e5c9bbe21008f73fd634f23 Mon Sep 17 00:00:00 2001 From: Vit Curda Date: Tue, 24 Jun 2025 11:24:37 +0000 Subject: [PATCH 1/2] certificate made optionall --- msal/application.py | 31 +++++++++++++++++++++---------- msal/sku.py | 2 +- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/msal/application.py b/msal/application.py index 24ef91d7..06a14f2b 100644 --- a/msal/application.py +++ b/msal/application.py @@ -66,10 +66,19 @@ def _str2bytes(raw): except: return raw +def _extract_cert_and_thumbprints(private_key, cert): + # Cert concepts https://security.stackexchange.com/a/226758/125264 + from cryptography.hazmat.primitives import hashes, serialization + cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM).decode() + x5c = [ + '\n'.join(cert_pem.splitlines()[1:-1]) + ] + sha256_thumbprint = cert.fingerprint(hashes.SHA256()).hex() + sha1_thumbprint = cert.fingerprint(hashes.SHA1()).hex() + return private_key, sha256_thumbprint, sha1_thumbprint, x5c def _parse_pfx(pfx_path, passphrase_bytes): # Cert concepts https://security.stackexchange.com/a/226758/125264 - from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.serialization import pkcs12 with open(pfx_path, 'rb') as f: private_key, cert, _ = pkcs12.load_key_and_certificates( # cryptography 2.5+ @@ -77,14 +86,7 @@ def _parse_pfx(pfx_path, passphrase_bytes): f.read(), passphrase_bytes) if not (private_key and cert): raise ValueError("Your PFX file shall contain both private key and cert") - cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM).decode() # cryptography 1.0+ - x5c = [ - '\n'.join(cert_pem.splitlines()[1:-1]) # Strip the "--- header ---" and "--- footer ---" - ] - sha256_thumbprint = cert.fingerprint(hashes.SHA256()).hex() # cryptography 0.7+ - sha1_thumbprint = cert.fingerprint(hashes.SHA1()).hex() # cryptography 0.7+ - # https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object - return private_key, sha256_thumbprint, sha1_thumbprint, x5c + return _extract_cert_and_thumbprints(private_key, cert) def _load_private_key_from_pem_str(private_key_pem_str, passphrase_bytes): @@ -306,7 +308,7 @@ def __init__( { "private_key": "...-----BEGIN PRIVATE KEY-----... in PEM format", - "thumbprint": "A1B2C3D4E5F6...", + "thumbprint": "A1B2C3D4E5F6...", (Optinal, if not provided, MSAL will calculate it. Added in version 1.34.0) "public_certificate": "...-----BEGIN CERTIFICATE-----...", "passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)", } @@ -815,6 +817,15 @@ def _build_client(self, client_credential, authority, skip_regional_client=False passphrase_bytes) if client_credential.get("public_certificate") is True and x5c: headers["x5c"] = x5c + elif (client_credential.get("private_key") + and client_credential.get("public_certificate") + and not client_credential.get("thumbprint")): # in case user does not pass thumbprint but only certificate and private key + private_key, sha256_thumbprint, sha1_thumbprint, x5c =( + _extract_cert_and_thumbprints( + client_credential['private_key'], + client_credential['public_certificate'])) + if x5c: + headers["x5c"] = x5c elif ( client_credential.get("private_key") # PEM blob and client_credential.get("thumbprint")): diff --git a/msal/sku.py b/msal/sku.py index 4cbd6310..9b871873 100644 --- a/msal/sku.py +++ b/msal/sku.py @@ -2,5 +2,5 @@ """ # The __init__.py will import this. Not the other way around. -__version__ = "1.33.0b1" +__version__ = "1.34.0b1" SKU = "MSAL.Python" From b7bfcffbe22666dc85502b89d678f489a6ce45c7 Mon Sep 17 00:00:00 2001 From: Vit Curda Date: Thu, 26 Jun 2025 13:10:05 +0000 Subject: [PATCH 2/2] update as per discussion --- msal/application.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/msal/application.py b/msal/application.py index 06a14f2b..e04abc09 100644 --- a/msal/application.py +++ b/msal/application.py @@ -308,7 +308,7 @@ def __init__( { "private_key": "...-----BEGIN PRIVATE KEY-----... in PEM format", - "thumbprint": "A1B2C3D4E5F6...", (Optinal, if not provided, MSAL will calculate it. Added in version 1.34.0) + "thumbprint": "A1B2C3D4E5F6...", (Deprecated. Provide the public certificate instead. Added in version 1.34.0) "public_certificate": "...-----BEGIN CERTIFICATE-----...", "passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)", } @@ -819,13 +819,20 @@ def _build_client(self, client_credential, authority, skip_regional_client=False headers["x5c"] = x5c elif (client_credential.get("private_key") and client_credential.get("public_certificate") - and not client_credential.get("thumbprint")): # in case user does not pass thumbprint but only certificate and private key + and not client_credential.get("thumbprint")): #in case user does not pass thumbprint but only certificate and private key + if passphrase_bytes: # PEM with passphrase + private_key = _load_private_key_from_pem_str( + client_credential['private_key'], passphrase_bytes) + else: # PEM without passphrase + private_key = client_credential['private_key'] + private_key, sha256_thumbprint, sha1_thumbprint, x5c =( _extract_cert_and_thumbprints( - client_credential['private_key'], + private_key, client_credential['public_certificate'])) if x5c: headers["x5c"] = x5c + elif ( client_credential.get("private_key") # PEM blob and client_credential.get("thumbprint")):