From b9b8f225de51d56af77fb8f0cdd68a2830b993d1 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Wed, 8 Mar 2023 17:36:59 +0000 Subject: [PATCH] switch to PEM encoded keys for API --- docs/pycose/keys/cosekey.rst | 18 +++++++--------- pycose/keys/cosekey.py | 41 +++++++++++++++++++++++++++++++----- pycose/keys/ec2.py | 11 +++++----- pycose/keys/okp.py | 9 ++++---- pycose/keys/rsa.py | 11 +++++----- tests/test_ec2_keys.py | 11 ++++++---- tests/test_okp_keys.py | 11 ++++++---- tests/test_rsa_keys.py | 11 ++++++---- 8 files changed, 79 insertions(+), 44 deletions(-) diff --git a/docs/pycose/keys/cosekey.rst b/docs/pycose/keys/cosekey.rst index 730ddef..6d17ded 100644 --- a/docs/pycose/keys/cosekey.rst +++ b/docs/pycose/keys/cosekey.rst @@ -35,7 +35,7 @@ The :class:`~pycose.keys.cosekey.CoseKey` class can be used to decode serialized >>> cosekey.d b'\x8fx\x1a\tSr\xf8[m\x9fa\t\xaeB&\x11sM}\xbf\xa0\x06\x9a-\xf2\x93[\xb2\xe0S\xbf5' -Alternatively, :class:`~pycose.keys.cosekey.CoseKey` objects can be initialized from key objects of the `pyca/cryptography`_ package: +Alternatively, :class:`~pycose.keys.cosekey.CoseKey` objects can be initialized from PEM-encoded keys: .. _`pyca/cryptography`: https://cryptography.io/ @@ -43,15 +43,13 @@ Alternatively, :class:`~pycose.keys.cosekey.CoseKey` objects can be initialized :pyversion: >= 3.6 >>> from pycose.keys import CoseKey - >>> from cryptography.hazmat.primitives.serialization import load_pem_public_key - - >>> encoded_key = '-----BEGIN PUBLIC KEY-----\n' \ - ... 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyIBhex88X7Yrh5Q4hbmsUYpcVWNj\n' \ - ... 'mx1oE7TPomgpZJcQeNC3bX++GPsIWewWEGGFJKwHtRyfrL61DTTym3Rp8A==\n' \ - ... '-----END PUBLIC KEY-----\n' - >>> key = load_pem_public_key(encoded_key.encode("ascii")) - - >>> cosekey = CoseKey.from_cryptography_key(key) + + >>> pem = '-----BEGIN PUBLIC KEY-----\n' \ + ... 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyIBhex88X7Yrh5Q4hbmsUYpcVWNj\n' \ + ... 'mx1oE7TPomgpZJcQeNC3bX++GPsIWewWEGGFJKwHtRyfrL61DTTym3Rp8A==\n' \ + ... '-----END PUBLIC KEY-----\n' + + >>> cosekey = CoseKey.from_pem_public_key(pem) >>> cosekey diff --git a/pycose/keys/cosekey.py b/pycose/keys/cosekey.py index 228aa04..2fddc1d 100644 --- a/pycose/keys/cosekey.py +++ b/pycose/keys/cosekey.py @@ -5,6 +5,8 @@ import cbor2 +from cryptography.hazmat.primitives.serialization import NoEncryption, load_pem_private_key, load_pem_public_key + from pycose import utils from pycose.algorithms import CoseAlgorithm from pycose.exceptions import CoseException, CoseIllegalKeyType, CoseIllegalAlgorithm, CoseIllegalKeyOps @@ -94,9 +96,38 @@ def from_dict(cls, received: dict) -> 'CK': return key_obj - @classmethod - def from_cryptography_key( - cls, + @staticmethod + def from_pem_private_key( + pem: str, + password: Optional[bytes] = None, + optional_params: Optional[dict] = None + ) -> "CoseKey": + """ + Initialize a COSE key from a PEM-encoded private key. + + :param pem: PEM-encoded private key. + :param password: Password to decrypt the key. + :return: an initialized CoseKey object. + """ + ext_key = load_pem_private_key(pem.encode(), password) + return CoseKey._from_cryptography_key(ext_key, optional_params) + + @staticmethod + def from_pem_public_key( + pem: str, + optional_params: Optional[dict] = None + ) -> "CoseKey": + """ + Initialize a COSE key from a PEM-encoded public key. + + :param pem: PEM-encoded public key. + :return: an initialized CoseKey object. + """ + ext_key = load_pem_public_key(pem.encode()) + return CoseKey._from_cryptography_key(ext_key, optional_params) + + @staticmethod + def _from_cryptography_key( ext_key, optional_params: Optional[dict] = None ) -> "CoseKey": @@ -108,9 +139,9 @@ def from_cryptography_key( :return: An initialized COSE Key object. """ - for key_type in cls._key_types.values(): + for key_type in CoseKey._key_types.values(): if key_type._supports_cryptography_key_type(ext_key): - return key_type.from_cryptography_key(ext_key, optional_params) + return key_type._from_cryptography_key(ext_key, optional_params) raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") @staticmethod diff --git a/pycose/keys/ec2.py b/pycose/keys/ec2.py index 0394c9e..57fcd4c 100644 --- a/pycose/keys/ec2.py +++ b/pycose/keys/ec2.py @@ -42,9 +42,8 @@ def from_dict(cls, cose_key: dict) -> 'EC2Key': return cls(crv=curve, x=x, y=y, d=d, optional_params=_optional_params, allow_unknown_key_attrs=True) - @classmethod - def from_cryptography_key( - cls, + @staticmethod + def _from_cryptography_key( ext_key: Union[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey], optional_params: Optional[dict] = None ) -> 'EC2Key': @@ -53,7 +52,7 @@ def from_cryptography_key( :param ext_key: Python cryptography key. :return: an initialized EC key """ - if not cls._supports_cryptography_key_type(ext_key): + if not EC2Key._supports_cryptography_key_type(ext_key): raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") if isinstance(ext_key, ec.EllipticCurvePrivateKey): @@ -90,7 +89,7 @@ def from_cryptography_key( ) if optional_params: cose_key.update(optional_params) - return cls.from_dict(cose_key) + return EC2Key.from_dict(cose_key) @staticmethod def _supports_cryptography_key_type(ext_key) -> bool: @@ -276,7 +275,7 @@ def generate_key(cls, crv: Union[Type['CoseCurve'], str, int], optional_params: ext_key = ec.generate_private_key(crv.curve_obj, backend=default_backend()) - return cls.from_cryptography_key(ext_key, optional_params) + return cls._from_cryptography_key(ext_key, optional_params) def __delitem__(self, key): if self._key_transform(key) != KpKty and self._key_transform(key) != EC2KpCurve: diff --git a/pycose/keys/okp.py b/pycose/keys/okp.py index 9078c20..7950ef0 100644 --- a/pycose/keys/okp.py +++ b/pycose/keys/okp.py @@ -43,9 +43,8 @@ def from_dict(cls, cose_key: dict) -> 'OKPKey': return cls(crv=curve, x=x, d=d, optional_params=_optional_params, allow_unknown_key_attrs=True) - @classmethod - def from_cryptography_key( - cls, + @staticmethod + def _from_cryptography_key( ext_key: Union[ ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey, ed448.Ed448PrivateKey, ed448.Ed448PublicKey, @@ -60,7 +59,7 @@ def from_cryptography_key( :return: an initialized OKP key """ - curve = cls._curve_from_cryptography_key(ext_key) + curve = OKPKey._curve_from_cryptography_key(ext_key) if hasattr(ext_key, 'private_bytes'): priv_bytes = ext_key.private_bytes( @@ -241,7 +240,7 @@ def generate_key(cls, crv: Union[Type['CoseCurve'], str, int], optional_params: ext_key = crv.curve_obj.generate() - return cls.from_cryptography_key(ext_key, optional_params) + return cls._from_cryptography_key(ext_key, optional_params) def __delitem__(self, key: Union['KeyParam', str, int]): if self._key_transform(key) != KpKty and self._key_transform(key) != OKPKpCurve: diff --git a/pycose/keys/rsa.py b/pycose/keys/rsa.py index 4b4a581..c2d7b45 100644 --- a/pycose/keys/rsa.py +++ b/pycose/keys/rsa.py @@ -74,9 +74,8 @@ def from_dict(cls, cose_key: dict) -> 'RSAKey': optional_params=_optional_params, allow_unknown_key_attrs=True) - @classmethod - def from_cryptography_key( - cls, + @staticmethod + def _from_cryptography_key( ext_key: Union[rsa.RSAPrivateKey, rsa.RSAPublicKey], optional_params: Optional[dict] = None ) -> 'RSAKey': @@ -85,7 +84,7 @@ def from_cryptography_key( :param ext_key: Python cryptography key. :return: an initialized RSA key """ - if not cls._supports_cryptography_key_type(ext_key): + if not RSAKey._supports_cryptography_key_type(ext_key): raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") if isinstance(ext_key, rsa.RSAPrivateKey): @@ -110,7 +109,7 @@ def from_cryptography_key( }) if optional_params: cose_key.update(optional_params) - return cls.from_dict(cose_key) + return RSAKey.from_dict(cose_key) @staticmethod def _supports_cryptography_key_type(ext_key) -> bool: @@ -313,7 +312,7 @@ def generate_key(cls, key_bits: int, optional_params: dict = None) -> 'RSAKey': ext_key = rsa.generate_private_key(public_exponent=65537, key_size=key_bits, backend=default_backend()) - return cls.from_cryptography_key(ext_key, optional_params) + return cls._from_cryptography_key(ext_key, optional_params) def __repr__(self): hdr = f'' diff --git a/tests/test_ec2_keys.py b/tests/test_ec2_keys.py index 63f470f..1d916b0 100644 --- a/tests/test_ec2_keys.py +++ b/tests/test_ec2_keys.py @@ -3,6 +3,7 @@ import pytest from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat from pycose.algorithms import Es256 from pycose.keys.curves import P521, P384, P256 @@ -77,20 +78,22 @@ def test_ec2_public_keys_from_dicts(kty_attr, kty_value, crv_attr, crv_value, x_ assert _is_valid_ec2_key(cose_key) -def test_ec2_private_key_from_cryptography(): +def test_ec2_private_key_from_pem(): from_bstr = lambda enc: int.from_bytes(enc, byteorder='big') pub_nums = ec.EllipticCurvePublicNumbers(from_bstr(p256_x), from_bstr(p256_y), ec.SECP256R1()) priv_nums = ec.EllipticCurvePrivateNumbers(from_bstr(p256_d), pub_nums) private_key = priv_nums.private_key() - cose_key = CoseKey.from_cryptography_key(private_key) + pem = private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()).decode() + cose_key = CoseKey.from_pem_private_key(pem) assert _is_valid_ec2_key(cose_key) -def test_ec2_public_key_from_cryptography(): +def test_ec2_public_key_from_pem(): from_bstr = lambda enc: int.from_bytes(enc, byteorder='big') pub_nums = ec.EllipticCurvePublicNumbers(from_bstr(p256_x), from_bstr(p256_y), ec.SECP256R1()) public_key = pub_nums.public_key() - cose_key = CoseKey.from_cryptography_key(public_key) + pem = public_key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode() + cose_key = CoseKey.from_pem_public_key(pem) assert _is_valid_ec2_key(cose_key) diff --git a/tests/test_okp_keys.py b/tests/test_okp_keys.py index a48d2dc..5463d13 100644 --- a/tests/test_okp_keys.py +++ b/tests/test_okp_keys.py @@ -4,6 +4,7 @@ import pytest from cryptography.hazmat.primitives.asymmetric import ed25519, ed448, x25519, x448 +from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat from pycose.algorithms import EdDSA from pycose.exceptions import CoseInvalidKey, CoseIllegalKeyType, CoseUnsupportedCurve, CoseIllegalKeyOps @@ -68,19 +69,21 @@ def test_okp_public_keys_from_dicts(kty_attr, kty_value, crv_attr, crv_value, x_ @pytest.mark.parametrize('key_class', [ ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey, x25519.X25519PrivateKey, x448.X448PrivateKey]) -def test_okp_private_key_from_cryptography(key_class): +def test_okp_private_key_from_pem(key_class): private_key = key_class.generate() - cose_key = CoseKey.from_cryptography_key(private_key) + pem = private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()).decode() + cose_key = CoseKey.from_pem_private_key(pem) assert _is_valid_okp_key(cose_key) @pytest.mark.parametrize('key_class', [ ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey, x25519.X25519PrivateKey, x448.X448PrivateKey]) -def test_okp_public_key_from_cryptography(key_class): +def test_okp_public_key_from_pem(key_class): private_key = key_class.generate() public_key = private_key.public_key() - cose_key = CoseKey.from_cryptography_key(public_key) + pem = public_key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode() + cose_key = CoseKey.from_pem_public_key(pem) assert _is_valid_okp_key(cose_key) diff --git a/tests/test_rsa_keys.py b/tests/test_rsa_keys.py index 513f9fa..45094a9 100644 --- a/tests/test_rsa_keys.py +++ b/tests/test_rsa_keys.py @@ -3,6 +3,7 @@ import pytest from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat from pycose.exceptions import CoseIllegalKeyType from pycose.keys import RSAKey, CoseKey @@ -51,14 +52,16 @@ def test_dict_operations_on_rsa_key(): assert 'KEY_OPS' not in key -def test_rsa_private_key_from_cryptography(): +def test_rsa_private_key_from_pem(): private_key = rsa.generate_private_key(65537, 2048) - cose_key = CoseKey.from_cryptography_key(private_key) + pem = private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()).decode() + cose_key = CoseKey.from_pem_private_key(pem) assert isinstance(cose_key, RSAKey) -def test_rsa_public_key_from_cryptography(): +def test_rsa_public_key_from_pem(): private_key = rsa.generate_private_key(65537, 2048) public_key = private_key.public_key() - cose_key = CoseKey.from_cryptography_key(public_key) + pem = public_key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode() + cose_key = CoseKey.from_pem_public_key(pem) assert isinstance(cose_key, RSAKey)