mirror of https://github.com/Yubico/python-fido2
Refactor quite a bit, add methods for verifying signatures.
This commit is contained in:
parent
5e4a1df567
commit
a143baa2b5
|
@ -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'
|
||||
|
|
|
@ -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'])
|
|
@ -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.
|
||||
|
|
|
@ -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()))
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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):
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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')
|
|
@ -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())
|
Loading…
Reference in New Issue