Skip to content

Commit

Permalink
switch to PEM encoded keys for API
Browse files Browse the repository at this point in the history
  • Loading branch information
letmaik authored and TimothyClaeys committed Dec 15, 2023
1 parent 95b5376 commit b9b8f22
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 44 deletions.
18 changes: 8 additions & 10 deletions docs/pycose/keys/cosekey.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,21 @@ 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/

.. 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)

>>> pem = '-----BEGIN PUBLIC KEY-----\n' \
... 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyIBhex88X7Yrh5Q4hbmsUYpcVWNj\n' \
... 'mx1oE7TPomgpZJcQeNC3bX++GPsIWewWEGGFJKwHtRyfrL61DTTym3Rp8A==\n' \
... '-----END PUBLIC KEY-----\n'

>>> cosekey = CoseKey.from_pem_public_key(pem)
>>> cosekey
<COSE_Key(EC2Key): {'EC2KpY': "b'\\x10x\\xd0\\xb7m' ... (32 B)", 'EC2KpX': "b'\\xc8\\x80a{\\x1f' ... (32 B)", 'EC2KpCurve': 'P256', 'KpKty': 'KtyEC2'}>

Expand Down
41 changes: 36 additions & 5 deletions pycose/keys/cosekey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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":
Expand All @@ -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
Expand Down
11 changes: 5 additions & 6 deletions pycose/keys/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 4 additions & 5 deletions pycose/keys/okp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 5 additions & 6 deletions pycose/keys/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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'<COSE_Key(RSAKey): {self._key_repr()}>'
Expand Down
11 changes: 7 additions & 4 deletions tests/test_ec2_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down
11 changes: 7 additions & 4 deletions tests/test_okp_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down
11 changes: 7 additions & 4 deletions tests/test_rsa_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

0 comments on commit b9b8f22

Please sign in to comment.