def Register(self, app_id, challenge, registered_keys): """Registers app_id with the security key. Executes the U2F registration flow with the security key. Args: app_id: The app_id to register the security key against. challenge: Server challenge passed to the security key. registered_keys: List of keys already registered for this app_id+user. Returns: RegisterResponse with key_handle and attestation information in it ( encoded in FIDO U2F binary format within registration_data field). Raises: U2FError: There was some kind of problem with registration (e.g. the device was already registered or there was a timeout waiting for the test of user presence). """ client_data = model.ClientData(model.ClientData.TYP_REGISTRATION, challenge, self.origin) challenge_param = self.InternalSHA256(client_data.GetJson()) app_param = self.InternalSHA256(app_id) for key in registered_keys: try: # skip non U2F_V2 keys if key.version != u'U2F_V2': continue resp = self.security_key.CmdAuthenticate( challenge_param, app_param, key.key_handle, True) # check_only mode CmdAuthenticate should always raise some # exception raise errors.HardwareError('Should Never Happen') except errors.TUPRequiredError: # This indicates key was valid. Thus, no need to register raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE) except errors.InvalidKeyHandleError as e: # This is the case of a key for a different token, so we just ignore it. pass except errors.HardwareError as e: raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) # Now register the new key for _ in range(30): try: resp = self.security_key.CmdRegister(challenge_param, app_param) return model.RegisterResponse(resp, client_data) except errors.TUPRequiredError as e: self.security_key.CmdWink() time.sleep(0.5) except errors.HardwareError as e: raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) raise errors.U2FError(errors.U2FError.TIMEOUT)
def Authenticate(self, app_id, challenge, registered_keys): """Authenticates app_id with the security key. Executes the U2F authentication/signature flow with the security key. Args: app_id: The app_id to register the security key against. challenge: Server challenge passed to the security key. registered_keys: List of keys already registered for this app_id+user. Returns: SignResponse with client_data, key_handle, and signature_data. The client data is an object, while the signature_data is encoded in FIDO U2F binary format. Raises: U2FError: There was some kind of problem with registration (e.g. the device was already registered or there was a timeout while waiting for the test of user presence.) """ client_data = model.ClientData(model.ClientData.TYP_AUTHENTICATION, challenge, self.origin) app_param = self.InternalSHA256(app_id) challenge_param = self.InternalSHA256(client_data.GetJson()) num_invalid_keys = 0 for key in registered_keys: try: if key.version != 'U2F_V2': continue for _ in range(10): try: resp = self.security_key.CmdAuthenticate( challenge_param, app_param, key.key_handle) return model.SignResponse(key.key_handle, resp, client_data) except errors.TUPRequiredError: self.security_key.CmdWink() time.sleep(0.5) except errors.InvalidKeyHandleError: num_invalid_keys += 1 continue except errors.HardwareError as e: raise errors.U2FError(errors.U2FError.BAD_REQUEST, e) if num_invalid_keys == len(registered_keys): # In this case, all provided keys were invalid. raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE) # In this case, the TUP was not pressed. raise errors.U2FError(errors.U2FError.TIMEOUT)
def testSignMultipleIneligible(self, mock_get_u2f_method): """Test signing with multiple keys registered, but none eligible.""" # Prepare u2f mocks mock_u2f = mock.MagicMock() mock_get_u2f_method.return_value = mock_u2f mock_authenticate = mock.MagicMock() mock_u2f.Authenticate = mock_authenticate mock_authenticate.side_effect = errors.U2FError( errors.U2FError.DEVICE_INELIGIBLE) # Call LocalAuthenticator challenge_item = { 'key': SIGN_SUCCESS['registered_key'], 'challenge': SIGN_SUCCESS['challenge'] } challenge_data = [challenge_item, challenge_item] authenticator = localauthenticator.LocalAuthenticator('testorigin') with self.assertRaises(errors.U2FError) as cm: authenticator.Authenticate(SIGN_SUCCESS['app_id'], challenge_data) self.assertEquals(cm.exception.code, errors.U2FError.DEVICE_INELIGIBLE)
def testSignMultipleSuccess(self, mock_get_u2f_method): """Test signing with multiple keys registered and one is eligible.""" # Prepare u2f mocks mock_u2f = mock.MagicMock() mock_get_u2f_method.return_value = mock_u2f mock_authenticate = mock.MagicMock() mock_u2f.Authenticate = mock_authenticate return_value = model.SignResponse( base64.urlsafe_b64decode(SIGN_SUCCESS['key_handle_encoded']), base64.urlsafe_b64decode(SIGN_SUCCESS['signature_data_encoded']), SIGN_SUCCESS['client_data']) mock_authenticate.side_effect = [ errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE), return_value ] # Call LocalAuthenticator challenge_item = { 'key': SIGN_SUCCESS['registered_key'], 'challenge': SIGN_SUCCESS['challenge'] } challenge_data = [challenge_item, challenge_item] authenticator = localauthenticator.LocalAuthenticator('testorigin') response = authenticator.Authenticate(SIGN_SUCCESS['app_id'], challenge_data) # Validate that u2f authenticate was called with the correct values self.assertTrue(mock_authenticate.called) authenticate_args = mock_authenticate.call_args[0] self.assertEqual(len(authenticate_args), 3) self.assertEqual(authenticate_args[0], SIGN_SUCCESS['app_id']) self.assertEqual(authenticate_args[1], SIGN_SUCCESS['challenge']) registered_keys = authenticate_args[2] self.assertEqual(len(registered_keys), 1) self.assertEqual(registered_keys[0], SIGN_SUCCESS['registered_key']) # Validate authenticator response self.assertEquals(response.get('clientData'), SIGN_SUCCESS['client_data_encoded']) self.assertEquals(response.get('signatureData'), SIGN_SUCCESS['signature_data_encoded']) self.assertEquals(response.get('applicationId'), SIGN_SUCCESS['app_id']) self.assertEquals(response.get('keyHandle'), SIGN_SUCCESS['key_handle_encoded'])
def Authenticate(self, app_id, challenge_data, print_callback=sys.stderr.write): """See base class.""" # If authenticator is not plugged in, prompt try: device = u2f.GetLocalU2FInterface(origin=self.origin) except errors.NoDeviceFoundError: print_callback( 'Please insert your security key and press enter...') raw_input() device = u2f.GetLocalU2FInterface(origin=self.origin) print_callback('Please touch your security key.\n') for challenge_item in challenge_data: raw_challenge = challenge_item['challenge'] key = challenge_item['key'] try: result = device.Authenticate(app_id, raw_challenge, [key]) except errors.U2FError as e: if e.code == errors.U2FError.DEVICE_INELIGIBLE: continue else: raise client_data = base64.urlsafe_b64encode( result.client_data.GetJson()) signature_data = base64.urlsafe_b64encode(result.signature_data) key_handle = base64.urlsafe_b64encode(result.key_handle) return { 'clientData': client_data, 'signatureData': signature_data, 'applicationId': app_id, 'keyHandle': key_handle, } raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE)
def _CallPlugin(self, cmd, input_json): """Calls the plugin and validates the response.""" # Calculate length of input input_length = len(input_json) length_bytes_le = struct.pack('<I', input_length) request = length_bytes_le + input_json.encode() # Call plugin sign_process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdout = sign_process.communicate(request)[0] exit_status = sign_process.wait() # Parse and validate response size response_len_le = stdout[:4] response_len = struct.unpack('<I', response_len_le)[0] response = stdout[4:] if response_len != len(response): raise errors.PluginError( 'Plugin response length {} does not match data {} (exit_status={})' .format(response_len, len(response), exit_status)) # Ensure valid json try: json_response = json.loads(response.decode()) except ValueError: raise errors.PluginError('Plugin returned invalid output (exit_status={})' .format(exit_status)) # Ensure response type if json_response.get('type') != 'sign_helper_reply': raise errors.PluginError('Plugin returned invalid response type ' '(exit_status={})' .format(exit_status)) # Parse response codes result_code = json_response.get('code') if result_code is None: raise errors.PluginError('Plugin missing result code (exit_status={})' .format(exit_status)) # Handle errors if result_code == SK_SIGNING_PLUGIN_TOUCH_REQUIRED: raise errors.U2FError(errors.U2FError.TIMEOUT) elif result_code == SK_SIGNING_PLUGIN_WRONG_DATA: raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE) elif result_code != SK_SIGNING_PLUGIN_NO_ERROR: raise errors.PluginError( 'Plugin failed with error {} - {} (exit_status={})' .format(result_code, json_response.get('errorDetail'), exit_status)) # Ensure response data is present response_data = json_response.get('responseData') if response_data is None: raise errors.PluginErrors( 'Plugin returned output with missing responseData (exit_status={})' .format(exit_status)) return response_data