diff --git a/jwskate/jwk/base.py b/jwskate/jwk/base.py index fd23a7d..962c7cf 100644 --- a/jwskate/jwk/base.py +++ b/jwskate/jwk/base.py @@ -19,6 +19,7 @@ from binapy import BinaPy from cryptography import x509 from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.serialization import pkcs12 from typing_extensions import Self from jwskate.jwa import ( @@ -1010,7 +1011,7 @@ def as_jwks(self) -> JwkSet: return JwkSet(keys=(self,)) @classmethod - def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> Jwk: + def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> Self: """Initialize a Jwk from a key from the `cryptography` library. The input key can be any private or public key supported by cryptography. @@ -1026,7 +1027,7 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> Jwk: TypeError: if the key type is not supported """ - for jwk_class in Jwk.__subclasses__(): + for jwk_class in cls.__subclasses__(): for cryptography_class in ( jwk_class.CRYPTOGRAPHY_PRIVATE_KEY_CLASSES + jwk_class.CRYPTOGRAPHY_PUBLIC_KEY_CLASSES ): @@ -1192,6 +1193,36 @@ def from_x509(cls, x509_pem: str | bytes) -> Self: cert = x509.load_pem_x509_certificate(x509_pem) return cls(cert.public_key()) + @classmethod + def from_pkcs12(cls, p12: bytes, password: bytes | str | None) -> Self: + """Read a private key from a PKCS12 data, password-protected. + + PKCS12 typically contain a private key and one or more associated certificates. + This will only read a single private key and return the matching `Jwk`. + PKCS12 files are binary, and usually have a `.p12` or `.pfx` extension. + + PKCS12 files are typically protected with a password, which you must provide + as parameter to this method. If you provide a password as a `str`, it will be + encoded with UTF-8 before being used for decryption, just like OpenSSL>1.1.0 + expects. If your PKCS12 file and password were generated with OpenSSL<=1.1.0, + you must provide the password as `bytes`, encoded with either 'ASCII' or 'ISO8859-1' + + See: + https://www.openssl.org/docs/man3.0/man7/passphrase-encoding.html#:~:text=PKCS#12 + + Args: + p12: the raw PKCS12 binary data + password: the decryption password, if any. + + Returns: + A `Jwk` instance with the private key read from the PKCS12 data + + """ + if isinstance(password, str): + password = password.encode("UTF-8") + key, _, _ = pkcs12.load_key_and_certificates(p12, password) + return cls.from_cryptography_key(key) + @classmethod def generate(cls, *, alg: str | None = None, kty: str | None = None, **kwargs: Any) -> Jwk: """Generate a Private Key and return it as a `Jwk` instance. diff --git a/tests/test_jwk/test_jwk.py b/tests/test_jwk/test_jwk.py index 99ae45a..ee2edc1 100644 --- a/tests/test_jwk/test_jwk.py +++ b/tests/test_jwk/test_jwk.py @@ -3,6 +3,7 @@ import secrets import pytest +from binapy import BinaPy from cryptography.hazmat.primitives.asymmetric import rsa from jwskate import ( @@ -503,3 +504,14 @@ def test_from_x509() -> None: assert not key.is_private assert not key.is_symmetric assert key.key_size == 2048 + assert key == Jwk.from_x509(ms_cert.encode()) + + +def test_from_pkcs12() -> None: + p12 = BinaPy("""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""").decode_from("b64") + + jwk = Jwk.from_pkcs12(p12, "jwskate!") + assert jwk.kty == "RSA" + assert jwk.is_private + assert jwk == Jwk.from_pkcs12(p12, b"jwskate!") +