Refactor quite a bit, add methods for verifying signatures.

This commit is contained in:
Dain Nilsson 2018-04-04 16:15:29 +02:00
parent 5e4a1df567
commit a143baa2b5
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
13 changed files with 527 additions and 92 deletions

View File

@ -25,4 +25,16 @@
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import abc
import six
if six.PY2:
@six.add_metaclass(abc.ABCMeta)
class ABC(object):
pass
abc.ABC = ABC
abc.abstractclassmethod = abc.abstractmethod
__version__ = '0.2.0-dev0'

94
fido_host/attestation.py Normal file
View File

@ -0,0 +1,94 @@
# 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 absolute_import, unicode_literals
from .cose import CoseKey, ES256
from cryptography.hazmat.backends import default_backend
from cryptography import x509
import abc
class Attestation(abc.ABC):
@abc.abstractmethod
def verify(self, statement, auth_data, client_data_hash):
pass
@staticmethod
def for_type(format):
return next(cls for cls in Attestation.__subclasses__()
if getattr(cls, 'FORMAT') == format)
class NoneAttestation(Attestation):
FORMAT = 'none'
def verify(self, statement, auth_data, client_data_hash):
if statement != {}:
raise ValueError('None Attestation requires empty statement.')
class FidoU2FAttestation(Attestation):
FORMAT = 'fido-u2f'
def verify(self, statement, auth_data, client_data_hash):
cd = auth_data.credential_data
pk = b'\x04' + cd.public_key[-2] + cd.public_key[-3]
FidoU2FAttestation.verify_signature(
auth_data.rp_id_hash,
client_data_hash,
cd.credential_id,
pk,
statement['x5c'][0],
statement['sig']
)
@staticmethod
def verify_signature(app_param, client_param, key_handle, public_key, cert,
signature):
m = b'\0' + app_param + client_param + key_handle + public_key
certificate = x509.load_der_x509_certificate(cert, default_backend())
ES256(certificate.public_key()).verify(m, signature)
class PackedAttestation(Attestation):
FORMAT = 'packed'
def verify(self, statement, auth_data, client_data_hash):
if 'ecdaaKeyId' in statement:
raise NotImplementedError('ECDAA not implemented')
alg = statement['alg']
x5c = statement.get('x5c')
if x5c:
cert = x509.load_der_x509_certificate(x5c[0], default_backend())
pub_key = CoseKey.for_alg(alg)(cert.public_key())
# TODO: Additional verification based on extension, etc.
else:
pub_key = CoseKey.parse(auth_data.credential_data.public_key)
if pub_key.ALGORITHM != alg:
raise ValueError('Algorithm does not match that of public key!')
pub_key.verify(auth_data + client_data_hash, statement['sig'])

View File

