1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-23 06:48:16 +00:00

core: Implement credential management.

This commit is contained in:
Andrew Kozlik 2019-09-05 12:27:49 +02:00
parent 3e2ae5e469
commit b89a9dc590
7 changed files with 283 additions and 92 deletions

View File

@ -10,37 +10,48 @@ _RESIDENT_CREDENTIAL_START_KEY = const(1)
_MAX_RESIDENT_CREDENTIALS = const(16)
def get_resident_credentials(rp_id_hash: Optional[bytes]) -> List[Credential]:
def get_resident_credentials(rp_id_hash: Optional[bytes] = None) -> List[Credential]:
creds = [] # type: List[Credential]
for i in range(
_RESIDENT_CREDENTIAL_START_KEY,
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
):
stored_cred_data = common._get(common._APP_FIDO2, i)
if stored_cred_data is None:
continue
stored_rp_id_hash = stored_cred_data[:32]
stored_cred_id = stored_cred_data[32:]
if rp_id_hash is not None and rp_id_hash != stored_rp_id_hash:
# Stored credential is not for this RP ID.
continue
stored_cred = Fido2Credential.from_cred_id(stored_cred_id, stored_rp_id_hash)
if stored_cred is not None:
creds.append(stored_cred)
for i in range(_MAX_RESIDENT_CREDENTIALS):
cred = get_resident_credential(i, rp_id_hash)
if cred is not None:
creds.append(cred)
return creds
def get_resident_credential(
index: int, rp_id_hash: Optional[bytes] = None
) -> Optional[Credential]:
if not (0 <= index < _MAX_RESIDENT_CREDENTIALS):
return None
stored_cred_data = common.get(
common.APP_FIDO2, index + _RESIDENT_CREDENTIAL_START_KEY
)
if stored_cred_data is None:
return None
stored_rp_id_hash = stored_cred_data[:32]
stored_cred_id = stored_cred_data[32:]
if rp_id_hash is not None and rp_id_hash != stored_rp_id_hash:
# Stored credential is not for this RP ID.
return None
stored_cred = Fido2Credential.from_cred_id(stored_cred_id, stored_rp_id_hash)
if stored_cred is None:
return None
stored_cred.index = index
return stored_cred
def store_resident_credential(cred: Fido2Credential) -> bool:
slot = None
for i in range(
_RESIDENT_CREDENTIAL_START_KEY,
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
):
stored_cred_data = common._get(common._APP_FIDO2, i)
for i in range(_MAX_RESIDENT_CREDENTIALS):
stored_cred_data = common.get(
common.APP_FIDO2, i + _RESIDENT_CREDENTIAL_START_KEY
)
if stored_cred_data is None:
if slot is None:
slot = i
@ -66,13 +77,21 @@ def store_resident_credential(cred: Fido2Credential) -> bool:
if slot is None:
return False
common._set(common._APP_FIDO2, slot, cred.rp_id_hash + cred.id)
common.set(
common.APP_FIDO2,
slot + _RESIDENT_CREDENTIAL_START_KEY,
cred.rp_id_hash + cred.id,
)
return True
def erase_resident_credentials() -> None:
for i in range(
_RESIDENT_CREDENTIAL_START_KEY,
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
):
common._delete(common._APP_FIDO2, i)
for i in range(_MAX_RESIDENT_CREDENTIALS):
common.delete(common.APP_FIDO2, i + _RESIDENT_CREDENTIAL_START_KEY)
def erase_resident_credential(index: int) -> bool:
if not (0 <= index < _MAX_RESIDENT_CREDENTIALS):
return False
common.delete(common.APP_FIDO2, index + _RESIDENT_CREDENTIAL_START_KEY)
return True

View File

