@@ -66,16 +66,21 @@ def _str2bytes(raw):
6666 except :
6767 return raw
6868
69- def _extract_cert_and_thumbprints (private_key , cert ):
69+ def _extract_cert_and_thumbprints (cert ):
7070 # Cert concepts https://security.stackexchange.com/a/226758/125264
7171 from cryptography .hazmat .primitives import hashes , serialization
72- cert_pem = cert .public_bytes (encoding = serialization .Encoding .PEM ).decode ()
72+ cert_pem = cert .public_bytes ( # Requires cryptography 1.0+
73+ encoding = serialization .Encoding .PEM ).decode ()
7374 x5c = [
74- '\n ' .join (cert_pem .splitlines ()[1 :- 1 ])
75+ '\n ' .join (
76+ cert_pem .splitlines ()
77+ [1 :- 1 ] # Strip the "--- header ---" and "--- footer ---"
78+ )
7579 ]
76- sha256_thumbprint = cert .fingerprint (hashes .SHA256 ()).hex ()
77- sha1_thumbprint = cert .fingerprint (hashes .SHA1 ()).hex ()
78- return private_key , sha256_thumbprint , sha1_thumbprint , x5c
80+ # https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object
81+ sha256_thumbprint = cert .fingerprint (hashes .SHA256 ()).hex () # Requires cryptography 0.7+
82+ sha1_thumbprint = cert .fingerprint (hashes .SHA1 ()).hex () # Requires cryptography 0.7+
83+ return sha256_thumbprint , sha1_thumbprint , x5c
7984
8085def _parse_pfx (pfx_path , passphrase_bytes ):
8186 # Cert concepts https://security.stackexchange.com/a/226758/125264
@@ -86,7 +91,8 @@ def _parse_pfx(pfx_path, passphrase_bytes):
8691 f .read (), passphrase_bytes )
8792 if not (private_key and cert ):
8893 raise ValueError ("Your PFX file shall contain both private key and cert" )
89- return _extract_cert_and_thumbprints (private_key , cert )
94+ sha256_thumbprint , sha1_thumbprint , x5c = _extract_cert_and_thumbprints (cert )
95+ return private_key , sha256_thumbprint , sha1_thumbprint , x5c
9096
9197
9298def _load_private_key_from_pem_str (private_key_pem_str , passphrase_bytes ):
@@ -290,7 +296,9 @@ def __init__(
290296
291297 {
292298 "private_key": "...-----BEGIN PRIVATE KEY-----... in PEM format",
293- "thumbprint": "An SHA-1 thumbprint such as A1B2C3D4E5F6...", # Optional since version 1.35.0
299+ "thumbprint": "An SHA-1 thumbprint such as A1B2C3D4E5F6..."
300+ "Optional since version 1.35.0."
301+ "When absent, MSAL will automatically calculate an SHA-256 thumbprint instead.",
294302 "passphrase": "Needed if the private_key is encrypted (Added in version 1.6.0)",
295303 "public_certificate": "...-----BEGIN CERTIFICATE-----...", # Needed if you use Subject Name/Issuer auth. Added in version 0.5.0.
296304 }
@@ -805,31 +813,30 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
805813 passphrase_bytes )
806814 if client_credential .get ("public_certificate" ) is True and x5c :
807815 headers ["x5c" ] = x5c
808- elif (client_credential .get ("private_key" )
809- and client_credential .get ("public_certificate" )
810- and not client_credential .get ("thumbprint" )): #in case user does not pass thumbprint but only certificate and private key
811- if passphrase_bytes : # PEM with passphrase
812- private_key = _load_private_key_from_pem_str (
813- client_credential ['private_key' ], passphrase_bytes )
814- else : # PEM without passphrase
815- private_key = client_credential ['private_key' ]
816-
817- private_key , sha256_thumbprint , sha1_thumbprint , x5c = (
818- _extract_cert_and_thumbprints (
819- private_key ,
820- client_credential ['public_certificate' ]))
821- if x5c :
822- headers ["x5c" ] = x5c
823-
824- elif (
825- client_credential .get ("private_key" ) # PEM blob
826- and client_credential .get ("thumbprint" )):
827- sha1_thumbprint = client_credential ["thumbprint" ]
828- if passphrase_bytes :
829- private_key = _load_private_key_from_pem_str (
816+ elif client_credential .get ("private_key" ): # PEM blob
817+ private_key = ( # handles both encrypted and unencrypted
818+ _load_private_key_from_pem_str (
830819 client_credential ['private_key' ], passphrase_bytes )
831- else : # PEM without passphrase
832- private_key = client_credential ['private_key' ]
820+ if passphrase_bytes
821+ else client_credential ['private_key' ]
822+ )
823+
824+ # Determine thumbprints based on what's provided
825+ if client_credential .get ("thumbprint" ):
826+ # User provided a thumbprint - use it as SHA-1 (legacy/manual approach)
827+ sha1_thumbprint = client_credential ["thumbprint" ]
828+ sha256_thumbprint = None
829+ elif isinstance (client_credential .get ('public_certificate' ), str ):
830+ # No thumbprint provided, but we have a certificate to calculate thumbprints
831+ from cryptography import x509
832+ cert = x509 .load_pem_x509_certificate (
833+ _str2bytes (client_credential ['public_certificate' ]))
834+ sha256_thumbprint , sha1_thumbprint , headers ["x5c" ] = (
835+ _extract_cert_and_thumbprints (cert ))
836+ else :
837+ raise ValueError (
838+ "You must provide either 'thumbprint' or 'public_certificate' "
839+ "from which the thumbprint can be calculated." )
833840 else :
834841 raise ValueError (
835842 "client_credential needs to follow this format "
0 commit comments