Skip to content

Commit d0ce38f

Browse files
authored
feat: add FIDO2 account selection and improve cancel/attestation hand… (#425)
feat: add FIDO2 account selection and improve cancel/attestation handling.
1 parent abbfa07 commit d0ce38f

7 files changed

Lines changed: 170 additions & 93 deletions

File tree

core/src/apps/webauthn/credential.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,11 @@ def app_name(self) -> str:
278278
return self.rp_id
279279

280280
def account_name(self) -> str | None:
281-
if self.user_name:
281+
# Some relying parties send a blank/whitespace-only "name" (e.g. " ");
282+
# treat those as absent and fall back to displayName, then user id.
283+
if self.user_name and self.user_name.strip():
282284
return self.user_name
283-
elif self.user_display_name:
285+
elif self.user_display_name and self.user_display_name.strip():
284286
return self.user_display_name
285287
elif self.user_id:
286288
return hexlify(self.user_id).decode()

core/src/apps/webauthn/fido2.py

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
from trezor.lvglui.i18n import gettext as _, keys as i18n_keys
1414
from trezor.ui.components.common.confirm import Pageable
1515
from trezor.ui.components.common.webauthn import ConfirmInfo
16-
from trezor.ui.layouts.lvgl.webauthn import confirm_webauthn, confirm_webauthn_reset
16+
from trezor.ui.layouts.lvgl.webauthn import (
17+
confirm_webauthn,
18+
confirm_webauthn_reset,
19+
select_webauthn_account,
20+
)
1721

1822
from apps.base import device_is_unlocked, set_homescreen
1923
from apps.common import cbor
@@ -176,7 +180,7 @@
176180
_FIDO_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"
177181
_FIDO_ATT_CERT = b"0\x82\x01\xcd0\x82\x01s\xa0\x03\x02\x01\x02\x02\x04\x03E`\xc40\n\x06\x08*\x86H\xce=\x04\x03\x020.1,0*\x06\x03U\x04\x03\x0c#Trezor FIDO Root CA Serial 841513560 \x17\r200406100417Z\x18\x0f20500406100417Z0x1\x0b0\t\x06\x03U\x04\x06\x13\x02CZ1\x1c0\x1a\x06\x03U\x04\n\x0c\x13SatoshiLabs, s.r.o.1\"0 \x06\x03U\x04\x0b\x0c\x19Authenticator Attestation1'0%\x06\x03U\x04\x03\x0c\x1eTrezor FIDO EE Serial 548784040Y0\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\xc1\xa33010!\x06\x0b+\x06\x01\x04\x01\x82\xe5\x1c\x01\x01\x04\x04\x12\x04\x10\xd6\xd0\xbd\xc3b\xee\xc4\xdb\xde\x8dzenJD\x870\x0c\x06\x03U\x1d\x13\x01\x01\xff\x04\x020\x000\n\x06\x08*\x86H\xce=\x04\x03\x02\x03H\x000E\x02 \x0b\xce\xc4R\xc3\n\x11'\xe5\xd5\xf5\xfc\xf5\xd6Wy\x11+\xe50\xad\x9d-TXJ\xbeE\x86\xda\x93\xc6\x02!\x00\xaf\xca=\xcf\xd8A\xb0\xadz\x9e$}\x0ff\xf4L,\x83\xf9T\xab\x95O\x896\xc15\x08\x7fX\xf1\x95"
178182
else:
179-
_FIDO_ATT_CERT = b"\x30\x82\x02\x65\x30\x82\x02\x0C\xA0\x03\x02\x01\x02\x02\x08\x2F\x1F\xAB\x58\x0B\xEB\xE5\xF0\x30\x0A\x06\x08\x2A\x86\x48\xCE\x3D\x04\x03\x02\x30\x81\x97\x31\x0B\x30\x09\x06\x03\x55\x04\x06\x13\x02\x43\x4E\x31\x10\x30\x0E\x06\x03\x55\x04\x08\x13\x07\x42\x45\x49\x4A\x49\x4E\x47\x31\x10\x30\x0E\x06\x03\x55\x04\x07\x13\x07\x48\x41\x49\x44\x49\x41\x4E\x31\x1F\x30\x1D\x06\x03\x55\x04\x0A\x13\x16\x4F\x4E\x45\x4B\x45\x59\x20\x47\x4C\x4F\x42\x41\x4C\x20\x43\x4F\x2E\x2C\x20\x4C\x54\x44\x31\x0F\x30\x0D\x06\x03\x55\x04\x0B\x13\x06\x4F\x4E\x45\x4B\x45\x59\x31\x14\x30\x12\x06\x03\x55\x04\x03\x13\x0B\x4F\x4E\x45\x4B\x45\x59\x20\x52\x4F\x4F\x54\x31\x1C\x30\x1A\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x09\x01\x16\x0D\x64\x65\x76\x40\x6F\x6E\x65\x6B\x65\x79\x2E\x73\x6F\x30\x1E\x17\x0D\x32\x34\x31\x30\x31\x38\x30\x32\x30\x37\x30\x30\x5A\x17\x0D\x32\x39\x31\x30\x31\x38\x30\x32\x30\x37\x30\x30\x5A\x30\x81\xAA\x31\x0B\x30\x09\x06\x03\x55\x04\x06\x13\x02\x43\x4E\x31\x10\x30\x0E\x06\x03\x55\x04\x08\x13\x07\x42\x45\x49\x4A\x49\x4E\x47\x31\x10\x30\x0E\x06\x03\x55\x04\x07\x13\x07\x48\x41\x49\x44\x49\x41\x4E\x31\x1F\x30\x1D\x06\x03\x55\x04\x0A\x13\x16\x4F\x4E\x45\x4B\x45\x59\x20\x47\x4C\x4F\x42\x41\x4C\x20\x43\x4F\x2E\x2C\x20\x4C\x54\x44\x31\x22\x30\x20\x06\x03\x55\x04\x0B\x13\x19\x41\x75\x74\x68\x65\x6E\x74\x69\x63\x61\x74\x6F\x72\x20\x41\x74\x74\x65\x73\x74\x61\x74\x69\x6F\x6E\x31\x14\x30\x12\x06\x03\x55\x04\x03\x13\x0B\x4F\x4E\x45\x4B\x45\x59\x20\x46\x49\x44\x4F\x31\x1C\x30\x1A\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x09\x01\x16\x0D\x64\x65\x76\x40\x6F\x6E\x65\x6B\x65\x79\x2E\x73\x6F\x30\x59\x30\x13\x06\x07\x2A\x86\x48\xCE\x3D\x02\x01\x06\x08\x2A\x86\x48\xCE\x3D\x03\x01\x07\x03\x42\x00\x04\x20\xC4\xC2\xCA\x28\x36\x66\xB2\xD7\xA0\x7C\x25\xB7\x2C\x5F\xC3\xAC\xFE\xB4\x9C\x64\xB0\x27\xC1\x84\xA3\xEA\x10\xE8\xD0\x3D\x48\xA4\xA4\x12\x6C\x3D\xBC\xC6\x1F\x9F\x54\xDA\xB5\xDE\x30\x85\xB7\x30\x9F\x28\x2A\xC7\x63\xAF\x6C\x0B\xF2\xFA\xA2\x33\x88\x0F\x75\xA3\x2D\x30\x2B\x30\x09\x06\x03\x55\x1D\x13\x04\x02\x30\x00\x30\x1E\x06\x09\x60\x86\x48\x01\x86\xF8\x42\x01\x0D\x04\x11\x16\x0F\x78\x63\x61\x20\x63\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\x30\x0A\x06\x08\x2A\x86\x48\xCE\x3D\x04\x03\x02\x03\x47\x00\x30\x44\x02\x20\x2F\x73\x6A\x80\xBC\x4F\x38\x0D\xDE\x21\xC1\x35\x40\x59\x09\x8E\x4C\x81\x9D\x3E\xA9\x6A\x51\x2F\xB3\x54\xEE\xEF\x5B\x84\xA5\xF9\x02\x20\x04\xD8\x37\x35\x88\x76\xED\x71\x02\x84\x82\x6E\x26\x3A\xB8\x8F\x82\xA4\xF8\xD4\x2E\x15\x94\xB6\xE2\x7E\xDD\xC3\x7D\xE1\xA5\x63"
183+
_FIDO_ATT_CERT = b"\x30\x82\x02\x7C\x30\x82\x02\x21\xA0\x03\x02\x01\x02\x02\x08\x2F\x1F\xAB\x58\x0B\xEB\xE5\xF0\x30\x0A\x06\x08\x2A\x86\x48\xCE\x3D\x04\x03\x02\x30\x81\x97\x31\x0B\x30\x09\x06\x03\x55\x04\x06\x13\x02\x43\x4E\x31\x10\x30\x0E\x06\x03\x55\x04\x08\x13\x07\x42\x45\x49\x4A\x49\x4E\x47\x31\x10\x30\x0E\x06\x03\x55\x04\x07\x13\x07\x48\x41\x49\x44\x49\x41\x4E\x31\x1F\x30\x1D\x06\x03\x55\x04\x0A\x13\x16\x4F\x4E\x45\x4B\x45\x59\x20\x47\x4C\x4F\x42\x41\x4C\x20\x43\x4F\x2E\x2C\x20\x4C\x54\x44\x31\x0F\x30\x0D\x06\x03\x55\x04\x0B\x13\x06\x4F\x4E\x45\x4B\x45\x59\x31\x14\x30\x12\x06\x03\x55\x04\x03\x13\x0B\x4F\x4E\x45\x4B\x45\x59\x20\x52\x4F\x4F\x54\x31\x1C\x30\x1A\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x09\x01\x16\x0D\x64\x65\x76\x40\x6F\x6E\x65\x6B\x65\x79\x2E\x73\x6F\x30\x1E\x17\x0D\x32\x34\x31\x30\x31\x38\x30\x32\x30\x37\x30\x30\x5A\x17\x0D\x34\x34\x31\x30\x31\x38\x30\x32\x30\x37\x30\x30\x5A\x30\x81\xAA\x31\x0B\x30\x09\x06\x03\x55\x04\x06\x13\x02\x43\x4E\x31\x10\x30\x0E\x06\x03\x55\x04\x08\x13\x07\x42\x45\x49\x4A\x49\x4E\x47\x31\x10\x30\x0E\x06\x03\x55\x04\x07\x13\x07\x48\x41\x49\x44\x49\x41\x4E\x31\x1F\x30\x1D\x06\x03\x55\x04\x0A\x13\x16\x4F\x4E\x45\x4B\x45\x59\x20\x47\x4C\x4F\x42\x41\x4C\x20\x43\x4F\x2E\x2C\x20\x4C\x54\x44\x31\x22\x30\x20\x06\x03\x55\x04\x0B\x13\x19\x41\x75\x74\x68\x65\x6E\x74\x69\x63\x61\x74\x6F\x72\x20\x41\x74\x74\x65\x73\x74\x61\x74\x69\x6F\x6E\x31\x14\x30\x12\x06\x03\x55\x04\x03\x13\x0B\x4F\x4E\x45\x4B\x45\x59\x20\x46\x49\x44\x4F\x31\x1C\x30\x1A\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x09\x01\x16\x0D\x64\x65\x76\x40\x6F\x6E\x65\x6B\x65\x79\x2E\x73\x6F\x30\x59\x30\x13\x06\x07\x2A\x86\x48\xCE\x3D\x02\x01\x06\x08\x2A\x86\x48\xCE\x3D\x03\x01\x07\x03\x42\x00\x04\x20\xC4\xC2\xCA\x28\x36\x66\xB2\xD7\xA0\x7C\x25\xB7\x2C\x5F\xC3\xAC\xFE\xB4\x9C\x64\xB0\x27\xC1\x84\xA3\xEA\x10\xE8\xD0\x3D\x48\xA4\xA4\x12\x6C\x3D\xBC\xC6\x1F\x9F\x54\xDA\xB5\xDE\x30\x85\xB7\x30\x9F\x28\x2A\xC7\x63\xAF\x6C\x0B\xF2\xFA\xA2\x33\x88\x0F\x75\xA3\x42\x30\x40\x30\x09\x06\x03\x55\x1D\x13\x04\x02\x30\x00\x30\x13\x06\x0B\x2B\x06\x01\x04\x01\x82\xE5\x1C\x02\x01\x01\x04\x04\x03\x02\x05\x60\x30\x1E\x06\x09\x60\x86\x48\x01\x86\xF8\x42\x01\x0D\x04\x11\x16\x0F\x78\x63\x61\x20\x63\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\x30\x0A\x06\x08\x2A\x86\x48\xCE\x3D\x04\x03\x02\x03\x49\x00\x30\x46\x02\x21\x00\xF8\xCD\xE4\x2F\x83\xBB\x8A\x97\xF4\x58\x64\x8F\x49\x46\x08\x31\x0F\x09\xBB\xB3\xBF\x54\x83\xFF\x27\x07\x25\xEB\x3D\xC4\x01\x08\x02\x21\x00\xF9\xA2\x7C\x63\x94\x2B\xB6\x66\x46\xC6\x8A\x87\x5E\x39\x23\x11\xEC\x39\x83\x10\xDD\x6F\x06\x74\x22\x9F\x26\x9D\x11\x04\x3B\x17"
180184
_BOGUS_APPID_CHROME = b"A" * 32
181185
_BOGUS_APPID_FIREFOX = b"\0" * 32
182186
_BOGUS_APPIDS = (_BOGUS_APPID_CHROME, _BOGUS_APPID_FIREFOX)
@@ -212,9 +216,6 @@
212216
_ALLOW_RESIDENT_CREDENTIALS = storage.device.get_se01_version() >= "1.1.5"
213217
_ALLOW_WINK = False
214218

215-
# The default attestation type to use in MakeCredential responses. If false, then basic attestation will be used by default.
216-
_DEFAULT_USE_SELF_ATTESTATION = True
217-
218219
# The default value of the use_sign_count flag for newly created credentials.
219220
_DEFAULT_USE_SIGN_COUNT = True
220221

@@ -620,6 +621,11 @@ async def handle_reports(usb_face: io.HID, spi_iface: io.SPI) -> None:
620621

621622
if req is None:
622623
continue
624+
if dialog_mgr.is_busy() and dialog_mgr.state is not None:
625+
active_iface = dialog_mgr.state.iface
626+
if dialog_mgr.iface is not active_iface:
627+
dialog_mgr.set_iface(active_iface)
628+
continue
623629
if dialog_mgr.is_busy() and (
624630
req.cid
625631
not in (
@@ -860,8 +866,9 @@ async def on_timeout(self) -> None:
860866
await self.on_decline()
861867

862868
async def on_cancel(self) -> None:
863-
cmd = cbor_error(self.cid, _ERR_KEEPALIVE_CANCEL)
864-
await send_cmd(cmd, self.iface)
869+
# The CTAP2 cancel response (_ERR_KEEPALIVE_CANCEL) is sent synchronously
870+
# from the _CMD_CANCEL dispatch path, because this coroutine is torn down
871+
# by DialogManager.reset() and can no longer await a send here.
865872
self.finished = True
866873

867874

@@ -1021,10 +1028,29 @@ def app_name(self) -> str:
10211028
def account_name(self) -> str | None:
10221029
return self._creds[self.page()].account_name()
10231030

1031+
def _single_line_label(self, label: str) -> str:
1032+
return label.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
1033+
1034+
def account_names(self) -> list[str]:
1035+
return [
1036+
self._single_line_label(
1037+
cred.account_name() or f"{cred.app_name()} #{i + 1}"
1038+
)
1039+
for i, cred in enumerate(self._creds)
1040+
]
1041+
10241042
def page_count(self) -> int:
10251043
return len(self._creds)
10261044

1045+
def select_account(self, index: int) -> None:
1046+
self._page = min(max(index, 0), self.page_count() - 1)
1047+
10271048
async def confirm_dialog(self) -> bool:
1049+
if self.page_count() > 1:
1050+
selected = await select_webauthn_account(None, self)
1051+
if selected is None:
1052+
return False
1053+
self.select_account(selected)
10281054
if not await confirm_webauthn(None, self):
10291055
return False
10301056
if self._user_verification:
@@ -1225,6 +1251,39 @@ async def dialog_workflow(self) -> None:
12251251
await self.state.on_decline()
12261252

12271253

1254+
def cmd_cancel(req: Cmd, dialog_mgr: DialogManager) -> Cmd | None:
1255+
if __debug__:
1256+
log.debug(__name__, "_CMD_CANCEL")
1257+
state = dialog_mgr.state
1258+
# Was a FIDO2/CTAP2 request pending? Only those get a CBOR cancel response.
1259+
is_fido2 = isinstance(state, Fido2State)
1260+
pending_cid = state.cid if is_fido2 else req.cid
1261+
pending_iface = state.iface if is_fido2 else dialog_mgr.iface
1262+
if is_fido2 and dialog_mgr.iface is not pending_iface:
1263+
dialog_mgr.set_iface(pending_iface)
1264+
return None
1265+
# Dismiss the on-screen overlay. lvgl windows are retained and are not removed
1266+
# when reset() closes the workflow coroutine, so destroy them explicitly here,
1267+
# otherwise the confirmation / PIN page stays up after the host cancels.
1268+
if state is not None:
1269+
# The register / authenticate confirmation window.
1270+
if isinstance(state, ConfirmInfo) and state.screen is not None:
1271+
state.screen.destroy()
1272+
state.screen = None
1273+
# The user-verification PIN window shown during verify_user(), if any.
1274+
from trezor.lvglui.scrs.pinscreen import InputPin
1275+
1276+
pin_wind = InputPin.get_window_if_visible()
1277+
if pin_wind is not None:
1278+
pin_wind.destroy()
1279+
dialog_mgr.result = _RESULT_CANCEL
1280+
dialog_mgr.reset()
1281+
if is_fido2:
1282+
dialog_mgr.set_iface(pending_iface)
1283+
return cbor_error(pending_cid, _ERR_KEEPALIVE_CANCEL)
1284+
return None
1285+
1286+
12281287
def dispatch_cmd_hid(req: Cmd, dialog_mgr: DialogManager) -> Cmd | None:
12291288
if req.cmd == _CMD_MSG:
12301289
try:
@@ -1304,11 +1363,7 @@ def dispatch_cmd_hid(req: Cmd, dialog_mgr: DialogManager) -> Cmd | None:
13041363
return cbor_error(req.cid, _ERR_INVALID_CMD)
13051364

13061365
elif req.cmd == _CMD_CANCEL:
1307-
if __debug__:
1308-
log.debug(__name__, "_CMD_CANCEL")
1309-
dialog_mgr.result = _RESULT_CANCEL
1310-
dialog_mgr.reset()
1311-
return None
1366+
return cmd_cancel(req, dialog_mgr)
13121367
else:
13131368
if __debug__:
13141369
log.warning(__name__, "_ERR_INVALID_CMD: %d", req.cmd)
@@ -1394,11 +1449,7 @@ def dispatch_cmd_ble(req: Cmd, dialog_mgr: DialogManager) -> Cmd | None:
13941449
log.debug(__name__, "_CMD_WINK")
13951450
return cmd_wink(req)
13961451
elif req.cmd == _CMD_CANCEL:
1397-
if __debug__:
1398-
log.debug(__name__, "_CMD_CANCEL")
1399-
dialog_mgr.result = _RESULT_CANCEL
1400-
dialog_mgr.reset()
1401-
return None
1452+
return cmd_cancel(req, dialog_mgr)
14021453
else:
14031454
if __debug__:
14041455
log.warning(__name__, "_ERR_INVALID_CMD: %d", req.cmd)
@@ -1827,16 +1878,6 @@ def cbor_make_credential_process(req: Cmd, dialog_mgr: DialogManager) -> State |
18271878
)
18281879

18291880

1830-
def use_self_attestation(rp_id_hash: bytes) -> bool:
1831-
from . import knownapps
1832-
1833-
app = knownapps.by_rp_id_hash(rp_id_hash)
1834-
if app is not None and app.use_self_attestation is not None:
1835-
return app.use_self_attestation
1836-
else:
1837-
return _DEFAULT_USE_SELF_ATTESTATION
1838-
1839-
18401881
def cbor_make_credential_sign(
18411882
client_data_hash: bytes, cred: Fido2Credential, user_verification: bool
18421883
) -> bytes:
@@ -1864,16 +1905,12 @@ def cbor_make_credential_sign(
18641905
+ extensions
18651906
)
18661907

1867-
if use_self_attestation(cred.rp_id_hash):
1868-
sig = cred.sign((authenticator_data, client_data_hash))
1869-
attestation_statement = {"alg": cred.algorithm, "sig": sig}
1870-
else:
1871-
sig = basic_attestation_sign((authenticator_data, client_data_hash))
1872-
attestation_statement = {
1873-
"alg": common.COSE_ALG_ES256,
1874-
"sig": sig,
1875-
"x5c": [_FIDO_ATT_CERT],
1876-
}
1908+
sig = basic_attestation_sign((authenticator_data, client_data_hash))
1909+
attestation_statement = {
1910+
"alg": common.COSE_ALG_ES256,
1911+
"sig": sig,
1912+
"x5c": [_FIDO_ATT_CERT],
1913+
}
18771914

18781915
# Encode the authenticatorMakeCredential response data.
18791916
return cbor.encode(

0 commit comments

Comments
 (0)