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

Merge pull request #522 from trezor/andrewkozlik/fido2-mgmt

FIDO2 resident credential management
This commit is contained in:
Andrew Kozlik 2019-09-17 20:35:53 +02:00 committed by GitHub
commit 0677a8e570
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 932 additions and 94 deletions

View File

@ -17,6 +17,8 @@ for fn in sorted(glob(os.path.join(MYDIR, "messages-*.proto"))):
continue
if prefix == "Nem":
prefix = "NEM"
elif prefix == "Webauthn":
prefix = "WebAuthn"
for line in f:
line = line.strip().split(" ")
if line[0] not in ["enum", "message"]:

View File

@ -0,0 +1,56 @@
syntax = "proto2";
package hw.trezor.messages.webauthn;
// Sugar for easier handling in Java
option java_package = "com.satoshilabs.trezor.lib.protobuf";
option java_outer_classname = "TrezorMessageWebAuthn";
/**
* Request: List resident credentials
* @start
* @next WebAuthnCredentials
* @next Failure
*/
message WebAuthnListResidentCredentials {
}
/**
* Request: Add resident credential
* @start
* @next Success
* @next Failure
*/
message WebAuthnAddResidentCredential {
optional bytes credential_id = 1;
}
/**
* Request: Remove resident credential
* @start
* @next Success
* @next Failure
*/
message WebAuthnRemoveResidentCredential {
optional uint32 index = 1;
}
/**
* Response: Resident credential list
* @start
* @next end
*/
message WebAuthnCredentials {
repeated WebAuthnCredential credentials = 1;
message WebAuthnCredential {
optional uint32 index = 1;
optional bytes id = 2;
optional string rp_id = 3;
optional string rp_name = 4;
optional bytes user_id = 5;
optional string user_name = 6;
optional string user_display_name = 7;
optional uint32 creation_time = 8;
optional bool hmac_secret = 9;
}
}

View File

@ -237,4 +237,10 @@ enum MessageType {
MessageType_BinanceOrderMsg = 707 [(wire_in) = true];
MessageType_BinanceCancelMsg = 708 [(wire_in) = true];
MessageType_BinanceSignedTx = 709 [(wire_out) = true];
// WebAuthn
MessageType_WebAuthnListResidentCredentials = 800 [(wire_in) = true];
MessageType_WebAuthnCredentials = 801 [(wire_out) = true];
MessageType_WebAuthnAddResidentCredential = 802 [(wire_in) = true];
MessageType_WebAuthnRemoveResidentCredential = 803 [(wire_in) = true];
}

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")

View File

@ -180,3 +180,7 @@ if not utils.BITCOIN_ONLY:
BinanceOrderMsg = 707
BinanceCancelMsg = 708
BinanceSignedTx = 709
WebAuthnListResidentCredentials = 800
WebAuthnCredentials = 801
WebAuthnAddResidentCredential = 802
WebAuthnRemoveResidentCredential = 803

View File

@ -0,0 +1,26 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnAddResidentCredential(p.MessageType):
MESSAGE_WIRE_TYPE = 802
def __init__(
self,
credential_id: bytes = None,
) -> None:
self.credential_id = credential_id
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('credential_id', p.BytesType, 0),
}

View File

@ -0,0 +1,49 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnCredential(p.MessageType):
def __init__(
self,
index: int = None,
id: bytes = None,
rp_id: str = None,
rp_name: str = None,
user_id: bytes = None,
user_name: str = None,
user_display_name: str = None,
creation_time: int = None,
hmac_secret: bool = None,
) -> None:
self.index = index
self.id = id
self.rp_id = rp_id
self.rp_name = rp_name
self.user_id = user_id
self.user_name = user_name
self.user_display_name = user_display_name
self.creation_time = creation_time
self.hmac_secret = hmac_secret
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('index', p.UVarintType, 0),
2: ('id', p.BytesType, 0),
3: ('rp_id', p.UnicodeType, 0),
4: ('rp_name', p.UnicodeType, 0),
5: ('user_id', p.BytesType, 0),
6: ('user_name', p.UnicodeType, 0),
7: ('user_display_name', p.UnicodeType, 0),
8: ('creation_time', p.UVarintType, 0),
9: ('hmac_secret', p.BoolType, 0),
}

