# Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations from typing import TYPE_CHECKING, Any, Mapping, Sequence, Tuple, TypeVar from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, padding, rsa from . import arkg from .utils import bytes2int, int2bytes if TYPE_CHECKING: # This type isn't available on cryptography <40. from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes _backend = default_backend() _mldsa = hasattr(_backend, "mldsa_supported") and _backend.mldsa_supported() class CoseKey(dict): """A COSE formatted public key. :param _: The COSE key paramters. :cvar ALGORITHM: COSE algorithm identifier. """ ALGORITHM: int = None # type: ignore def verify(self, message: bytes, signature: bytes) -> None: """Validates a digital signature over a given message. :param message: The message which was signed. :param signature: The signature to check. """ raise NotImplementedError("Signature verification not supported.") @classmethod def from_cryptography_key( cls: type[T_CoseKey], public_key: PublicKeyTypes ) -> T_CoseKey: """Converts a PublicKey object from Cryptography into a COSE key. :param public_key: Either an EC or RSA public key. :return: A CoseKey. """ raise NotImplementedError("Creation from cryptography not supported.") @staticmethod def for_alg(alg: int) -> type[CoseKey]: """Get a subclass of CoseKey corresponding to an algorithm identifier. :param alg: The COSE identifier of the algorithm. :return: A CoseKey. """ def find_subclass(base_cls: type[CoseKey]) -> type[CoseKey] | None: for cls in base_cls.__subclasses__(): if cls.ALGORITHM == alg: return cls subresult = find_subclass(cls) if subresult: return subresult return None return find_subclass(CoseKey) or UnsupportedKey @staticmethod def for_name(name: str) -> type[CoseKey]: """Get a subclass of CoseKey corresponding to an algorithm identifier. :param alg: The COSE identifier of the algorithm. :return: A CoseKey. """ def find_subclass(base_cls: type[CoseKey]) -> type[CoseKey] | None: for cls in base_cls.__subclasses__(): if cls.__name__ == name: return cls subresult = find_subclass(cls) if subresult: return subresult return None return find_subclass(CoseKey) or UnsupportedKey @staticmethod def parse(cose: Mapping[int, Any]) -> CoseKey: """Create a CoseKey from a dict""" alg = cose.get(3) if not alg: raise ValueError("COSE alg identifier must be provided.") return CoseKey.for_alg(alg)(cose) @staticmethod def supported_algorithms() -> Sequence[int]: """Get a list of all supported algorithm identifiers""" algs: list[type[CoseKey]] = [ ES256, EdDSA, ES384, ES512, PS256, RS256, ES256K, ] if _mldsa: algs.extend([MLDSA44, MLDSA65, MLDSA87]) return [cls.ALGORITHM for cls in algs] T_CoseKey = TypeVar("T_CoseKey", bound=CoseKey) class UnsupportedKey(CoseKey): """A COSE key with an unsupported algorithm.""" class ES256(CoseKey): ALGORITHM = -7 _HASH_ALG = hashes.SHA256() def verify(self, message, signature): if self[-1] != 1: raise ValueError("Unsupported elliptic curve") ec.EllipticCurvePublicNumbers( bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP256R1() ).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG)) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ec.EllipticCurvePublicKey) # noqa: S101 pn = public_key.public_numbers() return cls( { 1: 2, 3: cls.ALGORITHM, -1: 1, -2: int2bytes(pn.x, 32), -3: int2bytes(pn.y, 32), } ) @classmethod def from_ctap1(cls, data): """Creates an ES256 key from a CTAP1 formatted public key byte string. :param data: A 65 byte SECP256R1 public key. :return: A ES256 key. """ return cls({1: 2, 3: cls.ALGORITHM, -1: 1, -2: data[1:33], -3: data[33:65]}) class ESP256(ES256): # See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-10.html#name-elliptic-curve-digital-sign # noqa:E501 ALGORITHM = -9 class ES384(CoseKey): ALGORITHM = -35 _HASH_ALG = hashes.SHA384() def verify(self, message, signature): if self[-1] != 2: raise ValueError("Unsupported elliptic curve") ec.EllipticCurvePublicNumbers( bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP384R1() ).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG)) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ec.EllipticCurvePublicKey) # noqa: S101 pn = public_key.public_numbers() return cls( { 1: 2, 3: cls.ALGORITHM, -1: 2, -2: int2bytes(pn.x, 48), -3: int2bytes(pn.y, 48), } ) class ESP384(ES384): # See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-12.html#name-elliptic-curve-digital-sign # noqa:E501 ALGORITHM = -51 class ES512(CoseKey): ALGORITHM = -36 _HASH_ALG = hashes.SHA512() def verify(self, message, signature): if self[-1] != 3: raise ValueError("Unsupported elliptic curve") ec.EllipticCurvePublicNumbers( bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP521R1() ).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG)) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ec.EllipticCurvePublicKey) # noqa: S101 pn = public_key.public_numbers() return cls( { 1: 2, 3: cls.ALGORITHM, -1: 3, -2: int2bytes(pn.x, 66), -3: int2bytes(pn.y, 66), } ) class ESP512(ES512): # See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-12.html#name-elliptic-curve-digital-sign # noqa:E501 ALGORITHM = -52 class RS256(CoseKey): ALGORITHM = -257 _HASH_ALG = hashes.SHA256() def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( _backend ).verify(signature, message, padding.PKCS1v15(), self._HASH_ALG) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, rsa.RSAPublicKey) # noqa: S101 pn = public_key.public_numbers() return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)}) class PS256(CoseKey): ALGORITHM = -37 _HASH_ALG = hashes.SHA256() def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( _backend ).verify( signature, message, padding.PSS( mgf=padding.MGF1(self._HASH_ALG), salt_length=padding.PSS.MAX_LENGTH ), self._HASH_ALG, ) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, rsa.RSAPublicKey) # noqa: S101 pn = public_key.public_numbers() return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)}) class EdDSA(CoseKey): ALGORITHM = -8 def verify(self, message, signature): if self[-1] != 6: raise ValueError("Unsupported elliptic curve") ed25519.Ed25519PublicKey.from_public_bytes(self[-2]).verify(signature, message) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ed25519.Ed25519PublicKey) # noqa: S101 return cls( { 1: 1, 3: cls.ALGORITHM, -1: 6, -2: public_key.public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ), } ) class Ed25519(EdDSA): # See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-12.html#name-edwards-curve-digital-signa # noqa:E501 ALGORITHM = -19 class Ed448(CoseKey): # See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-12.html#name-edwards-curve-digital-signa # noqa:E501 ALGORITHM = -53 def verify(self, message, signature): if self[-1] != 7: raise ValueError("Unsupported elliptic curve") ed448.Ed448PublicKey.from_public_bytes(self[-2]).verify(signature, message) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ed448.Ed448PublicKey) # noqa: S101 return cls( { 1: 1, 3: cls.ALGORITHM, -1: 7, -2: public_key.public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ), } ) class RS1(CoseKey): ALGORITHM = -65535 _HASH_ALG = hashes.SHA1() # noqa: S303 def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( _backend ).verify(signature, message, padding.PKCS1v15(), self._HASH_ALG) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, rsa.RSAPublicKey) # noqa: S101 pn = public_key.public_numbers() return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)}) class ES256K(CoseKey): ALGORITHM = -47 _HASH_ALG = hashes.SHA256() def verify(self, message, signature): if self[-1] != 8: raise ValueError("Unsupported elliptic curve") ec.EllipticCurvePublicNumbers( bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP256K1() ).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG)) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ec.EllipticCurvePublicKey) # noqa: S101 pn = public_key.public_numbers() return cls( { 1: 2, 3: cls.ALGORITHM, -1: 8, -2: int2bytes(pn.x, 32), -3: int2bytes(pn.y, 32), } ) def _cose2key(cose: CoseKey, curve: ec.EllipticCurve) -> ec.EllipticCurvePublicKey: return ec.EllipticCurvePublicNumbers( bytes2int(cose[-2]), bytes2int(cose[-3]), curve ).public_key() # ESP256-split using private key derived by ARKG-P256 # https://www.ietf.org/archive/id/draft-bradleylundberg-cfrg-arkg-10.html#section-8.3 ESP256_SPLIT_ARKG_PLACEHOLDER = -65539 # placeholder value class ARKG_P256_PLACEHOLDER(CoseKey): """ https://www.ietf.org/archive/id/draft-bradleylundberg-cfrg-arkg-10.html#ARKG-P256 The identifier ARKG-P256 represents the following ARKG instance: BL: Elliptic curve addition as described in Section 3.1 with the parameters: crv: The NIST curve secp256r1 [SEC2]. hash-to-crv-suite: P256_XMD:SHA-256_SSWU_RO_ [RFC9380]. DST_ext: 'ARKG-P256'. KEM: ECDH as described in Section 3.3 with the parameters: crv: The NIST curve secp256r1 [SEC2]. Hash: SHA-256 [FIPS 180-4]. hash-to-crv-suite: P256_XMD:SHA-256_SSWU_RO_ [RFC9380]. DST_ext: 'ARKG-P256'. """ ALGORITHM = -65700 # placeholder value _CRV = ec.SECP256R1() _ARKG = arkg._ARKG( bl=arkg._BL(crv=_CRV, Hash=hashes.SHA256(), DST_ext=b"ARKG-P256"), kem=arkg._KEM(crv=_CRV, Hash=hashes.SHA256(), DST_ext=b"ARKG-ECDH.ARKG-P256"), ) @property def pk_bl(self) -> CoseKey: return CoseKey.parse(self[-1]) @property def pk_kem(self) -> CoseKey: return CoseKey.parse(self[-2]) def derive_public_key(self, ikm: bytes, ctx: bytes) -> Tuple[CoseKey, Mapping]: pk, kh = self._ARKG.derive_public_key( _cose2key(self.pk_bl, self._CRV), _cose2key(self.pk_kem, self._CRV), ikm, ctx, ) point_enc = pk.public_bytes( serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint ) ln = (len(point_enc) - 1) // 2 pk_cose = ESP256( { 1: 2, # kty: EC 3: self[-3], # alg: derived key alg (key -3) from seed (value -9) -1: 1, -2: point_enc[1 : ln + 1], # x-coordinate -3: point_enc[1 + ln :], # y-coordinate } ) # COSE_Sign_Args: https://www.ietf.org/archive/id/draft-bradleylundberg-cfrg-arkg-10.html#name-cose-signing-arguments args = { 3: ESP256_SPLIT_ARKG_PLACEHOLDER, -1: kh, -2: ctx, } return pk_cose, args # MLDSA support was added in cryptography 47.0.0, and requires a supported backend if _mldsa: from cryptography.hazmat.primitives.asymmetric import mldsa class MLDSA44(CoseKey): ALGORITHM = -48 def verify(self, message, signature): if self[1] != 7: raise ValueError("Invalid key type") pk = mldsa.MLDSA44PublicKey.from_public_bytes(self[-1]) pk.verify(signature, message) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, mldsa.MLDSA44PublicKey) # noqa: S101 return cls( { 1: 7, 3: cls.ALGORITHM, -1: public_key.public_bytes_raw(), } ) class MLDSA65(CoseKey): ALGORITHM = -49 def verify(self, message, signature): if self[1] != 7: raise ValueError("Invalid key type") pk = mldsa.MLDSA65PublicKey.from_public_bytes(self[-1]) pk.verify(signature, message) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, mldsa.MLDSA65PublicKey) # noqa: S101 return cls( { 1: 7, 3: cls.ALGORITHM, -1: public_key.public_bytes_raw(), } ) class MLDSA87(CoseKey): ALGORITHM = -50 def verify(self, message, signature): if self[1] != 7: raise ValueError("Invalid key type") pk = mldsa.MLDSA87PublicKey.from_public_bytes(self[-1]) pk.verify(signature, message) @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, mldsa.MLDSA87PublicKey) # noqa: S101 return cls( { 1: 7, 3: cls.ALGORITHM, -1: public_key.public_bytes_raw(), } )