# 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 fido_host.fido2 import (CTAP2, PinProtocolV1, Info, AttestedCredentialData, AuthenticatorData, AttestationObject, AssertionResponse) from fido_host import cbor from binascii import a2b_hex from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec import unittest import mock _AAGUID = a2b_hex('F8A011F38C0A4D15800617111F9EDC7D') _INFO = a2b_hex('a60182665532465f5632684649444f5f325f3002826375766d6b686d61632d7365637265740350f8a011f38c0a4d15800617111f9edc7d04a462726bf5627570f564706c6174f469636c69656e7450696ef4051904b0068101') # noqa class TestInfo(unittest.TestCase): def test_parse_bytes(self): info = Info(_INFO) self.assertEqual(info.versions, ['U2F_V2', 'FIDO_2_0']) self.assertEqual(info.extensions, ['uvm', 'hmac-secret']) self.assertEqual(info.aaguid, _AAGUID) self.assertEqual(info.options, { 'rk': True, 'up': True, 'plat': False, 'clientPin': False }) self.assertEqual(info.max_msg_size, 1200) self.assertEqual(info.pin_protocols, [1]) self.assertEqual(info.data, { Info.KEY.VERSIONS: ['U2F_V2', 'FIDO_2_0'], Info.KEY.EXTENSIONS: ['uvm', 'hmac-secret'], Info.KEY.AAGUID: _AAGUID, Info.KEY.OPTIONS: { 'clientPin': False, 'plat': False, 'rk': True, 'up': True }, Info.KEY.MAX_MSG_SIZE: 1200, Info.KEY.PIN_PROTOCOLS: [1] }) _ATT_CRED_DATA = a2b_hex('f8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a5290') # noqa _CRED_ID = a2b_hex('fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783') # noqa _PUB_KEY = {1: 2, 3: -7, -1: 1, -2: a2b_hex('643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf'), -3: a2b_hex('171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a5290')} # noqa class TestAttestedCredentialData(unittest.TestCase): def test_parse_bytes(self): data = AttestedCredentialData(_ATT_CRED_DATA) self.assertEqual(data.aaguid, _AAGUID) self.assertEqual(data.credential_id, _CRED_ID) self.assertEqual(data.public_key, _PUB_KEY) def test_create_from_args(self): data = AttestedCredentialData.create(_AAGUID, _CRED_ID, _PUB_KEY) self.assertEqual(_ATT_CRED_DATA, data) _AUTH_DATA_MC = a2b_hex('0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12410000001CF8A011F38C0A4D15800617111F9EDC7D0040FE3AAC036D14C1E1C65518B698DD1DA8F596BC33E11072813466C6BF3845691509B80FB76D59309B8D39E0A93452688F6CA3A39A76F3FC52744FB73948B15783A5010203262001215820643566C206DD00227005FA5DE69320616CA268043A38F08BDE2E9DC45A5CAFAF225820171353B2932434703726AAE579FA6542432861FE591E481EA22D63997E1A5290') # noqa _AUTH_DATA_GA = a2b_hex('0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000001D') # noqa _RP_ID_HASH = a2b_hex('0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12') # noqa class TestAuthenticatorData(unittest.TestCase): def test_parse_bytes_make_credential(self): data = AuthenticatorData(_AUTH_DATA_MC) self.assertEqual(data.rp_id_hash, _RP_ID_HASH) self.assertEqual(data.flags, 0x41) self.assertEqual(data.counter, 28) self.assertEqual(data.credential_data, _ATT_CRED_DATA) self.assertIsNone(data.extensions) def test_parse_bytes_get_assertion(self): data = AuthenticatorData(_AUTH_DATA_GA) self.assertEqual(data.rp_id_hash, _RP_ID_HASH) self.assertEqual(data.flags, 0x01) self.assertEqual(data.counter, 29) self.assertIsNone(data.credential_data) self.assertIsNone(data.extensions) _MC_RESP = a2b_hex('a301667061636b6564025900c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12410000001cf8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a529003a363616c67266373696758483046022100cc1ef43edf07de8f208c21619c78a565ddcf4150766ad58781193be8e0a742ed022100f1ed7c7243e45b7d8e5bda6b1abf10af7391789d1ef21b70bd69fed48dba4cb163783563815901973082019330820138a003020102020900859b726cb24b4c29300a06082a8648ce3d0403023047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e301e170d3136313230343131353530305a170d3236313230323131353530305a3047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3059301306072a8648ce3d020106082a8648ce3d03010703420004ad11eb0e8852e53ad5dfed86b41e6134a18ec4e1af8f221a3c7d6e636c80ea13c3d504ff2e76211bb44525b196c44cb4849979cf6f896ecd2bb860de1bf4376ba30d300b30090603551d1304023000300a06082a8648ce3d0403020349003046022100e9a39f1b03197525f7373e10ce77e78021731b94d0c03f3fda1fd22db3d030e7022100c4faec3445a820cf43129cdb00aabefd9ae2d874f9c5d343cb2f113da23723f3') # noqa _GA_RESP = a2b_hex('a301a26269645840fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b1578364747970656a7075626c69632d6b6579025900250021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12010000001d035846304402206765cbf6e871d3af7f01ae96f06b13c90f26f54b905c5166a2c791274fc2397102200b143893586cc799fba4da83b119eaea1bd80ac3ce88fcedb3efbd596a1f4f63') # noqa _CRED_ID = a2b_hex('FE3AAC036D14C1E1C65518B698DD1DA8F596BC33E11072813466C6BF3845691509B80FB76D59309B8D39E0A93452688F6CA3A39A76F3FC52744FB73948B15783') # noqa _CRED = {'type': 'public-key', 'id': _CRED_ID} _SIGNATURE = a2b_hex('304402206765CBF6E871D3AF7F01AE96F06B13C90F26F54B905C5166A2C791274FC2397102200B143893586CC799FBA4DA83B119EAEA1BD80AC3CE88FCEDB3EFBD596A1F4F63') # noqa class TestCTAP2(unittest.TestCase): def test_send_cbor_ok(self): ctap = CTAP2(mock.MagicMock()) ctap.device.call.return_value = b'\0' + cbor.dumps({1: b'response'}) self.assertEqual({1: b'response'}, ctap.send_cbor(2, b'foobar')) ctap.device.call.assert_called_with(0x10, b'\2' + cbor.dumps(b'foobar'), None) def test_get_info(self): ctap = CTAP2(mock.MagicMock()) ctap.device.call.return_value = b'\0' + _INFO info = ctap.get_info() ctap.device.call.assert_called_with(0x10, b'\4', None) self.assertIsInstance(info, Info) def test_make_credential(self): ctap = CTAP2(mock.MagicMock()) ctap.device.call.return_value = b'\0' + _MC_RESP resp = ctap.make_credential(1, 2, 3, 4) ctap.device.call.assert_called_with( 0x10, b'\1' + cbor.dumps({1: 1, 2: 2, 3: 3, 4: 4}), None) self.assertIsInstance(resp, AttestationObject) self.assertEqual(resp, _MC_RESP) self.assertEqual(resp.fmt, 'packed') self.assertEqual(resp.auth_data, _AUTH_DATA_MC) self.assertSetEqual(set(resp.att_statement.keys()), {'alg', 'sig', 'x5c'}) def test_get_assertion(self): ctap = CTAP2(mock.MagicMock()) ctap.device.call.return_value = b'\0' + _GA_RESP resp = ctap.get_assertion(1, 2) ctap.device.call.assert_called_with( 0x10, b'\2' + cbor.dumps({1: 1, 2: 2}), None) self.assertIsInstance(resp, AssertionResponse) self.assertEqual(resp, _GA_RESP) self.assertEqual(resp.credential, _CRED) self.assertEqual(resp.auth_data, _AUTH_DATA_GA) self.assertEqual(resp.signature, _SIGNATURE) self.assertIsNone(resp.user) self.assertIsNone(resp.number_of_credentials) EC_PRIV = 0x7452e599fee739d8a653f6a507343d12d382249108a651402520b72f24fe7684 EC_PUB_X = a2b_hex('44D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47F') # noqa EC_PUB_Y = a2b_hex('EC802EE7D22BD4E100F12E48537EB4E7E96ED3A47A0A3BD5F5EEAB65001664F9') # noqa DEV_PUB_X = a2b_hex('0501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168') # noqa DEV_PUB_Y = a2b_hex('D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47') # noqa SHARED = a2b_hex('c42a039d548100dfba521e487debcbbb8b66bb7496f8b1862a7a395ed83e1a1c') # noqa TOKEN_ENC = a2b_hex('7A9F98E31B77BE90F9C64D12E9635040') TOKEN = a2b_hex('aff12c6dcfbf9df52f7a09211e8865cd') PIN_HASH_ENC = a2b_hex('afe8327ce416da8ee3d057589c2ce1a9') class TestPinProtocolV1(unittest.TestCase): @mock.patch('cryptography.hazmat.primitives.asymmetric.ec.generate_private_key') # noqa def test_establish_shared_secret(self, patched_generate): prot = PinProtocolV1(mock.MagicMock()) patched_generate.return_value = ec.derive_private_key( EC_PRIV, ec.SECP256R1(), default_backend() ) prot.ctap.client_pin.return_value = { 1: { 1: 2, 3: -25, -1: 1, -2: DEV_PUB_X, -3: DEV_PUB_Y } } key_agreement, shared = prot._init_shared_secret() self.assertEqual(shared, SHARED) self.assertEqual(key_agreement[-2], EC_PUB_X) self.assertEqual(key_agreement[-3], EC_PUB_Y) def test_get_pin_token(self): prot = PinProtocolV1(mock.MagicMock()) prot._init_shared_secret = mock.Mock(return_value=({}, SHARED)) prot.ctap.client_pin.return_value = { 2: TOKEN_ENC } self.assertEqual(prot.get_pin_token('1234'), TOKEN) prot.ctap.client_pin.assert_called_once() self.assertEqual(prot.ctap.client_pin.call_args[1]['pin_hash_enc'], PIN_HASH_ENC) def test_set_pin(self): prot = PinProtocolV1(mock.MagicMock()) prot._init_shared_secret = mock.Mock(return_value=({}, SHARED)) prot.set_pin('1234') prot.ctap.client_pin.assert_called_with( 1, 3, key_agreement={}, new_pin_enc=a2b_hex('0222fc42c6dd76a274a7057858b9b29d98e8a722ec2dc6668476168c5320473cec9907b4cd76ce7943c96ba5683943211d84471e64d9c51e54763488cd66526a'), # noqa pin_auth=a2b_hex('7b40c084ccc5794194189ab57836475f') ) def test_change_pin(self): prot = PinProtocolV1(mock.MagicMock()) prot._init_shared_secret = mock.Mock(return_value=({}, SHARED)) prot.change_pin('1234', '4321') prot.ctap.client_pin.assert_called_with( 1, 4, key_agreement={}, new_pin_enc=a2b_hex('4280e14aac4fcbf02dd079985f0c0ffc9ea7d5f9c173fd1a4c843826f7590cb3c2d080c6923e2fe6d7a52c31ea1309d3fcca3dedae8a2ef14b6330cafc79339e'), # noqa pin_auth=a2b_hex('fb97e92f3724d7c85e001d7f93e6490a'), pin_hash_enc=a2b_hex('afe8327ce416da8ee3d057589c2ce1a9') ) def test_short_pin(self): prot = PinProtocolV1(mock.MagicMock()) with self.assertRaises(ValueError): prot.set_pin('123') def test_long_pin(self): prot = PinProtocolV1(mock.MagicMock()) with self.assertRaises(ValueError): prot.set_pin('1'*256)