From 5e445b0078343d6ca78ba402db1c57f2a5bcc357 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Fri, 24 Feb 2023 16:51:49 +0000 Subject: [PATCH 1/7] add from_cryptography_key helper --- pycose/keys/cosekey.py | 11 +++++ pycose/keys/ec2.py | 69 +++++++++++++++++++++++++----- pycose/keys/okp.py | 90 ++++++++++++++++++++++++++++++++++------ pycose/keys/rsa.py | 68 ++++++++++++++++++++++-------- pycose/keys/symmetric.py | 4 ++ 5 files changed, 201 insertions(+), 41 deletions(-) diff --git a/pycose/keys/cosekey.py b/pycose/keys/cosekey.py index 60e81db..8a229d2 100644 --- a/pycose/keys/cosekey.py +++ b/pycose/keys/cosekey.py @@ -94,6 +94,17 @@ def from_dict(cls, received: dict) -> 'CK': return key_obj + @classmethod + def from_cryptography_key( + cls, + ext_key, + optional_params: Optional[dict] = None + ) -> 'CK': + for key_type in cls._key_types.values(): + if key_type._supports_cryptography_key_type(ext_key): + return key_type.from_cryptography_key(ext_key, optional_params) + raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") + @staticmethod def base64decode(to_decode: str) -> bytes: """ diff --git a/pycose/keys/ec2.py b/pycose/keys/ec2.py index 5cf247e..5133881 100644 --- a/pycose/keys/ec2.py +++ b/pycose/keys/ec2.py @@ -9,10 +9,10 @@ from pycose.keys.keyops import SignOp, VerifyOp, DeriveKeyOp, DeriveBitsOp from pycose.keys.keyparam import EC2KeyParam, EC2KpCurve, EC2KpX, EC2KpY, EC2KpD, KpKty, KeyParam from pycose.keys.keytype import KtyEC2 +from pycose.keys.curves import CoseCurve if TYPE_CHECKING: from pycose.keys.keyops import KEYOPS - from pycose.keys.curves import CoseCurve @CoseKey.record_kty(KtyEC2) @@ -42,6 +42,60 @@ 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, + ext_key: Union[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey], + optional_params: Optional[dict] = None + ) -> 'EC2Key': + """ + Returns an initialized COSE Key object of type EC2Key. + :param ext_key: Python cryptography key. + :return: an initialized EC key + """ + if not cls._supports_cryptography_key_type(ext_key): + raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") + + if hasattr(ext_key, "private_numbers"): + priv_nums = ext_key.private_numbers() + pub_nums = priv_nums.public_numbers + else: + priv_nums = None + pub_nums = ext_key.public_numbers() + + curves = { + type(curve_cls.curve_obj): curve_cls + for curve_cls in CoseCurve.get_registered_classes().values() + if curve_cls.key_type == KtyEC2 + } + + if type(pub_nums.curve) not in curves: + raise CoseUnsupportedCurve(f"Unsupported EC Curve: {type(pub_nums.curve)}") + curve = curves[type(pub_nums.curve)] + + cose_key = {} + if pub_nums: + cose_key.update( + { + EC2KpCurve: curve, + EC2KpX: pub_nums.x.to_bytes(curve.size, "big"), + EC2KpY: pub_nums.y.to_bytes(curve.size, "big"), + } + ) + if priv_nums: + cose_key.update( + { + EC2KpD: priv_nums.private_value.to_bytes(curve.size, "big"), + } + ) + if optional_params: + cose_key.update(optional_params) + return cls.from_dict(cose_key) + + @staticmethod + def _supports_cryptography_key_type(ext_key) -> bool: + return isinstance(ext_key, (ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey)) + @staticmethod def _key_transform(key: Union[Type['EC2KeyParam'], Type['KeyParam'], str, int], allow_unknown_attrs: bool = False): return EC2KeyParam.from_id(key, allow_unknown_attrs) @@ -220,16 +274,9 @@ def generate_key(cls, crv: Union[Type['CoseCurve'], str, int], optional_params: if crv.key_type != KtyEC2: raise CoseUnsupportedCurve(f'Unsupported COSE curve: {crv}') - private_key = ec.generate_private_key(crv.curve_obj, backend=default_backend()) - d_value = private_key.private_numbers().private_value - x_coor = private_key.public_key().public_numbers().x - y_coor = private_key.public_key().public_numbers().y - - return EC2Key(crv=crv, - d=d_value.to_bytes(crv.size, "big"), - x=x_coor.to_bytes(crv.size, "big"), - y=y_coor.to_bytes(crv.size, "big"), - optional_params=optional_params) + ext_key = ec.generate_private_key(crv.curve_obj, backend=default_backend()) + + 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 020c51f..2d23c84 100644 --- a/pycose/keys/okp.py +++ b/pycose/keys/okp.py @@ -1,7 +1,8 @@ from typing import Optional, Type, Union, List, TYPE_CHECKING from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.serialization import PrivateFormat, PublicFormat, Encoding +from cryptography.hazmat.primitives.serialization import PrivateFormat, PublicFormat, Encoding, NoEncryption +from cryptography.hazmat.primitives.asymmetric import ed25519, ed448, x25519, x448 from pycose import utils from pycose.exceptions import CoseUnsupportedCurve, CoseInvalidKey, CoseIllegalKeyType, CoseIllegalKeyOps @@ -9,10 +10,11 @@ from pycose.keys.keyops import KEYOPS, SignOp, VerifyOp, DeriveBitsOp, DeriveKeyOp from pycose.keys.keyparam import OKPKeyParam, OKPKpCurve, OKPKpX, OKPKpD, KeyParam from pycose.keys.keytype import KtyOKP +from pycose.keys.curves import CoseCurve +from pycose.keys import curves if TYPE_CHECKING: from pycose.keys.keyops import KEYOPS - from pycose.keys.curves import CoseCurve @CoseKey.record_kty(KtyOKP) @@ -41,6 +43,77 @@ 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, + ext_key: Union[ + ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey, + ed448.Ed448PrivateKey, ed448.Ed448PublicKey, + x25519.X25519PrivateKey, x25519.X25519PublicKey, + x448.X448PrivateKey, x448.X448PublicKey + ], + optional_params: Optional[dict] = None, + ) -> 'OKPKey': + """ + Returns an initialized COSE Key object of type OKPKey. + :param ext_key: Python cryptography key. + :return: an initialized OKP key + """ + + curve = cls._curve_from_cryptography_key(ext_key) + + if hasattr(ext_key, 'private_bytes'): + priv_bytes = ext_key.private_bytes( + encoding=Encoding.Raw, + format=PrivateFormat.Raw, + encryption_algorithm=NoEncryption(), + ) + pub_bytes = ext_key.public_key().public_bytes( + encoding=Encoding.Raw, format=PublicFormat.Raw + ) + else: + priv_bytes = None + pub_bytes = ext_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw) + + cose_key = {} + cose_key.update( + { + OKPKpCurve: curve, + OKPKpX: pub_bytes, + } + ) + if priv_bytes: + cose_key.update( + { + OKPKpD: priv_bytes, + } + ) + if optional_params: + cose_key.update(optional_params) + return OKPKey.from_dict(cose_key) + + @staticmethod + def _curve_from_cryptography_key(ext_key) -> Type[CoseCurve]: + if isinstance(ext_key, (ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey)): + curve = curves.Ed25519 + elif isinstance(ext_key, (ed448.Ed448PrivateKey, ed448.Ed448PublicKey)): + curve = curves.Ed448 + elif isinstance(ext_key, (x25519.X25519PrivateKey, x25519.X25519PublicKey)): + curve = curves.X25519 + elif isinstance(ext_key, (x448.X448PrivateKey, x448.X448PublicKey)): + curve = curves.X448 + else: + raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") + return curve + + @classmethod + def _supports_cryptography_key_type(cls, ext_key) -> bool: + try: + cls._curve_from_cryptography_key(ext_key) + except CoseIllegalKeyType: + return False + return True + @staticmethod def _key_transform(key: Union[Type['OKPKeyParam'], Type['KeyParam'], str, int], allow_unknown_attrs: bool = False): @@ -175,18 +248,9 @@ def generate_key(cls, crv: Union[Type['CoseCurve'], str, int], optional_params: if crv.key_type != KtyOKP: raise CoseUnsupportedCurve(f'Unsupported COSE curve: {crv}') - encoding = Encoding(serialization.Encoding.Raw) - private_format = PrivateFormat(serialization.PrivateFormat.Raw) - public_format = PublicFormat(serialization.PublicFormat.Raw) - encryption = serialization.NoEncryption() - - private_key = crv.curve_obj.generate() + ext_key = crv.curve_obj.generate() - return OKPKey( - crv=crv, - x=private_key.public_key().public_bytes(encoding, public_format), - d=private_key.private_bytes(encoding, private_format, encryption), - optional_params=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 42d071a..e6228b8 100644 --- a/pycose/keys/rsa.py +++ b/pycose/keys/rsa.py @@ -14,6 +14,9 @@ from pycose.keys.keyops import KEYOPS from pycose.keys.keyparam import KeyParam +def to_bstr(dec): + blen = (dec.bit_length() + 7) // 8 + return dec.to_bytes(blen, byteorder="big") @CoseKey.record_kty(KtyRSA) class RSAKey(CoseKey): @@ -71,6 +74,50 @@ def from_dict(cls, cose_key: dict) -> 'RSAKey': optional_params=_optional_params, allow_unknown_key_attrs=True) + @classmethod + def from_cryptography_key( + cls, + ext_key: Union[rsa.RSAPrivateKey, rsa.RSAPublicKey], + optional_params: Optional[dict] = None + ) -> 'RSAKey': + """ + Returns an initialized COSE Key object of type RSAKey. + :param ext_key: Python cryptography key. + :return: an initialized RSA key + """ + if not cls._supports_cryptography_key_type(ext_key): + raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") + + if hasattr(ext_key, 'private_numbers'): + priv_nums = ext_key.private_numbers() + pub_nums = priv_nums.public_numbers + else: + priv_nums = None + pub_nums = ext_key.public_numbers() + + cose_key = {} + if pub_nums: + cose_key.update({ + RSAKpE: to_bstr(pub_nums.e), + RSAKpN: to_bstr(pub_nums.n), + }) + if priv_nums: + cose_key.update({ + RSAKpD: to_bstr(priv_nums.d), + RSAKpP: to_bstr(priv_nums.p), + RSAKpQ: to_bstr(priv_nums.q), + RSAKpDP: to_bstr(priv_nums.dmp1), + RSAKpDQ: to_bstr(priv_nums.dmq1), + RSAKpQInv: to_bstr(priv_nums.iqmp), + }) + if optional_params: + cose_key.update(optional_params) + return cls.from_dict(cose_key) + + @staticmethod + def _supports_cryptography_key_type(ext_key) -> bool: + return isinstance(ext_key, (rsa.RSAPrivateKey, rsa.RSAPublicKey)) + @staticmethod def _key_transform(key: Union[Type['RSAKeyParam'], Type['KeyParam'], str, int], allow_unknown_attrs: bool = False): return RSAKeyParam.from_id(key, allow_unknown_attrs) @@ -255,8 +302,8 @@ def key_ops(self, new_key_ops: List[Type['KEYOPS']]) -> None: else: CoseKey.key_ops.fset(self, new_key_ops) - @staticmethod - def generate_key(key_bits: int, optional_params: dict = None) -> 'RSAKey': + @classmethod + def generate_key(cls, key_bits: int, optional_params: dict = None) -> 'RSAKey': """ Generate a random RSAKey COSE key object. The RSA keys have two primes (see section 4 of RFC 8230). @@ -266,22 +313,9 @@ def generate_key(key_bits: int, optional_params: dict = None) -> 'RSAKey': :return: An COSE `RSAKey` key. """ - key = rsa.generate_private_key(public_exponent=65537, key_size=key_bits, backend=default_backend()) - - private_numbers = key.private_numbers() - p = private_numbers.p.to_bytes((private_numbers.p.bit_length() + 7) // 8, byteorder='big') - q = private_numbers.q.to_bytes((private_numbers.q.bit_length() + 7) // 8, byteorder='big') - d = private_numbers.d.to_bytes((private_numbers.d.bit_length() + 7) // 8, byteorder='big') - dp = private_numbers.dmp1.to_bytes((private_numbers.dmp1.bit_length() + 7) // 8, byteorder='big') - dq = private_numbers.dmq1.to_bytes((private_numbers.dmq1.bit_length() + 7) // 8, byteorder='big') - qinv = private_numbers.iqmp.to_bytes((private_numbers.iqmp.bit_length() + 7) // 8, byteorder='big') - - public_numbers = private_numbers.public_numbers - - n = public_numbers.n.to_bytes((public_numbers.n.bit_length() + 7) // 8, byteorder='big') - e = public_numbers.e.to_bytes((public_numbers.e.bit_length() + 7) // 8, byteorder='big') + ext_key = rsa.generate_private_key(public_exponent=65537, key_size=key_bits, backend=default_backend()) - return RSAKey(n=n, e=e, d=d, p=p, q=q, dp=dp, dq=dq, qinv=qinv, optional_params=optional_params) + return cls.from_cryptography_key(ext_key, optional_params) def __repr__(self): hdr = f'' diff --git a/pycose/keys/symmetric.py b/pycose/keys/symmetric.py index 07e6e3f..a66fdae 100644 --- a/pycose/keys/symmetric.py +++ b/pycose/keys/symmetric.py @@ -33,6 +33,10 @@ def from_dict(cls, cose_key: dict) -> 'SymmetricKey': return cls(k=k, optional_params=_optional_params, allow_unknown_key_attrs=True) + @staticmethod + def _supports_cryptography_key_type(ext_key) -> bool: + return False + @staticmethod def _key_transform(key: Union[Type['SymmetricKeyParam'], Type['KeyParam'], str, int], allow_unknown_attrs: bool = False): From 6ad4ffe82276151c4be3af92d4a07b2ea118659f Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Fri, 24 Feb 2023 16:57:51 +0000 Subject: [PATCH 2/7] docstring --- pycose/keys/cosekey.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pycose/keys/cosekey.py b/pycose/keys/cosekey.py index 8a229d2..45328e3 100644 --- a/pycose/keys/cosekey.py +++ b/pycose/keys/cosekey.py @@ -100,6 +100,14 @@ def from_cryptography_key( ext_key, optional_params: Optional[dict] = None ) -> 'CK': + """ + Initialize a COSE key from a cryptography key. + + :param ext_key: A cryptography key. + :param optional_params: Optional parameters to add to the key. + :return: An initialized COSE Key object. + """ + for key_type in cls._key_types.values(): if key_type._supports_cryptography_key_type(ext_key): return key_type.from_cryptography_key(ext_key, optional_params) From c688d30559b9bd95cad2f2e91154ace8a8cf64cd Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Thu, 2 Mar 2023 15:55:19 +0000 Subject: [PATCH 3/7] extend cosekey docs --- docs/pycose/keys/cosekey.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/pycose/keys/cosekey.rst b/docs/pycose/keys/cosekey.rst index 80d7f5f..730ddef 100644 --- a/docs/pycose/keys/cosekey.rst +++ b/docs/pycose/keys/cosekey.rst @@ -35,6 +35,27 @@ 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: + +.. _`pyca/cryptography`: https://cryptography.io/ + +.. doctest:: + :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) + >>> cosekey + + + Overview -------- From aa130a15f8dc78a215fb81bb121c0e2a7e2cdf86 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Thu, 2 Mar 2023 16:43:45 +0000 Subject: [PATCH 4/7] add tests --- tests/test_ec2_keys.py | 19 +++++++++++++++++++ tests/test_okp_keys.py | 21 +++++++++++++++++++++ tests/test_rsa_keys.py | 15 +++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/tests/test_ec2_keys.py b/tests/test_ec2_keys.py index c9a7b6d..63f470f 100644 --- a/tests/test_ec2_keys.py +++ b/tests/test_ec2_keys.py @@ -2,6 +2,8 @@ import pytest +from cryptography.hazmat.primitives.asymmetric import ec + from pycose.algorithms import Es256 from pycose.keys.curves import P521, P384, P256 from pycose.exceptions import CoseInvalidKey, CoseIllegalKeyType, CoseException, CoseUnsupportedCurve @@ -75,6 +77,23 @@ 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(): + 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) + assert _is_valid_ec2_key(cose_key) + + +def test_ec2_public_key_from_cryptography(): + 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) + assert _is_valid_ec2_key(cose_key) + + @pytest.mark.parametrize('crv', [P256, P384, P521]) def test_ec2_key_generation_encoding_decoding(crv): trails = 256 diff --git a/tests/test_okp_keys.py b/tests/test_okp_keys.py index 3e3c649..a48d2dc 100644 --- a/tests/test_okp_keys.py +++ b/tests/test_okp_keys.py @@ -3,6 +3,8 @@ import pytest +from cryptography.hazmat.primitives.asymmetric import ed25519, ed448, x25519, x448 + from pycose.algorithms import EdDSA from pycose.exceptions import CoseInvalidKey, CoseIllegalKeyType, CoseUnsupportedCurve, CoseIllegalKeyOps from pycose.keys import OKPKey, CoseKey @@ -63,6 +65,25 @@ def test_okp_public_keys_from_dicts(kty_attr, kty_value, crv_attr, crv_value, x_ assert _is_valid_okp_key(cose_key) +@pytest.mark.parametrize('key_class', [ + ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey, + x25519.X25519PrivateKey, x448.X448PrivateKey]) +def test_okp_private_key_from_cryptography(key_class): + private_key = key_class.generate() + cose_key = CoseKey.from_cryptography_key(private_key) + 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): + private_key = key_class.generate() + public_key = private_key.public_key() + cose_key = CoseKey.from_cryptography_key(public_key) + assert _is_valid_okp_key(cose_key) + + @pytest.mark.parametrize('crv', [X25519, X448, Ed25519, Ed448, 4, 'X25519', 'X448']) def test_okp_key_generation_encoding_decoding(crv): trails = 256 diff --git a/tests/test_rsa_keys.py b/tests/test_rsa_keys.py index 7cee126..513f9fa 100644 --- a/tests/test_rsa_keys.py +++ b/tests/test_rsa_keys.py @@ -2,6 +2,8 @@ import pytest +from cryptography.hazmat.primitives.asymmetric import rsa + from pycose.exceptions import CoseIllegalKeyType from pycose.keys import RSAKey, CoseKey from pycose.keys.keyops import SignOp @@ -47,3 +49,16 @@ def test_dict_operations_on_rsa_key(): assert 'subject_name' in key assert 'KEY_OPS' not in key + + +def test_rsa_private_key_from_cryptography(): + private_key = rsa.generate_private_key(65537, 2048) + cose_key = CoseKey.from_cryptography_key(private_key) + assert isinstance(cose_key, RSAKey) + + +def test_rsa_public_key_from_cryptography(): + private_key = rsa.generate_private_key(65537, 2048) + public_key = private_key.public_key() + cose_key = CoseKey.from_cryptography_key(public_key) + assert isinstance(cose_key, RSAKey) From ed4b83939103b62b8c82664f970a917f4207ab69 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Wed, 8 Mar 2023 16:09:22 +0000 Subject: [PATCH 5/7] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Paul LiƩtar --- pycose/keys/okp.py | 17 +++++------------ pycose/keys/rsa.py | 12 +++++------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/pycose/keys/okp.py b/pycose/keys/okp.py index 2d23c84..94efbdf 100644 --- a/pycose/keys/okp.py +++ b/pycose/keys/okp.py @@ -75,19 +75,12 @@ def from_cryptography_key( priv_bytes = None pub_bytes = ext_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw) - cose_key = {} - cose_key.update( - { - OKPKpCurve: curve, - OKPKpX: pub_bytes, - } - ) + cose_key = { + OKPKpCurve: curve, + OKPKpX: pub_bytes, + } if priv_bytes: - cose_key.update( - { - OKPKpD: priv_bytes, - } - ) + cose_key[OKPKpD] = priv_bytes if optional_params: cose_key.update(optional_params) return OKPKey.from_dict(cose_key) diff --git a/pycose/keys/rsa.py b/pycose/keys/rsa.py index e6228b8..4b4a581 100644 --- a/pycose/keys/rsa.py +++ b/pycose/keys/rsa.py @@ -88,19 +88,17 @@ def from_cryptography_key( if not cls._supports_cryptography_key_type(ext_key): raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") - if hasattr(ext_key, 'private_numbers'): + if isinstance(ext_key, rsa.RSAPrivateKey): priv_nums = ext_key.private_numbers() pub_nums = priv_nums.public_numbers else: priv_nums = None pub_nums = ext_key.public_numbers() - cose_key = {} - if pub_nums: - cose_key.update({ - RSAKpE: to_bstr(pub_nums.e), - RSAKpN: to_bstr(pub_nums.n), - }) + cose_key = { + RSAKpE: to_bstr(pub_nums.e), + RSAKpN: to_bstr(pub_nums.n), + } if priv_nums: cose_key.update({ RSAKpD: to_bstr(priv_nums.d), From 95b5376b5f610fc0b3680aa438d5e28e81c80034 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Wed, 8 Mar 2023 16:10:59 +0000 Subject: [PATCH 6/7] address more feedback --- pycose/keys/cosekey.py | 2 +- pycose/keys/ec2.py | 2 +- pycose/keys/okp.py | 18 ++++++++---------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pycose/keys/cosekey.py b/pycose/keys/cosekey.py index 45328e3..228aa04 100644 --- a/pycose/keys/cosekey.py +++ b/pycose/keys/cosekey.py @@ -99,7 +99,7 @@ def from_cryptography_key( cls, ext_key, optional_params: Optional[dict] = None - ) -> 'CK': + ) -> "CoseKey": """ Initialize a COSE key from a cryptography key. diff --git a/pycose/keys/ec2.py b/pycose/keys/ec2.py index 5133881..0394c9e 100644 --- a/pycose/keys/ec2.py +++ b/pycose/keys/ec2.py @@ -56,7 +56,7 @@ def from_cryptography_key( if not cls._supports_cryptography_key_type(ext_key): raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") - if hasattr(ext_key, "private_numbers"): + if isinstance(ext_key, ec.EllipticCurvePrivateKey): priv_nums = ext_key.private_numbers() pub_nums = priv_nums.public_numbers else: diff --git a/pycose/keys/okp.py b/pycose/keys/okp.py index 94efbdf..9078c20 100644 --- a/pycose/keys/okp.py +++ b/pycose/keys/okp.py @@ -88,16 +88,14 @@ def from_cryptography_key( @staticmethod def _curve_from_cryptography_key(ext_key) -> Type[CoseCurve]: if isinstance(ext_key, (ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey)): - curve = curves.Ed25519 - elif isinstance(ext_key, (ed448.Ed448PrivateKey, ed448.Ed448PublicKey)): - curve = curves.Ed448 - elif isinstance(ext_key, (x25519.X25519PrivateKey, x25519.X25519PublicKey)): - curve = curves.X25519 - elif isinstance(ext_key, (x448.X448PrivateKey, x448.X448PublicKey)): - curve = curves.X448 - else: - raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") - return curve + return curves.Ed25519 + if isinstance(ext_key, (ed448.Ed448PrivateKey, ed448.Ed448PublicKey)): + return curves.Ed448 + if isinstance(ext_key, (x25519.X25519PrivateKey, x25519.X25519PublicKey)): + return curves.X25519 + if isinstance(ext_key, (x448.X448PrivateKey, x448.X448PublicKey)): + return curves.X448 + raise CoseIllegalKeyType(f"Unsupported key type: {type(ext_key)}") @classmethod def _supports_cryptography_key_type(cls, ext_key) -> bool: From b9b8f225de51d56af77fb8f0cdd68a2830b993d1 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Wed, 8 Mar 2023 17:36:59 +0000 Subject: [PATCH 7/7] 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)