@ -3,11 +3,12 @@ import ustruct
import utime
from micropython import const
from trezor import config, io, log, loop, ui, utils, workflow
from trezor import config, io, log, loop, ui, utils, wire, workflow
from trezor.crypto import aes, der, hashlib, hmac, random
from trezor.crypto.curve import nist256p1
from trezor.messages import MessageType
from trezor.ui.confirm import CONFIRMED, Confirm, ConfirmPageable, Pageable
from trezor.ui.text import Text, text_center_trim_left, text_center_trim_right
from trezor.ui.text import Text
from apps.common import cbor, storage
from apps.common.storage.webauthn import (
@ -15,6 +16,7 @@ from apps.common.storage.webauthn import (
get_resident_credentials,
store_resident_credential,
)
from apps.webauthn.confirm import ConfirmContent, ConfirmInfo
from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential
if __debug__:
@ -464,6 +466,19 @@ def send_cmd_sync(cmd: Cmd, iface: io.HID) -> None:
def boot(iface: io.HID) -> None:
wire.add(
MessageType.WebAuthnListResidentCredentials,
__name__,
"list_resident_credentials",
)
wire.add(
MessageType.WebAuthnAddResidentCredential, __name__, "add_resident_credential"
)
wire.add(
MessageType.WebAuthnRemoveResidentCredential,
__name__,
"remove_resident_credential",
)
loop.schedule(handle_reports(iface))
@ -526,64 +541,6 @@ async def confirm(*args: Any, **kwargs: Any) -> bool:
return await dialog is CONFIRMED
class ConfirmInfo:
def __init__(self) -> None:
self.app_icon = None # type: Optional[bytes]
def get_header(self) -> Optional[str]:
return None
def app_name(self) -> str:
raise NotImplementedError
def account_name(self) -> Optional[str]:
return None
def load_icon(self, rp_id_hash: bytes) -> None:
from trezor import res
from apps.webauthn import knownapps
try:
namepart = knownapps.knownapps[rp_id_hash].lower().replace(" ", "_")
icon = res.load("apps/webauthn/res/icon_%s.toif" % namepart)
except Exception as e:
icon = res.load("apps/webauthn/res/icon_webauthn.toif")
if __debug__:
log.exception(__name__, e)
self.app_icon = icon
class ConfirmContent(ui.Component):
def __init__(self, info: ConfirmInfo) -> None:
self.info = info
self.repaint = True
def on_render(self) -> None:
if self.repaint:
header = self.info.get_header()
if header is None or self.info.app_icon is None:
return
ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN)
ui.display.image((ui.WIDTH - 64) // 2, 48, self.info.app_icon)
app_name = self.info.app_name()
account_name = self.info.account_name()
# Dummy requests usually have some text as both app_name and account_name,
# in that case show the text only once.
if account_name is not None:
if app_name != account_name:
text_center_trim_left(ui.WIDTH // 2, 140, app_name)
text_center_trim_right(ui.WIDTH // 2, 172, account_name)
else:
text_center_trim_right(ui.WIDTH // 2, 156, account_name)
else:
text_center_trim_left(ui.WIDTH // 2, 156, app_name)
self.repaint = False
class State:
def __init__(self, cid: int, iface: io.HID) -> None:
self.cid = cid

View File

@ -0,0 +1,54 @@
from trezor import ui, wire
from trezor.messages.Success import Success
from trezor.messages.WebAuthnAddResidentCredential import WebAuthnAddResidentCredential
from trezor.ui.text import Text
from apps.common.confirm import require_confirm
from apps.common.storage.webauthn import store_resident_credential
from apps.webauthn.confirm import ConfirmContent, ConfirmInfo
from apps.webauthn.credential import Fido2Credential
if False:
from typing import Optional
class ConfirmAddCredential(ConfirmInfo):
def __init__(self, cred: Fido2Credential):
self._cred = cred
self.load_icon(cred.rp_id_hash)
def get_header(self) -> str:
return "Import credential"
def app_name(self) -> str:
return self._cred.app_name()
def account_name(self) -> Optional[str]:
return self._cred.account_name()
async def add_resident_credential(
ctx: wire.Context, msg: WebAuthnAddResidentCredential
) -> Success:
if not msg.credential_id:
raise wire.ProcessError("Missing credential ID parameter.")
cred = Fido2Credential.from_cred_id(msg.credential_id, None)
if cred is None:
text = Text("Import credential", ui.ICON_WRONG, ui.RED)
text.normal(
"The credential you are",
"trying to import does",
"not belong to this",
"authenticator.",
)
await require_confirm(ctx, text, confirm=None)
raise wire.ActionCancelled("Cancelled")
content = ConfirmContent(ConfirmAddCredential(cred))
await require_confirm(ctx, content)
if store_resident_credential(cred):
return Success(message="Credential added")
else:
raise wire.ProcessError("Internal credential storage is full.")

View File

@ -0,0 +1,62 @@
from trezor import log, ui
from trezor.ui.text import text_center_trim_left, text_center_trim_right
if False:
from typing import Optional
class ConfirmInfo:
def __init__(self) -> None:
self.app_icon = None # type: Optional[bytes]
def get_header(self) -> str:
raise NotImplementedError
def app_name(self) -> str:
raise NotImplementedError
def account_name(self) -> Optional[str]:
return None
def load_icon(self, rp_id_hash: bytes) -> None:
from trezor import res
from apps.webauthn import knownapps
try:
namepart = knownapps.knownapps[rp_id_hash].lower().replace(" ", "_")
icon = res.load("apps/webauthn/res/icon_%s.toif" % namepart)
except Exception as e:
icon = res.load("apps/webauthn/res/icon_webauthn.toif")
if __debug__:
log.exception(__name__, e)
self.app_icon = icon
class ConfirmContent(ui.Component):
def __init__(self, info: ConfirmInfo) -> None:
self.info = info
self.repaint = True
def on_render(self) -> None:
if self.repaint:
header = self.info.get_header()
ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN)
if self.info.app_icon is not None:
ui.display.image((ui.WIDTH - 64) // 2, 48, self.info.app_icon)
app_name = self.info.app_name()
account_name = self.info.account_name()
# Dummy requests usually have some text as both app_name and account_name,
# in that case show the text only once.
if account_name is not None:
if app_name != account_name:
text_center_trim_left(ui.WIDTH // 2, 140, app_name)
text_center_trim_right(ui.WIDTH // 2, 172, account_name)
else:
text_center_trim_right(ui.WIDTH // 2, 156, account_name)
else:
text_center_trim_left(ui.WIDTH // 2, 156, app_name)
self.repaint = False

View File

@ -30,6 +30,7 @@ _U2F_KEY_PATH = const(0x80553246)
class Credential:
def __init__(self) -> None:
self.index = None # type Optional[int]
self.id = b"" # type: bytes
self.rp_id = "" # type: str
self.rp_id_hash = b"" # type: bytes
@ -104,7 +105,9 @@ class Fido2Credential(Credential):
self.id = _CRED_ID_VERSION + iv + ciphertext + tag
@staticmethod
def from_cred_id(cred_id: bytes, rp_id_hash: bytes) -> Optional["Fido2Credential"]:
def from_cred_id(
cred_id: bytes, rp_id_hash: Optional[bytes]
) -> Optional["Fido2Credential"]:
if len(cred_id) < _CRED_ID_MIN_LENGTH or cred_id[0:4] != _CRED_ID_VERSION:
return None
@ -114,6 +117,16 @@ class Fido2Credential(Credential):
iv = cred_id[4:16]
ciphertext = cred_id[16:-16]
tag = cred_id[-16:]
if rp_id_hash is None:
ctx = chacha20poly1305(key, iv)
data = ctx.decrypt(ciphertext)
try:
rp_id = cbor.decode(data)[_CRED_ID_RP_ID]
except Exception:
return None
rp_id_hash = hashlib.sha256(rp_id).digest()
ctx = chacha20poly1305(key, iv)
ctx.auth(rp_id_hash)
data = ctx.decrypt(ciphertext)

View File

@ -0,0 +1,38 @@
from trezor import wire
from trezor.messages.WebAuthnCredential import WebAuthnCredential
from trezor.messages.WebAuthnCredentials import WebAuthnCredentials
from trezor.messages.WebAuthnListResidentCredentials import (
WebAuthnListResidentCredentials,
)
from trezor.ui.text import Text
from apps.common.confirm import require_confirm
from apps.common.storage.webauthn import get_resident_credentials
async def list_resident_credentials(
ctx: wire.Context, msg: WebAuthnListResidentCredentials
) -> WebAuthnCredentials:
text = Text("List credentials")
text.normal(
"Do you want to export",
"information about the",
"resident credentials",
"stored on this device?",
)
await require_confirm(ctx, text)
creds = [
WebAuthnCredential(
index=cred.index,
id=cred.id,
rp_id=cred.rp_id,
rp_name=cred.rp_name,
user_id=cred.user_id,
user_name=cred.user_name,
user_display_name=cred.user_display_name,
creation_time=cred._creation_time,
hmac_secret=cred.hmac_secret,
)
for cred in get_resident_credentials()
]
return WebAuthnCredentials(creds)

View File

@ -0,0 +1,48 @@
from trezor import wire
from trezor.messages.Success import Success
from trezor.messages.WebAuthnRemoveResidentCredential import (
WebAuthnRemoveResidentCredential,
)
from apps.common.confirm import require_confirm
from apps.common.storage.webauthn import (
erase_resident_credential,
get_resident_credential,
)
from apps.webauthn.confirm import ConfirmContent, ConfirmInfo
from apps.webauthn.credential import Fido2Credential
if False:
from typing import Optional
class ConfirmRemoveCredential(ConfirmInfo):
def __init__(self, cred: Fido2Credential):
self._cred = cred
self.load_icon(cred.rp_id_hash)
def get_header(self) -> str:
return "Remove credential"
def app_name(self) -> str:
return self._cred.app_name()
def account_name(self) -> Optional[str]:
return self._cred.account_name()
async def remove_resident_credential(
ctx: wire.Context, msg: WebAuthnRemoveResidentCredential
) -> Success:
if msg.index is None:
raise wire.ProcessError("Missing credential index parameter.")
cred = get_resident_credential(msg.index)
if cred is None:
raise wire.ProcessError("Invalid credential index.")
content = ConfirmContent(ConfirmRemoveCredential(cred))
await require_confirm(ctx, content)
erase_resident_credential(msg.index)
return Success(message="Credential removed")