@ -27,15 +27,16 @@
from __future__ import absolute_import, unicode_literals
from .ctap import CtapError
from .hid import STATUS
from .u2f import CTAP1, APDU, ApduError
from .fido2 import (CTAP2, PinProtocolV1, AttestedCredentialData,
AuthenticatorData, AttestationObject, AssertionResponse)
from .ctap import CtapError
from .ctap1 import CTAP1, APDU, ApduError
from .ctap2 import (CTAP2, PinProtocolV1, AttestationObject, AssertionResponse)
from .cose import ES256
from .rpid import verify_rp_id, verify_app_id
from .utils import Timeout, sha256, hmac_sha256, websafe_decode, websafe_encode
from enum import IntEnum, unique
from enum import Enum, IntEnum, unique
import json
import six
class ClientData(bytes):
@ -84,7 +85,7 @@ class ClientError(Exception):
self.cause = None
def __repr__(self):
r = 'U2F Client error: {0} - {0.name}'.format(self.code)
r = 'Client error: {0} - {0.name}'.format(self.code)
if self.cause:
r += '. Caused by {}'.format(self.cause)
return r
@ -143,6 +144,12 @@ def _call_polling(poll_delay, timeout, on_keepalive, func, *args, **kwargs):
raise ClientError.ERR.TIMEOUT()
@unique
class U2F_TYPE(six.text_type, Enum):
REGISTER = 'navigator.id.finishEnrollment'
SIGN = 'navigator.id.getAssertion'
class U2fClient(object):
def __init__(self, device, origin, verify=verify_app_id):
self.poll_delay = 0.25
@ -188,7 +195,7 @@ class U2fClient(object):
raise ClientError.ERR.DEVICE_INELIGIBLE()
client_data = ClientData.build(
typ='navigator.id.finishEnrollment',
typ=U2F_TYPE.REGISTER,
challenge=challenge,
origin=self.origin
)
@ -198,6 +205,7 @@ class U2fClient(object):
self.poll_delay, timeout, on_keepalive, self.ctap.register,
client_data.hash, app_param
)
return {
'registrationData': reg_data.b64,
'clientData': client_data.b64
@ -206,7 +214,7 @@ class U2fClient(object):
def sign(self, app_id, challenge, registered_keys, timeout=None,
on_keepalive=None):
client_data = ClientData.build(
typ='navigator.id.getAssertion',
typ=U2F_TYPE.SIGN,
challenge=challenge,
origin=self.origin
)
@ -238,9 +246,9 @@ class U2fClient(object):
@unique
class CRED_ALGO(IntEnum):
ES256 = -7
RS256 = -257
class FIDO2_TYPE(six.text_type, Enum):
MAKE_CREDENTIAL = 'webauthn.create'
GET_ASSERTION = 'webauthn.get'
class Fido2Client(object):
@ -266,13 +274,13 @@ class Fido2Client(object):
pass # Fall through to ClientError
raise ClientError.ERR.BAD_REQUEST()
def make_credential(self, rp, user, challenge, algos=[CRED_ALGO.ES256],
def make_credential(self, rp, user, challenge, algos=[ES256.ALGORITHM],
exclude_list=None, extensions=None, rk=False, uv=False,
pin=None, timeout=None, on_keepalive=None):
self._verify_rp_id(rp['id'])
client_data = ClientData.build(
type='webauthn.create',
type=FIDO2_TYPE.MAKE_CREDENTIAL,
clientExtensions={},
challenge=challenge,
origin=self.origin
@ -319,7 +327,7 @@ class Fido2Client(object):
def _ctap1_make_credential(self, client_data, rp, user, algos, exclude_list,
extensions, rk, uv, pin, timeout, on_keepalive):
if rk or uv:
if rk or uv or ES256.ALGORITHM not in algos:
raise CtapError(CtapError.ERR.UNSUPPORTED_OPTION)
app_param = sha256(rp['id'].encode())
@ -336,32 +344,10 @@ class Fido2Client(object):
self.ctap.register, dummy_param, app_param)
raise ClientError.ERR.DEVICE_INELIGIBLE()
reg_resp = _call_polling(self.ctap1_poll_delay, timeout, on_keepalive,
self.ctap.register, client_data.hash,
app_param)
return AttestationObject.create(
'fido-u2f',
AuthenticatorData.create(
app_param,
0x41,
0,
AttestedCredentialData.create(
b'\0'*16, # aaguid
reg_resp.key_handle,
{ # EC256 public key
1: 2,
3: -7,
-1: 1,
-2: reg_resp.public_key[1:1+32],
-3: reg_resp.public_key[33:33+32]
}
)
),
{ # att_statement
'x5c': [reg_resp.certificate],
'sig': reg_resp.signature
}
return AttestationObject.from_ctap1(
app_param,
_call_polling(self.ctap1_poll_delay, timeout, on_keepalive,
self.ctap.register, client_data.hash, app_param)
)
def get_assertion(self, rp_id, challenge, allow_list=None, extensions=None,
@ -370,7 +356,7 @@ class Fido2Client(object):
self._verify_rp_id(rp_id)
client_data = ClientData.build(
type='webauthn.get',
type=FIDO2_TYPE.GET_ASSERTION,
clientExtensions={},
challenge=challenge,
origin=self.origin
@ -429,15 +415,9 @@ class Fido2Client(object):
self.ctap1_poll_delay, timeout, on_keepalive,
self.ctap.authenticate, client_param, app_param, cred['id']
)
return [AssertionResponse.create(
cred,
AuthenticatorData.create(
app_param,
auth_resp.user_presence & 0x01,
auth_resp.counter
),
auth_resp.signature
)]
return [
AssertionResponse.from_ctap1(app_param, cred, auth_resp)
]
except ClientError as e:
if e.code == ClientError.ERR.TIMEOUT:
raise # Other errors are ignored so we move to the next.

112
fido_host/cose.py Normal file
View File

@ -0,0 +1,112 @@
# 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 absolute_import, unicode_literals
from .utils import bytes2int, int2bytes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding
import abc
class CoseKey(abc.ABC):
@abc.abstractmethod
def verify(self, message, signature):
raise NotImplementedError('Signature verification not supported.')
@abc.abstractmethod
def to_cose(self):
raise NotImplemented
@abc.abstractclassmethod
def from_cose(cls, cose):
raise NotImplemented
@staticmethod
def for_alg(alg):
return next(cls for cls in CoseKey.__subclasses__()
if getattr(cls, 'ALGORITHM') == alg or cls.__name__ == alg)
@staticmethod
def parse(cose):
return CoseKey.for_alg(cose[3]).from_cose(cose)
class ES256(CoseKey):
ALGORITHM = -7
def __init__(self, public_key):
self.public_key = public_key
def verify(self, message, signature):
self.public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
def to_cose(self):
pn = self.public_key.public_numbers()
return {
1: 2,
3: self.ALGORITHM,
-1: 1,
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32)
}
@classmethod
def from_cose(cls, cose):
return cls(ec.EllipticCurvePublicNumbers(
bytes2int(cose[-2]), bytes2int(cose[-3]),
ec.SECP256R1()).public_key(default_backend()))
@classmethod
def from_ctap1(cls, data):
return cls.from_cose({-2: data[1:33], -3: data[33:65]})
class RS256(CoseKey):
ALGORITHM = -257
def __init__(self, public_key):
self.public_key = public_key
def verify(self, message, signature):
self.public_key.verify(signature, message, padding.PKCS1v15(),
hashes.SHA256())
def to_cose(self):
pn = self.public_key.public_numbers()
return {
1: 3,
3: self.ALGORITHM,
-1: int2bytes(pn.n),
-2: int2bytes(pn.e)
}
@classmethod
def from_cose(cls, cose):
return cls(rsa.RSAPublicNumbers(bytes2int(cose[-2]), bytes2int(cose[-1])
).public_key(default_backend()))

View File

@ -29,15 +29,6 @@ from __future__ import absolute_import
from enum import IntEnum, unique
import abc
import six
if six.PY2:
@six.add_metaclass(abc.ABCMeta)
class ABC(object):
pass
abc.ABC = ABC
abc.abstractclassmethod = abc.abstractmethod
class CtapDevice(abc.ABC):
@ -58,14 +49,12 @@ class CtapDevice(abc.ABC):
as an argument. The callback is only invoked once for consecutive
keepalive messages with the same status.
"""
pass
@abc.abstractclassmethod
def list_devices(cls):
"""
Generates instances of cls for discoverable devices.
"""
pass
class CtapError(Exception):

View File

@ -28,7 +28,9 @@
from __future__ import absolute_import, unicode_literals
from .hid import CTAPHID
from .utils import websafe_encode
from .utils import websafe_encode, websafe_decode, bytes2int
from .cose import ES256
from .attestation import FidoU2FAttestation
from enum import IntEnum, unique
from binascii import b2a_hex
import struct
@ -43,7 +45,6 @@ class APDU(IntEnum):
class ApduError(Exception):
def __init__(self, code, data=b''):
self.code = code
self.data = data
@ -66,7 +67,7 @@ class RegistrationData(bytes):
cert_len = six.indexbytes(self, cert_offs + 1)
if cert_len > 0x80:
n_bytes = cert_len - 0x80
cert_len = int(b2a_hex(self[cert_offs+2:cert_offs+2+n_bytes]), 16) \
cert_len = bytes2int(self[cert_offs+2:cert_offs+2+n_bytes]) \
+ n_bytes
cert_len += 2
self.certificate = self[cert_offs:cert_offs+cert_len]
@ -76,6 +77,11 @@ class RegistrationData(bytes):
def b64(self):
return websafe_encode(self)
def verify(self, app_param, client_param):
FidoU2FAttestation.verify_signature(
app_param, client_param, self.key_handle, self.public_key,
self.certificate, self.signature)
def __repr__(self):
return ("RegistrationData(public_key: h'%s', key_handle: h'%s', "
"certificate: h'%s', signature: h'%s')") % (
@ -90,6 +96,10 @@ class RegistrationData(bytes):
def __str__(self):
return '%r' % self
@classmethod
def from_b64(cls, data):
return cls(websafe_decode(data))
class SignatureData(bytes):
def __init__(self, _):
@ -100,6 +110,10 @@ class SignatureData(bytes):
def b64(self):
return websafe_encode(self)
def verify(self, app_param, client_param, public_key):
m = app_param + self[:5] + client_param
ES256.from_ctap1(public_key).verify(m, self.signature)
def __repr__(self):
return ('SignatureData(user_presence: 0x%02x, counter: %d, '
"signature: h'%s'") % (self.user_presence, self.counter,
@ -108,9 +122,12 @@ class SignatureData(bytes):
def __str__(self):
return '%r' % self
@classmethod
def from_b64(cls, data):
return cls(websafe_decode(data))
class CTAP1(object):
@unique
class INS(IntEnum):
REGISTER = 0x01

View File

@ -30,13 +30,14 @@ from __future__ import absolute_import, unicode_literals
from . import cbor
from .ctap import CtapError
from .hid import CTAPHID, CAPABILITY
from .utils import Timeout, sha256, hmac_sha256
from .utils import Timeout, sha256, hmac_sha256, bytes2int, int2bytes
from .attestation import Attestation
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from binascii import b2a_hex, a2b_hex
from binascii import b2a_hex
from enum import IntEnum, unique
import struct
import six
@ -206,10 +207,44 @@ class AttestationObject(bytes):
def __str__(self):
return self.__repr__()
def verify(self, client_param):
attestation = Attestation.for_type(self.fmt)
if attestation:
attestation().verify(self.att_statement, self.auth_data,
client_param)
else:
raise ValueError('Unsupported format: %s' % self.fmt)
@classmethod
def create(cls, fmt, auth_data, att_stmt):
return cls(cbor.dumps(args(fmt, auth_data, att_stmt)))
@classmethod
def from_ctap1(cls, app_param, registration):
return cls.create(
'fido-u2f',
AuthenticatorData.create(
app_param,
0x41,
0,
AttestedCredentialData.create(
b'\0'*16, # aaguid
registration.key_handle,
{ # EC256 public key
1: 2,
3: -7,
-1: 1,
-2: registration.public_key[1:1+32],
-3: registration.public_key[33:33+32]
}
)
),
{ # att_statement
'x5c': [registration.certificate],
'sig': registration.signature
}
)
class AssertionResponse(bytes):
@unique
@ -243,11 +278,26 @@ class AssertionResponse(bytes):
def __str__(self):
return self.__repr__()
def verify(self, client_param, public_key):
public_key.verify(self.auth_data + client_param)
@classmethod
def create(cls, credential, auth_data, signature, user=None, n_creds=None):
return cls(cbor.dumps(args(credential, auth_data, signature, user,
n_creds)))
@classmethod
def from_ctap1(cls, app_param, credential, authentication):
return cls.create(
credential,
AuthenticatorData.create(
app_param,
authentication.user_presence & 0x01,
authentication.counter
),
authentication.signature
)
class CTAP2(object):
@unique
@ -372,22 +422,21 @@ class PinProtocolV1(object):
def _init_shared_secret(self):
be = default_backend()
sk = ec.generate_private_key(ec.SECP256R1(), be)
pk = sk.public_key().public_numbers()
pn = sk.public_key().public_numbers()
key_agreement = {
1: 2,
3: -15,
-1: 1,
-2: a2b_hex('%064x' % pk.x),
-3: a2b_hex('%064x' % pk.y)
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32)
}
resp = self.ctap.client_pin(PinProtocolV1.VERSION,
PinProtocolV1.CMD.GET_KEY_AGREEMENT)
pk = resp[PinProtocolV1.RESULT.KEY_AGREEMENT]
x = int(b2a_hex(pk[-2]), 16)
y = int(b2a_hex(pk[-3]), 16)
x = bytes2int(pk[-2])
y = bytes2int(pk[-3])
pk = ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key(be)
shared_secret = sha256(sk.exchange(ec.ECDH(), pk))
shared_secret = sha256(sk.exchange(ec.ECDH(), pk)) # x-coordinate, 32b
return key_agreement, shared_secret
def get_pin_token(self, pin):

View File

@ -29,6 +29,7 @@ from base64 import urlsafe_b64decode, urlsafe_b64encode
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hmac, hashes
from threading import Timer, Event
from binascii import b2a_hex
import six
import numbers
@ -37,7 +38,9 @@ __all__ = [
'websafe_encode',
'websafe_decode',
'sha256',
'hmac_sha256'
'hmac_sha256',
'bytes2int',
'int2bytes'
]
@ -53,6 +56,20 @@ def hmac_sha256(key, data):
return h.finalize()
def bytes2int(value):
return int(b2a_hex(value), 16)
def int2bytes(value, minlen=-1):
ba = []
while value > 0xff:
ba.append(0xff & value)
value >>= 8
ba.append(value)
ba.extend([0]*(minlen - len(ba)))
return bytes(bytearray(reversed(ba)))
def websafe_decode(data):
if isinstance(data, six.text_type):
data = data.encode('ascii')

58
test/test_attestation.py Normal file
View File

@ -0,0 +1,58 @@
# Copyright (c) 2013 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 absolute_import, unicode_literals
from fido_host.ctap2 import AuthenticatorData
from fido_host.attestation import FidoU2FAttestation, PackedAttestation
from binascii import a2b_hex
import unittest
class TestAttestationObject(unittest.TestCase):
def test_fido_u2f_attestation(self):
FidoU2FAttestation().verify(
{
'sig': a2b_hex(b'30450220324779C68F3380288A1197B6095F7A6EB9B1B1C127F66AE12A99FE8532EC23B9022100E39516AC4D61EE64044D50B415A6A4D4D84BA6D895CB5AB7A1AA7D081DE341FA'), # noqa
'x5c': [a2b_hex(b'3082024A30820132A0030201020204046C8822300D06092A864886F70D01010B0500302E312C302A0603550403132359756269636F2055324620526F6F742043412053657269616C203435373230303633313020170D3134303830313030303030305A180F32303530303930343030303030305A302C312A302806035504030C2159756269636F205532462045452053657269616C203234393138323332343737303059301306072A8648CE3D020106082A8648CE3D030107034200043CCAB92CCB97287EE8E639437E21FCD6B6F165B2D5A3F3DB131D31C16B742BB476D8D1E99080EB546C9BBDF556E6210FD42785899E78CC589EBE310F6CDB9FF4A33B3039302206092B0601040182C40A020415312E332E362E312E342E312E34313438322E312E323013060B2B0601040182E51C020101040403020430300D06092A864886F70D01010B050003820101009F9B052248BC4CF42CC5991FCAABAC9B651BBE5BDCDC8EF0AD2C1C1FFB36D18715D42E78B249224F92C7E6E7A05C49F0E7E4C881BF2E94F45E4A21833D7456851D0F6C145A29540C874F3092C934B43D222B8962C0F410CEF1DB75892AF116B44A96F5D35ADEA3822FC7146F6004385BCB69B65C99E7EB6919786703C0D8CD41E8F75CCA44AA8AB725AD8E799FF3A8696A6F1B2656E631B1E40183C08FDA53FA4A8F85A05693944AE179A1339D002D15CABD810090EC722EF5DEF9965A371D415D624B68A2707CAD97BCDD1785AF97E258F33DF56A031AA0356D8E8D5EBCADC74E071636C6B110ACE5CC9B90DFEACAE640FF1BB0F1FE5DB4EFF7A95F060733F5')] # noqa
},
AuthenticatorData(a2b_hex(b'1194228DA8FDBDEEFD261BD7B6595CFD70A50D70C6407BCF013DE96D4EFB17DE41000000000000000000000000000000000000000000403EBD89BF77EC509755EE9C2635EFAAAC7B2B9C5CEF1736C3717DA48534C8C6B654D7FF945F50B5CC4E78055BDD396B64F78DA2C5F96200CCD415CD08FE420038A5010203262001215820E87625896EE4E46DC032766E8087962F36DF9DFE8B567F3763015B1990A60E1422582027DE612D66418BDA1950581EBC5C8C1DAD710CB14C22F8C97045F4612FB20C91')), # noqa
a2b_hex(b'687134968222EC17202E42505F8ED2B16AE22F16BB05B88C25DB9E602645F141') # noqa
)
def test_packed_attestation(self):
PackedAttestation().verify(
{
'alg': -7,
'sig': a2b_hex(b'304402204D49A9F9D58E2BDF8C20489BA318636422E4860048E391A105A408EBB556623F02201232B8F6CCA0A56F6654A1824B0E6F085DF2D7CF922F6F62A6B2F5F520D57EE6'), # noqa
'x5c': [a2b_hex(b'3082019330820138A003020102020900859B726CB24B4C29300A06082A8648CE3D0403023047310B300906035504061302555331143012060355040A0C0B59756269636F205465737431223020060355040B0C1941757468656E74696361746F72204174746573746174696F6E301E170D3136313230343131353530305A170D3236313230323131353530305A3047310B300906035504061302555331143012060355040A0C0B59756269636F205465737431223020060355040B0C1941757468656E74696361746F72204174746573746174696F6E3059301306072A8648CE3D020106082A8648CE3D03010703420004AD11EB0E8852E53AD5DFED86B41E6134A18EC4E1AF8F221A3C7D6E636C80EA13C3D504FF2E76211BB44525B196C44CB4849979CF6F896ECD2BB860DE1BF4376BA30D300B30090603551D1304023000300A06082A8648CE3D0403020349003046022100E9A39F1B03197525F7373E10CE77E78021731B94D0C03F3FDA1FD22DB3D030E7022100C4FAEC3445A820CF43129CDB00AABEFD9AE2D874F9C5D343CB2F113DA23723F3')] # noqa
},
AuthenticatorData(a2b_hex(b'0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12410000002BF8A011F38C0A4D15800617111F9EDC7D0040A17370D9C1759005700C8DE77E7DFD3A0A5300E0A26E5213AA40D6DF10EE4028B58B5F34167035D840BEBAE0C5CE8FD05AD9BD33E3BE7D1C558D81AB4803570BA5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C')), # noqa
a2b_hex(b'11B8E5AA20AF76E2E9A6229E2D151480DC5B303130473205E69DD36AD742109C') # noqa
)

View File

@ -36,8 +36,8 @@ from binascii import a2b_hex
from fido_host.utils import sha256, websafe_decode
from fido_host.hid import CAPABILITY
from fido_host.ctap import CtapError
from fido_host.u2f import ApduError, APDU, RegistrationData, SignatureData
from fido_host.fido2 import Info, AttestationObject
from fido_host.ctap1 import ApduError, APDU, RegistrationData, SignatureData
from fido_host.ctap2 import Info, AttestationObject
from fido_host.client import ClientData, U2fClient, ClientError, Fido2Client

72
test/test_cose.py Normal file
View File

@ -0,0 +1,72 @@
# Copyright (c) 2013 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 absolute_import, unicode_literals
from fido_host import cbor
from fido_host.cose import CoseKey, ES256, RS256
from binascii import a2b_hex
import unittest
_ES256_KEY = a2b_hex(b'A5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C') # noqa
_RS256_KEY = a2b_hex(b'A401030339010020590100B610DCE84B65029FAE24F7BF8A1730D37BC91435642A628E691E9B030BF3F7CEC59FF91CBE82C54DE16C136FA4FA8A58939B5A950B32E03073592FEC8D8B33601C04F70E5E2D5CF7B4E805E1990EA5A86928A1B390EB9026527933ACC03E6E41DC0BE40AA5EB7B9B460743E4DD80895A758FB3F3F794E5E9B8310D3A60C28F2410D95CF6E732749A243A30475267628B456DE770BC2185BBED1D451ECB0062A3D132C0E4D842E0DDF93A444A3EE33A85C2E913156361713155F1F1DC64E8E68ED176466553BBDE669EB82810B104CB4407D32AE6316C3BD6F382EC3AE2C5FD49304986D64D92ED11C25B6C5CF1287233545A987E9A3E169F99790603DBA5C8AD2143010001') # noqa
class TestCoseKey(unittest.TestCase):
def test_ES256_parse_verify(self):
key = CoseKey.parse(cbor.loads(_ES256_KEY)[0])
self.assertIsInstance(key, ES256)
self.assertEqual(key.to_cose(), {
1: 2,
3: -7,
-1: 1,
-2: a2b_hex(b'A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1'), # noqa
-3: a2b_hex(b'FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C') # noqa
})
key.verify(
a2b_hex(b'0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000002C' + # noqa
b'7B89F12A9088B0F5EE0EF8F6718BCCC374249C31AEEBAEB79BD0450132CD536C'), # noqa
a2b_hex(b'304402202B3933FE954A2D29DE691901EB732535393D4859AAA80D58B08741598109516D0220236FBE6B52326C0A6B1CFDC6BF0A35BDA92A6C2E41E40C3A1643428D820941E0') # noqa
)
def test_RS256_parse_verify(self):
key = CoseKey.parse(cbor.loads(_RS256_KEY)[0])
self.assertIsInstance(key, RS256)
self.assertEqual(key.to_cose(), {
1: 3,
3: -257,
-1: a2b_hex(b'B610DCE84B65029FAE24F7BF8A1730D37BC91435642A628E691E9B030BF3F7CEC59FF91CBE82C54DE16C136FA4FA8A58939B5A950B32E03073592FEC8D8B33601C04F70E5E2D5CF7B4E805E1990EA5A86928A1B390EB9026527933ACC03E6E41DC0BE40AA5EB7B9B460743E4DD80895A758FB3F3F794E5E9B8310D3A60C28F2410D95CF6E732749A243A30475267628B456DE770BC2185BBED1D451ECB0062A3D132C0E4D842E0DDF93A444A3EE33A85C2E913156361713155F1F1DC64E8E68ED176466553BBDE669EB82810B104CB4407D32AE6316C3BD6F382EC3AE2C5FD49304986D64D92ED11C25B6C5CF1287233545A987E9A3E169F99790603DBA5C8AD'), # noqa
-2: a2b_hex(b'010001') # noqa
})
key.verify(
a2b_hex(b'0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000002E' + # noqa
b'CC9340FD84950987BA667DBE9B2C97C7241E15E2B54869A0DD1CE2013C4064B8'), # noqa
a2b_hex(b'071B707D11F0E7F62861DFACA89C4E674321AD8A6E329FDD40C7D6971348FBB0514E7B2B0EFE215BAAC0365C4124A808F8180D6575B710E7C01DAE8F052D0C5A2CE82F487C656E7AD824F3D699BE389ADDDE2CBF39E87A8955E93202BAE8830AB4139A7688DFDAD849F1BB689F3852BA05BED70897553CC44704F6941FD1467AD6A46B4DAB503716D386FE7B398E78E0A5A8C4040539D2C9BFA37E4D94F96091FFD1D194DE2CA58E9124A39757F013801421E09BD261ADA31992A8B0386A80AF51A87BD0CEE8FDAB0D4651477670D4C7B245489BED30A57B83964DB79418D5A4F5F2E5ABCA274426C9F90B007A962AE15DFF7343AF9E110746E2DB9226D785C6') # noqa
)

View File

@ -27,7 +27,8 @@
from __future__ import absolute_import, unicode_literals
from fido_host.u2f import CTAP1, ApduError
from fido_host.ctap1 import CTAP1, ApduError
from fido_host.client import ClientData
from binascii import a2b_hex
import unittest
import mock
@ -65,33 +66,53 @@ class TestCTAP1(unittest.TestCase):
ctap = CTAP1(mock.MagicMock())
ctap.device.call.return_value = a2b_hex('0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871') + b'\x90\x00' # noqa
resp = ctap.register(b'\1'*32, b'\2'*32)
ctap.device.call.assert_called_with(0x03, b'\0\1\0\0\0\0\x40' +
b'\1'*32 + b'\2'*32 + b'\0\0')
client_param = a2b_hex(b'4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb') # noqa
app_param = a2b_hex(b'f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4') # noqa
resp = ctap.register(client_param, app_param)
ctap.device.call.assert_called_with(
0x03,
b'\0\1\0\0\0\0\x40' +
client_param +
app_param +
b'\0\0'
)
self.assertEqual(resp.public_key, a2b_hex('04b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9')) # noqa
self.assertEqual(resp.key_handle, a2b_hex('2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25')) # noqa
self.assertEqual(resp.certificate, a2b_hex('3082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df')) # noqa
self.assertEqual(resp.signature, a2b_hex('304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871')) # noqa
client_data = ClientData(b'{"typ":"navigator.id.finishEnrollment","challenge":"vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo","cid_pubkey":{"kty":"EC","crv":"P-256","x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8","y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},"origin":"http://example.com"}') # noqa
resp.verify(app_param, client_param)
def test_authenticate(self):
ctap = CTAP1(mock.MagicMock())
ctap.device.call.return_value = a2b_hex('0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f') + b'\x90\x00' # noqa
resp = ctap.authenticate(b'\1'*32, b'\2'*32, b'\3'*64)
client_param = a2b_hex(b'ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57') # noqa
app_param = a2b_hex(b'4b0be934baebb5d12d26011b69227fa5e86df94e7d94aa2949a89f2d493992ca') # noqa
key_handle = b'\3'*64
resp = ctap.authenticate(client_param, app_param, key_handle)
ctap.device.call.assert_called_with(0x03, b'\0\2\3\0\0\0\x81' +
b'\1'*32 + b'\2'*32 + b'\x40' +
b'\3'*64 + b'\0\0')
client_param + app_param + b'\x40' +
key_handle + b'\0\0')
self.assertEqual(resp.user_presence, 1)
self.assertEqual(resp.counter, 1)
self.assertEqual(resp.signature, a2b_hex('304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f')) # noqa
ctap.authenticate(b'\1'*32, b'\2'*32, b'\3'*8)
ctap.device.call.assert_called_with(0x03, b'\0\2\3\0\0\0\x49' +
b'\1'*32 + b'\2'*32 + b'\x08' +
b'\3'*8 + b'\0\0')
public_key = a2b_hex(b'04d368f1b665bade3c33a20f1e429c7750d5033660c019119d29aa4ba7abc04aa7c80a46bbe11ca8cb5674d74f31f8a903f6bad105fb6ab74aefef4db8b0025e1d') # noqa
client_data = ClientData(b'{"typ":"navigator.id.getAssertion","challenge":"opsXqUifDriAAmWclinfbS0e-USY0CgyJHe_Otd7z8o","cid_pubkey":{"kty":"EC","crv":"P-256","x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8","y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},"origin":"http://example.com"}') # noqa
resp.verify(app_param, client_param, public_key)
ctap.authenticate(b'\1'*32, b'\2'*32, b'\3'*8, True)
key_handle = b'\4'*8
ctap.authenticate(client_param, app_param, key_handle)
ctap.device.call.assert_called_with(0x03, b'\0\2\3\0\0\0\x49' +
client_param + app_param + b'\x08' +
key_handle + b'\0\0')
ctap.authenticate(client_param, app_param, key_handle, True)
ctap.device.call.assert_called_with(0x03, b'\0\2\7\0\0\0\x49' +
b'\1'*32 + b'\2'*32 + b'\x08' +
b'\3'*8 + b'\0\0')
client_param + app_param + b'\x08' +
key_handle + b'\0\0')

View File

@ -27,7 +27,8 @@
from __future__ import absolute_import, unicode_literals
from fido_host.fido2 import (CTAP2, PinProtocolV1, Info, AttestedCredentialData,
from fido_host.ctap1 import RegistrationData
from fido_host.ctap2 import (CTAP2, PinProtocolV1, Info, AttestedCredentialData,
AuthenticatorData, AttestationObject,
AssertionResponse)
from fido_host import cbor
@ -119,6 +120,19 @@ _CRED = {'type': 'public-key', 'id': _CRED_ID}
_SIGNATURE = a2b_hex('304402206765CBF6E871D3AF7F01AE96F06B13C90F26F54B905C5166A2C791274FC2397102200B143893586CC799FBA4DA83B119EAEA1BD80AC3CE88FCEDB3EFBD596A1F4F63') # noqa
class TestAttestationObject(unittest.TestCase):
def test_fido_u2f_attestation(self):
att = AttestationObject.from_ctap1(
a2b_hex(b'1194228DA8FDBDEEFD261BD7B6595CFD70A50D70C6407BCF013DE96D4EFB17DE'), # noqa
RegistrationData(a2b_hex(b'0504E87625896EE4E46DC032766E8087962F36DF9DFE8B567F3763015B1990A60E1427DE612D66418BDA1950581EBC5C8C1DAD710CB14C22F8C97045F4612FB20C91403EBD89BF77EC509755EE9C2635EFAAAC7B2B9C5CEF1736C3717DA48534C8C6B654D7FF945F50B5CC4E78055BDD396B64F78DA2C5F96200CCD415CD08FE4200383082024A30820132A0030201020204046C8822300D06092A864886F70D01010B0500302E312C302A0603550403132359756269636F2055324620526F6F742043412053657269616C203435373230303633313020170D3134303830313030303030305A180F32303530303930343030303030305A302C312A302806035504030C2159756269636F205532462045452053657269616C203234393138323332343737303059301306072A8648CE3D020106082A8648CE3D030107034200043CCAB92CCB97287EE8E639437E21FCD6B6F165B2D5A3F3DB131D31C16B742BB476D8D1E99080EB546C9BBDF556E6210FD42785899E78CC589EBE310F6CDB9FF4A33B3039302206092B0601040182C40A020415312E332E362E312E342E312E34313438322E312E323013060B2B0601040182E51C020101040403020430300D06092A864886F70D01010B050003820101009F9B052248BC4CF42CC5991FCAABAC9B651BBE5BDCDC8EF0AD2C1C1FFB36D18715D42E78B249224F92C7E6E7A05C49F0E7E4C881BF2E94F45E4A21833D7456851D0F6C145A29540C874F3092C934B43D222B8962C0F410CEF1DB75892AF116B44A96F5D35ADEA3822FC7146F6004385BCB69B65C99E7EB6919786703C0D8CD41E8F75CCA44AA8AB725AD8E799FF3A8696A6F1B2656E631B1E40183C08FDA53FA4A8F85A05693944AE179A1339D002D15CABD810090EC722EF5DEF9965A371D415D624B68A2707CAD97BCDD1785AF97E258F33DF56A031AA0356D8E8D5EBCADC74E071636C6B110ACE5CC9B90DFEACAE640FF1BB0F1FE5DB4EFF7A95F060733F530450220324779C68F3380288A1197B6095F7A6EB9B1B1C127F66AE12A99FE8532EC23B9022100E39516AC4D61EE64044D50B415A6A4D4D84BA6D895CB5AB7A1AA7D081DE341FA')) # noqa
)
att.verify(a2b_hex(b'687134968222EC17202E42505F8ED2B16AE22F16BB05B88C25DB9E602645F141')) # noqa
def test_packed_attestation(self):
att = AttestationObject(a2b_hex(b'a301667061636b6564025900c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12410000002bf8a011f38c0a4d15800617111f9edc7d0040a17370d9c1759005700c8de77e7dfd3a0a5300e0a26e5213aa40d6df10ee4028b58b5f34167035d840bebae0c5ce8fd05ad9bd33e3be7d1c558d81ab4803570ba5010203262001215820a5fd5ce1b1c458c530a54fa61b31bf6b04be8b97afde54dd8cbb69275a8a1be1225820fa3a3231dd9deed9d1897be5a6228c59501e4bcd12975d3dff730f01278ea61c03a363616c6726637369675846304402204d49a9f9d58e2bdf8c20489ba318636422e4860048e391a105a408ebb556623f02201232b8f6cca0a56f6654a1824b0e6f085df2d7cf922f6f62a6b2f5f520d57ee663783563815901973082019330820138a003020102020900859b726cb24b4c29300a06082a8648ce3d0403023047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e301e170d3136313230343131353530305a170d3236313230323131353530305a3047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3059301306072a8648ce3d020106082a8648ce3d03010703420004ad11eb0e8852e53ad5dfed86b41e6134a18ec4e1af8f221a3c7d6e636c80ea13c3d504ff2e76211bb44525b196c44cb4849979cf6f896ecd2bb860de1bf4376ba30d300b30090603551d1304023000300a06082a8648ce3d0403020349003046022100e9a39f1b03197525f7373e10ce77e78021731b94d0c03f3fda1fd22db3d030e7022100c4faec3445a820cf43129cdb00aabefd9ae2d874f9c5d343cb2f113da23723f3')) # noqa
att.verify(a2b_hex(b'11B8E5AA20AF76E2E9A6229E2D151480DC5B303130473205E69DD36AD742109C')) # noqa
class TestCTAP2(unittest.TestCase):
def test_send_cbor_ok(self):
ctap = CTAP2(mock.MagicMock())