mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-14 03:30:02 +00:00
Merge pull request #736 from trezor/andrewkozlik/731
U2F UX improvements
This commit is contained in:
commit
20cda137b7
@ -110,13 +110,18 @@ _CLIENTPIN_SUBCMD_GET_KEY_AGREEMENT = const(0x02)
|
||||
_CLIENTPIN_RESP_KEY_AGREEMENT = const(0x01) # COSE_Key, optional
|
||||
|
||||
# status codes for the keepalive cmd
|
||||
_KEEPALIVE_STATUS_NONE = const(0x00)
|
||||
_KEEPALIVE_STATUS_PROCESSING = const(0x01) # still processing the current request
|
||||
_KEEPALIVE_STATUS_UP_NEEDED = const(0x02) # waiting for user presence
|
||||
|
||||
# time intervals and timeouts
|
||||
_KEEPALIVE_INTERVAL_MS = const(80) # interval between keepalive commands
|
||||
_CTAP_HID_TIMEOUT_MS = const(500)
|
||||
_U2F_CONFIRM_TIMEOUT_MS = const(10 * 1000)
|
||||
_CTAP_HID_TIMEOUT_MS = const(
|
||||
500
|
||||
) # maximum interval between CTAP HID continuation frames
|
||||
_U2F_CONFIRM_TIMEOUT_MS = const(
|
||||
3 * 1000
|
||||
) # maximum U2F pollling interval, Chrome uses 200 ms
|
||||
_FIDO2_CONFIRM_TIMEOUT_MS = const(60 * 1000)
|
||||
_POPUP_TIMEOUT_MS = const(4 * 1000)
|
||||
|
||||
@ -175,7 +180,9 @@ _U2FHID_IF_VERSION = const(2) # interface version
|
||||
_U2F_REGISTER_ID = const(0x05) # version 2 registration identifier
|
||||
_U2F_ATT_PRIV_KEY = b"q&\xac+\xf6D\xdca\x86\xad\x83\xef\x1f\xcd\xf1*W\xb5\xcf\xa2\x00\x0b\x8a\xd0'\xe9V\xe8T\xc5\n\x8b"
|
||||
_U2F_ATT_CERT = b"0\x82\x01\x180\x81\xc0\x02\t\x00\xb1\xd9\x8fBdr\xd3,0\n\x06\x08*\x86H\xce=\x04\x03\x020\x151\x130\x11\x06\x03U\x04\x03\x0c\nTrezor U2F0\x1e\x17\r160429133153Z\x17\r260427133153Z0\x151\x130\x11\x06\x03U\x04\x03\x0c\nTrezor U2F0Y0\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x03B\x00\x04\xd9\x18\xbd\xfa\x8aT\xac\x92\xe9\r\xa9\x1f\xcaz\xa2dT\xc0\xd1s61M\xde\x83\xa5K\x86\xb5\xdfN\xf0Re\x9a\x1do\xfc\xb7F\x7f\x1a\xcd\xdb\x8a3\x08\x0b^\xed\x91\x89\x13\xf4C\xa5&\x1b\xc7{h`o\xc10\n\x06\x08*\x86H\xce=\x04\x03\x02\x03G\x000D\x02 $\x1e\x81\xff\xd2\xe5\xe6\x156\x94\xc3U.\x8f\xeb\xd7\x1e\x895\x92\x1c\xb4\x83ACq\x1cv\xea\xee\xf3\x95\x02 _\x80\xeb\x10\xf2\\\xcc9\x8b<\xa8\xa9\xad\xa4\x02\x7f\x93\x13 w\xb7\xab\xcewFZ'\xf5=3\xa1\x1d"
|
||||
_BOGUS_APPID = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
_BOGUS_APPID_CHROME = b"A" * 32
|
||||
_BOGUS_APPID_FIREFOX = b"\0" * 32
|
||||
_BOGUS_APPIDS = (_BOGUS_APPID_CHROME, _BOGUS_APPID_FIREFOX)
|
||||
_AAGUID = b"\xd6\xd0\xbd\xc3b\xee\xc4\xdb\xde\x8dzenJD\x87" # First 16 bytes of SHA-256("TREZOR 2")
|
||||
_BOGUS_PRIV_KEY = b"\xAA" * 32
|
||||
|
||||
@ -218,6 +225,9 @@ _USE_BASIC_ATTESTATION = False
|
||||
# The CID of the last WINK command. Used to ensure that we do only one WINK at a time on any given CID.
|
||||
_last_wink_cid = 0
|
||||
|
||||
# The CID of the last successful U2F_AUTHENTICATE check-only request.
|
||||
_last_good_auth_check_cid = 0
|
||||
|
||||
|
||||
class CborError(Exception):
|
||||
def __init__(self, code: int):
|
||||
@ -598,6 +608,10 @@ class U2fState(State, ConfirmInfo):
|
||||
self._req_data = req_data
|
||||
self.load_icon(self._cred.rp_id_hash)
|
||||
|
||||
def keepalive_status(self) -> int:
|
||||
# Run the keepalive loop to check for timeout, but do not send any keepalive messages.
|
||||
return _KEEPALIVE_STATUS_NONE
|
||||
|
||||
def timeout_ms(self) -> int:
|
||||
return _U2F_CONFIRM_TIMEOUT_MS
|
||||
|
||||
@ -615,13 +629,20 @@ class U2fConfirmRegister(U2fState):
|
||||
super().__init__(cid, iface, req_data, cred)
|
||||
|
||||
async def confirm_dialog(self) -> bool:
|
||||
if self._cred.rp_id_hash == _BOGUS_APPID:
|
||||
if self._cred.rp_id_hash in _BOGUS_APPIDS:
|
||||
text = Text("U2F", ui.ICON_WRONG, ui.RED)
|
||||
text.bold("Not registered.")
|
||||
text.br_half()
|
||||
text.normal(
|
||||
"Another U2F device", "was used to register", "in this application."
|
||||
)
|
||||
if self.cid == _last_good_auth_check_cid:
|
||||
text.bold("Already registered.")
|
||||
text.br_half()
|
||||
text.normal(
|
||||
"This device is already", "registered with this", "application."
|
||||
)
|
||||
else:
|
||||
text.bold("Not registered.")
|
||||
text.br_half()
|
||||
text.normal(
|
||||
"This device is not", "registered with this", "application."
|
||||
)
|
||||
return await Popup(text, _POPUP_TIMEOUT_MS)
|
||||
else:
|
||||
content = ConfirmContent(self)
|
||||
@ -945,11 +966,12 @@ class DialogManager:
|
||||
|
||||
async def keepalive_loop(self) -> None:
|
||||
try:
|
||||
if not isinstance(self.state, Fido2State):
|
||||
if not isinstance(self.state, (U2fState, Fido2State)):
|
||||
return
|
||||
while utime.ticks_ms() < self.deadline:
|
||||
cmd = cmd_keepalive(self.state.cid, self.state.keepalive_status())
|
||||
await send_cmd(cmd, self.iface)
|
||||
if self.state.keepalive_status() != _KEEPALIVE_STATUS_NONE:
|
||||
cmd = cmd_keepalive(self.state.cid, self.state.keepalive_status())
|
||||
await send_cmd(cmd, self.iface)
|
||||
await loop.sleep(_KEEPALIVE_INTERVAL_MS * 1000)
|
||||
finally:
|
||||
self.keepalive = None
|
||||
@ -1128,11 +1150,18 @@ def msg_register(req: Msg, dialog_mgr: DialogManager) -> Cmd:
|
||||
dialog_mgr.reset_timeout()
|
||||
|
||||
# wait for a button or continue
|
||||
if dialog_mgr.result != _RESULT_CONFIRM:
|
||||
if dialog_mgr.result == _RESULT_NONE:
|
||||
if __debug__:
|
||||
log.info(__name__, "waiting for button")
|
||||
return msg_error(req.cid, _SW_CONDITIONS_NOT_SATISFIED)
|
||||
|
||||
if dialog_mgr.result != _RESULT_CONFIRM:
|
||||
if __debug__:
|
||||
log.info(__name__, "request declined")
|
||||
# There is no standard way to decline a U2F request, but responding with ERR_CHANNEL_BUSY
|
||||
# doesn't seem to violate the protocol and at least stops Chrome from polling.
|
||||
return cmd_error(req.cid, _ERR_CHANNEL_BUSY)
|
||||
|
||||
# sign the registration challenge and return
|
||||
if __debug__:
|
||||
log.info(__name__, "signing register")
|
||||
@ -1199,6 +1228,8 @@ def msg_authenticate(req: Msg, dialog_mgr: DialogManager) -> Cmd:
|
||||
if req.p1 == _AUTH_CHECK_ONLY:
|
||||
if __debug__:
|
||||
log.info(__name__, "_AUTH_CHECK_ONLY")
|
||||
global _last_good_auth_check_cid
|
||||
_last_good_auth_check_cid = req.cid
|
||||
return msg_error(req.cid, _SW_CONDITIONS_NOT_SATISFIED)
|
||||
|
||||
# from now on, only _AUTH_ENFORCE is supported
|
||||
@ -1215,11 +1246,18 @@ def msg_authenticate(req: Msg, dialog_mgr: DialogManager) -> Cmd:
|
||||
dialog_mgr.reset_timeout()
|
||||
|
||||
# wait for a button or continue
|
||||
if dialog_mgr.result != _RESULT_CONFIRM:
|
||||
if dialog_mgr.result == _RESULT_NONE:
|
||||
if __debug__:
|
||||
log.info(__name__, "waiting for button")
|
||||
return msg_error(req.cid, _SW_CONDITIONS_NOT_SATISFIED)
|
||||
|
||||
if dialog_mgr.result != _RESULT_CONFIRM:
|
||||
if __debug__:
|
||||
log.info(__name__, "request declined")
|
||||
# There is no standard way to decline a U2F request, but responding with ERR_CHANNEL_BUSY
|
||||
# doesn't seem to violate the protocol and at least stops Chrome from polling.
|
||||
return cmd_error(req.cid, _ERR_CHANNEL_BUSY)
|
||||
|
||||
# sign the authentication challenge and return
|
||||
if __debug__:
|
||||
log.info(__name__, "signing authentication")
|
||||
@ -1400,7 +1438,9 @@ def cbor_make_credential(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]:
|
||||
# User verification requested, but PIN is not enabled.
|
||||
state_set = dialog_mgr.set_state(Fido2ConfirmNoPin(req.cid, dialog_mgr.iface))
|
||||
if state_set:
|
||||
return cbor_error(req.cid, _ERR_UNSUPPORTED_OPTION)
|
||||
# We should return _ERR_UNSUPPORTED_OPTION, but since we claim in GetInfo that the PIN
|
||||
# is set even when it's not, it makes more sense to return _ERR_OPERATION_DENIED.
|
||||
return cbor_error(req.cid, _ERR_OPERATION_DENIED)
|
||||
else:
|
||||
return cmd_error(req.cid, _ERR_CHANNEL_BUSY)
|
||||
|
||||
@ -1557,7 +1597,9 @@ def cbor_get_assertion(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]:
|
||||
# User verification requested, but PIN is not enabled.
|
||||
state_set = dialog_mgr.set_state(Fido2ConfirmNoPin(req.cid, dialog_mgr.iface))
|
||||
if state_set:
|
||||
return cbor_error(req.cid, _ERR_UNSUPPORTED_OPTION)
|
||||
# We should return _ERR_UNSUPPORTED_OPTION, but since we claim in GetInfo that the PIN
|
||||
# is set even when it's not, it makes more sense to return _ERR_OPERATION_DENIED.
|
||||
return cbor_error(req.cid, _ERR_OPERATION_DENIED)
|
||||
else:
|
||||
return cmd_error(req.cid, _ERR_CHANNEL_BUSY)
|
||||
|
||||
@ -1715,6 +1757,8 @@ def cbor_get_assertion_sign(
|
||||
|
||||
|
||||
def cbor_get_info(req: Cmd) -> Cmd:
|
||||
# Note: We claim that the PIN is set even when it's not, because otherwise
|
||||
# login.live.com shows an error, but doesn't instruct the user to set a PIN.
|
||||
response_data = {
|
||||
_GETINFO_RESP_VERSIONS: ["U2F_V2", "FIDO_2_0"],
|
||||
_GETINFO_RESP_EXTENSIONS: ["hmac-secret"],
|
||||
|
Loading…
Reference in New Issue
Block a user