View File

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
from .WebAuthnCredential import WebAuthnCredential
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnCredentials(p.MessageType):
MESSAGE_WIRE_TYPE = 801
def __init__(
self,
credentials: List[WebAuthnCredential] = None,
) -> None:
self.credentials = credentials if credentials is not None else []
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('credentials', WebAuthnCredential, p.FLAG_REPEATED),
}

View File

@ -0,0 +1,14 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnListResidentCredentials(p.MessageType):
MESSAGE_WIRE_TYPE = 800

View File

@ -0,0 +1,26 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnRemoveResidentCredential(p.MessageType):
MESSAGE_WIRE_TYPE = 803
def __init__(
self,
index: int = None,
) -> None:
self.index = index
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('index', p.UVarintType, 0),
}

View File

@ -2,7 +2,7 @@ ifneq ($(V),1)
Q := @
endif
SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple Tezos
SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple Tezos WebAuthn
ifeq ($(BITCOIN_ONLY), 1)
SKIPPED_MESSAGES += Ethereum Lisk NEM Stellar

View File

@ -82,4 +82,7 @@ Use the following command to see all options:
tezos-sign-tx Sign Tezos transaction.
verify-message Verify message.
version Show version of trezorctl/trezorlib.
webauthn-add-credential Add the credential with the given ID as a resident credential.
webauthn-list-credentials List all resident credentials on the device.
webauthn-remove-credential Remove the resident credential at the given index.
wipe-device Reset device to factory defaults and remove all private data.

View File

@ -54,6 +54,7 @@ from trezorlib import (
tezos,
tools,
ui,
webauthn,
)
from trezorlib.client import TrezorClient
from trezorlib.transport import enumerate_devices, get_transport
@ -1929,6 +1930,58 @@ def binance_sign_tx(connect, address, file):
return binance.sign_tx(client, address_n, json.load(file))
#
# WebAuthn functions
#
@cli.command(help="List all resident credentials on the device.")
@click.pass_obj
def webauthn_list_credentials(connect):
creds = webauthn.list_credentials(connect())
for cred in creds:
click.echo("")
click.echo("WebAuthn credential at index {}:".format(cred.index))
if cred.rp_id is not None:
click.echo(" Relying party ID: {}".format(cred.rp_id))
if cred.rp_name is not None:
click.echo(" Relying party name: {}".format(cred.rp_name))
if cred.user_id is not None:
click.echo(" User ID: {}".format(cred.user_id.hex()))
if cred.user_name is not None:
click.echo(" User name: {}".format(cred.user_name))
if cred.user_display_name is not None:
click.echo(" User display name: {}".format(cred.user_display_name))
if cred.creation_time is not None:
click.echo(" Creation time: {}".format(cred.creation_time))
if cred.hmac_secret is not None:
click.echo(" hmac-secret enabled: {}".format(cred.hmac_secret))
click.echo(" Credential ID: {}".format(cred.id.hex()))
if not creds:
click.echo("There are no resident credentials stored on the device.")
@cli.command()
@click.argument("hex_credential_id")
@click.pass_obj
def webauthn_add_credential(connect, hex_credential_id):
"""Add the credential with the given ID as a resident credential.
HEX_CREDENTIAL_ID is the credential ID as a hexadecimal string.
"""
return webauthn.add_credential(connect(), bytes.fromhex(hex_credential_id))
@cli.command(help="Remove the resident credential at the given index.")
@click.option(
"-i", "--index", required=True, type=click.IntRange(0, 15), help="Credential index."
)
@click.pass_obj
def webauthn_remove_credential(connect, index):
return webauthn.remove_credential(connect(), index)
#
# Main
#

View File

@ -177,3 +177,7 @@ BinanceTransferMsg = 706
BinanceOrderMsg = 707
BinanceCancelMsg = 708
BinanceSignedTx = 709
WebAuthnListResidentCredentials = 800
WebAuthnCredentials = 801
WebAuthnAddResidentCredential = 802
WebAuthnRemoveResidentCredential = 803

View File

@ -0,0 +1,26 @@
# Automatically generated by pb2py
# fmt: off
from .. import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnAddResidentCredential(p.MessageType):
MESSAGE_WIRE_TYPE = 802
def __init__(
self,
credential_id: bytes = None,
) -> None:
self.credential_id = credential_id
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('credential_id', p.BytesType, 0),
}

