Source code for fido2.cose

# 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 .utils import bytes2int, int2bytes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding, ed25519, types
from typing import Sequence, Type, Mapping, Any, TypeVar


[docs] class CoseKey(dict): """A COSE formatted public key. :param _: The COSE key paramters. :cvar ALGORITHM: COSE algorithm identifier. """ ALGORITHM: int = None # type: ignore
[docs] 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.")
[docs] @classmethod def from_cryptography_key( cls: Type[T_CoseKey], public_key: types.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.")
[docs] @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. """ for cls in CoseKey.__subclasses__(): if cls.ALGORITHM == alg: return cls return UnsupportedKey
[docs] @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. """ for cls in CoseKey.__subclasses__(): if cls.__name__ == name: return cls return UnsupportedKey
[docs] @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)
[docs] @staticmethod def supported_algorithms() -> Sequence[int]: """Get a list of all supported algorithm identifiers""" algs: Sequence[Type[CoseKey]] = [ ES256, EdDSA, ES384, ES512, PS256, RS256, ES256K, ] return [cls.ALGORITHM for cls in algs]
T_CoseKey = TypeVar("T_CoseKey", bound=CoseKey)
[docs] class UnsupportedKey(CoseKey): """A COSE key with an unsupported algorithm."""
[docs] class ES256(CoseKey): ALGORITHM = -7 _HASH_ALG = hashes.SHA256()
[docs] 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(default_backend()).verify( signature, message, ec.ECDSA(self._HASH_ALG) )
[docs] @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ec.EllipticCurvePublicKey) # nosec pn = public_key.public_numbers() return cls( { 1: 2, 3: cls.ALGORITHM, -1: 1, -2: int2bytes(pn.x, 32), -3: int2bytes(pn.y, 32), } )
[docs] @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]})
[docs] class ES384(CoseKey): ALGORITHM = -35 _HASH_ALG = hashes.SHA384()
[docs] 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(default_backend()).verify( signature, message, ec.ECDSA(self._HASH_ALG) )
[docs] @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ec.EllipticCurvePublicKey) # nosec pn = public_key.public_numbers() return cls( { 1: 2, 3: cls.ALGORITHM, -1: 2, -2: int2bytes(pn.x, 48), -3: int2bytes(pn.y, 48), } )
[docs] class ES512(CoseKey): ALGORITHM = -36 _HASH_ALG = hashes.SHA512()
[docs] 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(default_backend()).verify( signature, message, ec.ECDSA(self._HASH_ALG) )
[docs] @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ec.EllipticCurvePublicKey) # nosec pn = public_key.public_numbers() return cls( { 1: 2, 3: cls.ALGORITHM, -1: 3, -2: int2bytes(pn.x, 66), -3: int2bytes(pn.y, 66), } )
[docs] class RS256(CoseKey): ALGORITHM = -257 _HASH_ALG = hashes.SHA256()
[docs] def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( default_backend() ).verify(signature, message, padding.PKCS1v15(), self._HASH_ALG)
[docs] @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, rsa.RSAPublicKey) # nosec pn = public_key.public_numbers() return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)})
[docs] class PS256(CoseKey): ALGORITHM = -37 _HASH_ALG = hashes.SHA256()
[docs] def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( default_backend() ).verify( signature, message, padding.PSS( mgf=padding.MGF1(self._HASH_ALG), salt_length=padding.PSS.MAX_LENGTH ), self._HASH_ALG, )
[docs] @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, rsa.RSAPublicKey) # nosec pn = public_key.public_numbers() return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)})
[docs] class EdDSA(CoseKey): ALGORITHM = -8
[docs] def verify(self, message, signature): if self[-1] != 6: raise ValueError("Unsupported elliptic curve") ed25519.Ed25519PublicKey.from_public_bytes(self[-2]).verify(signature, message)
[docs] @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ed25519.Ed25519PublicKey) # nosec return cls( { 1: 1, 3: cls.ALGORITHM, -1: 6, -2: public_key.public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ), } )
[docs] class RS1(CoseKey): ALGORITHM = -65535 _HASH_ALG = hashes.SHA1() # nosec
[docs] def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( default_backend() ).verify(signature, message, padding.PKCS1v15(), self._HASH_ALG)
[docs] @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, rsa.RSAPublicKey) # nosec pn = public_key.public_numbers() return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)})
[docs] class ES256K(CoseKey): ALGORITHM = -47 _HASH_ALG = hashes.SHA256()
[docs] 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(default_backend()).verify( signature, message, ec.ECDSA(self._HASH_ALG) )
[docs] @classmethod def from_cryptography_key(cls, public_key): assert isinstance(public_key, ec.EllipticCurvePublicKey) # nosec pn = public_key.public_numbers() return cls( { 1: 2, 3: cls.ALGORITHM, -1: 8, -2: int2bytes(pn.x, 32), -3: int2bytes(pn.y, 32), } )