1# Copyright 2016 Google Inc. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Implement a high level U2F API analogous to the javascript API spec. 16 17This modules implements a high level U2F API that is analogous in spirit 18to the high level U2F javascript API. It supports both registration and 19authetication. For the purposes of this API, the "origin" is the hostname 20of the machine this library is running on. 21""" 22 23import hashlib 24import socket 25import time 26 27from pyu2f import errors 28from pyu2f import hardware 29from pyu2f import hidtransport 30from pyu2f import model 31 32 33def GetLocalU2FInterface(origin=socket.gethostname()): 34 """Obtains a U2FInterface for the first valid local U2FHID device found.""" 35 hid_transports = hidtransport.DiscoverLocalHIDU2FDevices() 36 for t in hid_transports: 37 try: 38 return U2FInterface(security_key=hardware.SecurityKey(transport=t), 39 origin=origin) 40 except errors.UnsupportedVersionException: 41 # Skip over devices that don't speak the proper version of the protocol. 42 pass 43 44 # Unable to find a device 45 raise errors.NoDeviceFoundError() 46 47 48class U2FInterface(object): 49 """High level U2F interface. 50 51 Implements a high level interface in the spirit of the FIDO U2F 52 javascript API high level interface. It supports registration 53 and authentication (signing). 54 55 IMPORTANT NOTE: This class does NOT validate the app id against the 56 origin. In particular, any user can assert any app id all the way to 57 the device. The security model of a python library is such that doing 58 so would not provide significant benfit as it could be bypassed by the 59 caller talking to a lower level of the API. In fact, so could the origin 60 itself. The origin is still set to a plausible value (the hostname) by 61 this library. 62 63 TODO(gdasher): Figure out a plan on how to address this gap/document the 64 consequences of this more clearly. 65 """ 66 67 def __init__(self, security_key, origin=socket.gethostname()): 68 self.origin = origin 69 self.security_key = security_key 70 71 if self.security_key.CmdVersion() != b'U2F_V2': 72 raise errors.UnsupportedVersionException() 73 74 def Register(self, app_id, challenge, registered_keys): 75 """Registers app_id with the security key. 76 77 Executes the U2F registration flow with the security key. 78 79 Args: 80 app_id: The app_id to register the security key against. 81 challenge: Server challenge passed to the security key. 82 registered_keys: List of keys already registered for this app_id+user. 83 84 Returns: 85 RegisterResponse with key_handle and attestation information in it ( 86 encoded in FIDO U2F binary format within registration_data field). 87 88 Raises: 89 U2FError: There was some kind of problem with registration (e.g. 90 the device was already registered or there was a timeout waiting 91 for the test of user presence). 92 """ 93 client_data = model.ClientData(model.ClientData.TYP_REGISTRATION, challenge, 94 self.origin) 95 challenge_param = self.InternalSHA256(client_data.GetJson()) 96 app_param = self.InternalSHA256(app_id) 97 98 for key in registered_keys: 99 try: 100 # skip non U2F_V2 keys 101 if key.version != u'U2F_V2': 102 continue 103 resp = self.security_key.CmdAuthenticate(challenge_param, app_param, 104 key.key_handle, True) 105 # check_only mode CmdAuthenticate should always raise some 106 # exception 107 raise errors.HardwareError('Should Never Happen') 108 109 except errors.TUPRequiredError: 110 # This indicates key was valid. Thus, no need to register 111 raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE) 112 except errors.InvalidKeyHandleError as e: 113 # This is the case of a key for a different token, so we just ignore it. 114 pass 115 except errors.HardwareError as e: 116 raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) 117 118 # Now register the new key 119 for _ in range(30): 120 try: 121 resp = self.security_key.CmdRegister(challenge_param, app_param) 122 return model.RegisterResponse(resp, client_data) 123 except errors.TUPRequiredError as e: 124 self.security_key.CmdWink() 125 time.sleep(0.5) 126 except errors.HardwareError as e: 127 raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) 128 129 raise errors.U2FError(errors.U2FError.TIMEOUT) 130 131 def Authenticate(self, app_id, challenge, registered_keys): 132 """Authenticates app_id with the security key. 133 134 Executes the U2F authentication/signature flow with the security key. 135 136 Args: 137 app_id: The app_id to register the security key against. 138 challenge: Server challenge passed to the security key as a bytes object. 139 registered_keys: List of keys already registered for this app_id+user. 140 141 Returns: 142 SignResponse with client_data, key_handle, and signature_data. The client 143 data is an object, while the signature_data is encoded in FIDO U2F binary 144 format. 145 146 Raises: 147 U2FError: There was some kind of problem with authentication (e.g. 148 there was a timeout while waiting for the test of user presence.) 149 """ 150 client_data = model.ClientData(model.ClientData.TYP_AUTHENTICATION, 151 challenge, self.origin) 152 app_param = self.InternalSHA256(app_id) 153 challenge_param = self.InternalSHA256(client_data.GetJson()) 154 num_invalid_keys = 0 155 for key in registered_keys: 156 try: 157 if key.version != u'U2F_V2': 158 continue 159 for _ in range(30): 160 try: 161 resp = self.security_key.CmdAuthenticate(challenge_param, app_param, 162 key.key_handle) 163 return model.SignResponse(key.key_handle, resp, client_data) 164 except errors.TUPRequiredError: 165 self.security_key.CmdWink() 166 time.sleep(0.5) 167 except errors.InvalidKeyHandleError: 168 num_invalid_keys += 1 169 continue 170 except errors.HardwareError as e: 171 raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) 172 173 if num_invalid_keys == len(registered_keys): 174 # In this case, all provided keys were invalid. 175 raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE) 176 177 # In this case, the TUP was not pressed. 178 raise errors.U2FError(errors.U2FError.TIMEOUT) 179 180 def InternalSHA256(self, string): 181 md = hashlib.sha256() 182 md.update(string.encode()) 183 return md.digest() 184