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