u2f.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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. """Implement a high level U2F API analogous to the javascript API spec.
  15. This modules implements a high level U2F API that is analogous in spirit
  16. to the high level U2F javascript API. It supports both registration and
  17. authetication. For the purposes of this API, the "origin" is the hostname
  18. of the machine this library is running on.
  19. """
  20. import hashlib
  21. import socket
  22. import time
  23. from pyu2f import errors
  24. from pyu2f import hardware
  25. from pyu2f import hidtransport
  26. from pyu2f import model
  27. def GetLocalU2FInterface(origin=socket.gethostname()):
  28. """Obtains a U2FInterface for the first valid local U2FHID device found."""
  29. hid_transports = hidtransport.DiscoverLocalHIDU2FDevices()
  30. for t in hid_transports:
  31. try:
  32. return U2FInterface(security_key=hardware.SecurityKey(transport=t),
  33. origin=origin)
  34. except errors.UnsupportedVersionException:
  35. # Skip over devices that don't speak the proper version of the protocol.
  36. pass
  37. # Unable to find a device
  38. raise errors.NoDeviceFoundError()
  39. class U2FInterface(object):
  40. """High level U2F interface.
  41. Implements a high level interface in the spirit of the FIDO U2F
  42. javascript API high level interface. It supports registration
  43. and authentication (signing).
  44. IMPORTANT NOTE: This class does NOT validate the app id against the
  45. origin. In particular, any user can assert any app id all the way to
  46. the device. The security model of a python library is such that doing
  47. so would not provide significant benfit as it could be bypassed by the
  48. caller talking to a lower level of the API. In fact, so could the origin
  49. itself. The origin is still set to a plausible value (the hostname) by
  50. this library.
  51. TODO(gdasher): Figure out a plan on how to address this gap/document the
  52. consequences of this more clearly.
  53. """
  54. def __init__(self, security_key, origin=socket.gethostname()):
  55. self.origin = origin
  56. self.security_key = security_key
  57. if self.security_key.CmdVersion() != b'U2F_V2':
  58. raise errors.UnsupportedVersionException()
  59. def Register(self, app_id, challenge, registered_keys):
  60. """Registers app_id with the security key.
  61. Executes the U2F registration flow with the security key.
  62. Args:
  63. app_id: The app_id to register the security key against.
  64. challenge: Server challenge passed to the security key.
  65. registered_keys: List of keys already registered for this app_id+user.
  66. Returns:
  67. RegisterResponse with key_handle and attestation information in it (
  68. encoded in FIDO U2F binary format within registration_data field).
  69. Raises:
  70. U2FError: There was some kind of problem with registration (e.g.
  71. the device was already registered or there was a timeout waiting
  72. for the test of user presence).
  73. """
  74. client_data = model.ClientData(model.ClientData.TYP_REGISTRATION, challenge,
  75. self.origin)
  76. challenge_param = self.InternalSHA256(client_data.GetJson())
  77. app_param = self.InternalSHA256(app_id)
  78. for key in registered_keys:
  79. try:
  80. # skip non U2F_V2 keys
  81. if key.version != u'U2F_V2':
  82. continue
  83. resp = self.security_key.CmdAuthenticate(challenge_param, app_param,
  84. key.key_handle, True)
  85. # check_only mode CmdAuthenticate should always raise some
  86. # exception
  87. raise errors.HardwareError('Should Never Happen')
  88. except errors.TUPRequiredError:
  89. # This indicates key was valid. Thus, no need to register
  90. raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE)
  91. except errors.InvalidKeyHandleError as e:
  92. # This is the case of a key for a different token, so we just ignore it.
  93. pass
  94. except errors.HardwareError as e:
  95. raise errors.U2FError(errors.U2FError.BAD_REQUEST, e)
  96. # Now register the new key
  97. for _ in range(30):
  98. try:
  99. resp = self.security_key.CmdRegister(challenge_param, app_param)
  100. return model.RegisterResponse(resp, client_data)
  101. except errors.TUPRequiredError as e:
  102. self.security_key.CmdWink()
  103. time.sleep(0.5)
  104. except errors.HardwareError as e:
  105. raise errors.U2FError(errors.U2FError.BAD_REQUEST, e)
  106. raise errors.U2FError(errors.U2FError.TIMEOUT)
  107. def Authenticate(self, app_id, challenge, registered_keys):
  108. """Authenticates app_id with the security key.
  109. Executes the U2F authentication/signature flow with the security key.
  110. Args:
  111. app_id: The app_id to register the security key against.
  112. challenge: Server challenge passed to the security key as a bytes object.
  113. registered_keys: List of keys already registered for this app_id+user.
  114. Returns:
  115. SignResponse with client_data, key_handle, and signature_data. The client
  116. data is an object, while the signature_data is encoded in FIDO U2F binary
  117. format.
  118. Raises:
  119. U2FError: There was some kind of problem with authentication (e.g.
  120. there was a timeout while waiting for the test of user presence.)
  121. """
  122. client_data = model.ClientData(model.ClientData.TYP_AUTHENTICATION,
  123. challenge, self.origin)
  124. app_param = self.InternalSHA256(app_id)
  125. challenge_param = self.InternalSHA256(client_data.GetJson())
  126. num_invalid_keys = 0
  127. for key in registered_keys:
  128. try:
  129. if key.version != u'U2F_V2':
  130. continue
  131. for _ in range(30):
  132. try:
  133. resp = self.security_key.CmdAuthenticate(challenge_param, app_param,
  134. key.key_handle)
  135. return model.SignResponse(key.key_handle, resp, client_data)
  136. except errors.TUPRequiredError:
  137. self.security_key.CmdWink()
  138. time.sleep(0.5)
  139. except errors.InvalidKeyHandleError:
  140. num_invalid_keys += 1
  141. continue
  142. except errors.HardwareError as e:
  143. raise errors.U2FError(errors.U2FError.BAD_REQUEST, e)
  144. if num_invalid_keys == len(registered_keys):
  145. # In this case, all provided keys were invalid.
  146. raise errors.U2FError(errors.U2FError.DEVICE_INELIGIBLE)
  147. # In this case, the TUP was not pressed.
  148. raise errors.U2FError(errors.U2FError.TIMEOUT)
  149. def InternalSHA256(self, string):
  150. md = hashlib.sha256()
  151. md.update(string.encode())
  152. return md.digest()