View File

@ -0,0 +1,49 @@
# Automatically generated by pb2py
# fmt: off
from .. import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnCredential(p.MessageType):
def __init__(
self,
index: int = None,
id: bytes = None,
rp_id: str = None,
rp_name: str = None,
user_id: bytes = None,
user_name: str = None,
user_display_name: str = None,
creation_time: int = None,
hmac_secret: bool = None,
) -> None:
self.index = index
self.id = id
self.rp_id = rp_id
self.rp_name = rp_name
self.user_id = user_id
self.user_name = user_name
self.user_display_name = user_display_name
self.creation_time = creation_time
self.hmac_secret = hmac_secret
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('index', p.UVarintType, 0),
2: ('id', p.BytesType, 0),
3: ('rp_id', p.UnicodeType, 0),
4: ('rp_name', p.UnicodeType, 0),
5: ('user_id', p.BytesType, 0),
6: ('user_name', p.UnicodeType, 0),
7: ('user_display_name', p.UnicodeType, 0),
8: ('creation_time', p.UVarintType, 0),
9: ('hmac_secret', p.BoolType, 0),
}

View File

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
from .. import protobuf as p
from .WebAuthnCredential import WebAuthnCredential
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnCredentials(p.MessageType):
MESSAGE_WIRE_TYPE = 801
def __init__(
self,
credentials: List[WebAuthnCredential] = None,
) -> None:
self.credentials = credentials if credentials is not None else []
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('credentials', WebAuthnCredential, p.FLAG_REPEATED),
}

View File

@ -0,0 +1,14 @@
# Automatically generated by pb2py
# fmt: off
from .. import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnListResidentCredentials(p.MessageType):
MESSAGE_WIRE_TYPE = 800

View File

@ -0,0 +1,26 @@
# Automatically generated by pb2py
# fmt: off
from .. import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class WebAuthnRemoveResidentCredential(p.MessageType):
MESSAGE_WIRE_TYPE = 803
def __init__(
self,
index: int = None,
) -> None:
self.index = index
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('index', p.UVarintType, 0),
}

View File

@ -247,6 +247,11 @@ from .TxRequest import TxRequest
from .TxRequestDetailsType import TxRequestDetailsType
from .TxRequestSerializedType import TxRequestSerializedType
from .VerifyMessage import VerifyMessage
from .WebAuthnAddResidentCredential import WebAuthnAddResidentCredential
from .WebAuthnCredential import WebAuthnCredential
from .WebAuthnCredentials import WebAuthnCredentials
from .WebAuthnListResidentCredentials import WebAuthnListResidentCredentials
from .WebAuthnRemoveResidentCredential import WebAuthnRemoveResidentCredential
from .WipeDevice import WipeDevice
from .WordAck import WordAck
from .WordRequest import WordRequest

View File

@ -0,0 +1,34 @@
# This file is part of the Trezor project.
#
# Copyright (C) 2019 SatoshiLabs and contributors
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
from . import messages as proto
from .tools import expect
@expect(proto.WebAuthnCredentials, field="credentials")
def list_credentials(client):
return client.call(proto.WebAuthnListResidentCredentials())
@expect(proto.Success, field="message")
def add_credential(client, credential_id):
return client.call(proto.WebAuthnAddResidentCredential(credential_id))
@expect(proto.Success, field="message")
def remove_credential(client, index):
return client.call(proto.WebAuthnRemoveResidentCredential(index))

View File

@ -0,0 +1,193 @@
# This file is part of the Trezor project.
#
# Copyright (C) 2012-2019 SatoshiLabs and contributors
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import pytest
from trezorlib import webauthn
from trezorlib.exceptions import Cancelled, TrezorFailure
from ..common import MNEMONIC12
CRED1 = bytes.fromhex(
"f1d00200f8221312f7898e31ea5ec30409527c2b0bde0b9dfdd7eaab4424173f"
"bf75ab67627fff60974460d903d7d96bb9e974c169a01b2c38cf2305da304169"
"d4e28f59053a2564bebb3eb3f06c2182f1ea4a2f7cebd8f92a930a76f3b45334"
"1e3f3285a575a54bcba9cf8a088dbfe24e8e691a5926160174e03aa941828f49"
"e42b47804d"
)
CRED2 = bytes.fromhex(
"f1d00200eb3b566f4ea0a219552b2efd2c76e1ffc2e641d3bf91ec92d47a4ed4"
"d78cf42845248c4e982a503618bac0cecfb0fa91fa10821df1efe1d59ac8314e"
"b57eb7f32a1a605f91e8692daf1a679b55ab1acadfded5e0c7fd1365e2801759"
"bd3a4450dd5589586ab072da79"
)
CRED3 = bytes.fromhex(
"f1d00200ebee50034eb7affb555602eed0812b63d158b57a4188523ad064a719"
"febf477c52cfcc7ded8d7a7a83af52287ed1ecee9f74f62b7e55ad8e814c062e"
"009bb3b3391dfec79dc93053b0279eca7207358a0962865da55668b2509de773"
"8c819dbeead9997778319ac1f1c7318fd6"
)
CREDS = [
bytes.fromhex(
"f1d0020029a297837485bf2b43f2a8cc53b759a03201cf6902cf25794a375214"
"aea1357cee1e2fa9188e8fb74e5b5501767ca740cd1f0c745bb72afd"
),
bytes.fromhex(
"f1d00200ce4e44a4d5076b7d3037ca039894738183f18b0ef5edfa84b59ba4e9"
"2e9ce5fe02ddd6cd397c459636dfb45af740d268bd67610578581cc1"
),
bytes.fromhex(
"f1d00200776ac8476ac5a621c135e9ab3d5c5c1d836843eddad88f94ff044989"
"cc941f5971bd3df1a3008e12ad16a11753cdfe113d023784a29bbbe0"
),
bytes.fromhex(
"f1d00200f4bf428bc3ea21a64691bc1cfb3ae14d4ed29621777856ea81b8936e"
"51293fb8b073ab1c03fe7016b01f9e2bcac796f3c3c33515ffbf88c2"
),
bytes.fromhex(
"f1d0020055e4d0a8b06951564f71dd601287929b396013d1b1cfd1ab237a6e1d"
"b53b7f562465ed53b3fc8ba7f0b5e05498fd13badfaac358694e76f2"
),
bytes.fromhex(
"f1d00200ea2b8789416aa55dac3e8446da76a9fba3f52722329bf4820480faf1"
"ed35f2eb8577a0e3bbcecd6177d1a4c21faafc3411281ebbc2a8f100"
),
bytes.fromhex(
"f1d0020043e37bb7c62fd11b6d446da96741123b38ab9123d695537357373970"
"8d0e7aaff1ed90306da2779c23fde88c68cd37171c871af4f6c6cc08"
),
bytes.fromhex(
"f1d00200309ced39cf016b1ae284cd63e48310dd73e14f5f3af681fcfd84e121"
"6cbab4b1d00f505445b839bca1909521e4ba06209fd161bb98eb2b7d"
),
bytes.fromhex(
"f1d00200c19e3a3e2ce982419b52487e84ceb42a92bbda1c029b1bb3e832ffa7"
"0321c22edfb6163ee5ec2be03b1b291f451667a6020a720c41653745"
),
bytes.fromhex(
"f1d0020046ce52d1ed50a900687d6ba20863cc9c0cd6ee9fb72129a0f63eb598"
"dcd3cd79c449d251240e2098f4b29e4cfa28ab7b45b77f045589312d"
),
bytes.fromhex(
"f1d002004f92099262dbedc059237e3aff412204131dad9cbad98147322b00ed"
"988cd7f7b2ea2f34b0388b3efa1246477d058e4d94773a38355bc2e7"
),
bytes.fromhex(
"f1d00200ac93867d1bfbe6a6be75d943354f280e32fafce204bcee65db097666"
"e805b80d38f4f3094f334fb310d4f5cc80ccef603fdd6ba320b4eb73"
),
bytes.fromhex(
"f1d002006d5d6efbe81fe81927029727409d0f242a4da827947ec55e118cd65c"
"e6f0d1ae4c7ac578f3682806b5e0e5bfaaf7d0416960ece3fc219516"
),
bytes.fromhex(
"f1d00200e231eba4d9875231644ff1e38c83be7ce3508401b6184320a2ea3dc2"
"6092f807aba192c6fc5e7286dfc0e5ccc4738d6d8c8a1a440140b47a"
),
bytes.fromhex(
"f1d002008841311e477753cbfa4b21779d4c04e7c5532f956f2c6995b99e1392"
"1143b64b4099c98b4b1c012ef06c1bfa673f192fec193f05cf26c0cc"
),
]
@pytest.mark.skip_t1
@pytest.mark.altcoin
class TestMsgWebAuthn:
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
def test_add_remove(self, client):
# Remove index 0 should fail.
with pytest.raises(TrezorFailure):
webauthn.remove_credential(client, 0)
# List should be empty.
assert webauthn.list_credentials(client) == []
# Add valid credential #1.
webauthn.add_credential(client, CRED1)
# Check that the credential was added and parameters are correct.
creds = webauthn.list_credentials(client)
assert len(creds) == 1
assert creds[0].rp_id == "example.com"
assert creds[0].rp_name == "Example"
assert creds[0].user_id == bytes.fromhex(
"3082019330820138A0030201023082019330820138A003020102308201933082"
)
assert creds[0].user_name == "johnpsmith@example.com"
assert creds[0].user_display_name == "John P. Smith"
assert creds[0].creation_time == 3
assert creds[0].hmac_secret is True
# Add valid credential #2, which has same rpId and userId as credential #1.
webauthn.add_credential(client, CRED2)
# Check that the credential #2 replaced credential #1 and parameters are correct.
creds = webauthn.list_credentials(client)
assert len(creds) == 1
assert creds[0].rp_id == "example.com"
assert creds[0].rp_name is None
assert creds[0].user_id == bytes.fromhex(
"3082019330820138A0030201023082019330820138A003020102308201933082"
)
assert creds[0].user_name == "johnpsmith@example.com"
assert creds[0].user_display_name is None
assert creds[0].creation_time == 2
assert creds[0].hmac_secret is True
# Adding an invalid credential should appear as if user cancelled.
with pytest.raises(Cancelled):
webauthn.add_credential(client, CRED1[:-2])
# Check that the credential was not added.
creds = webauthn.list_credentials(client)
assert len(creds) == 1
# Add valid credential, which has same userId as #2, but different rpId.
webauthn.add_credential(client, CRED3)
# Check that the credential was added.
creds = webauthn.list_credentials(client)
assert len(creds) == 2
# Fill up with 14 more valid credentials.
for cred in CREDS[:14]:
webauthn.add_credential(client, cred)
# Adding one more valid credential to full storage should fail.
with pytest.raises(TrezorFailure):
webauthn.add_credential(client, CREDS[14])
# Remove index 16 should fail.
with pytest.raises(TrezorFailure):
webauthn.remove_credential(client, 16)
# Remove index 2.
webauthn.remove_credential(client, 2)
# Check that the credential was removed.
creds = webauthn.list_credentials(client)
assert len(creds) == 15
# Adding another valid credential should succeed now.
webauthn.add_credential(client, CREDS[14])
# Check that the credential was added.
creds = webauthn.list_credentials(client)
assert len(creds) == 16

View File

@ -20,6 +20,7 @@ CORE_PROTOBUF_SOURCES="\
$PROTOB/messages-ripple.proto \
$PROTOB/messages-stellar.proto \
$PROTOB/messages-tezos.proto \
$PROTOB/messages-webauthn.proto \
"
PYTHON_PROTOBUF_SOURCES=$PROTOB/*.proto
@ -72,7 +73,7 @@ do_rebuild() {
sed -i "3ifrom trezor import utils\n" "$DESTDIR"/Capability.py
sed -i "3ifrom trezor import utils\n" "$DESTDIR"/MessageType.py
sed -i "/^EthereumGetPublicKey/iif not utils.BITCOIN_ONLY:" "$DESTDIR"/MessageType.py
for altcoin in Ethereum NEM Lisk Tezos Stellar Cardano Ripple Monero DebugMonero Eos Binance; do
for altcoin in Ethereum NEM Lisk Tezos Stellar Cardano Ripple Monero DebugMonero Eos Binance WebAuthn; do
sed -i "s:^$altcoin: $altcoin:" "$DESTDIR"/Capability.py
sed -i "s:^$altcoin: $altcoin:" "$DESTDIR"/MessageType.py
done