mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-22 15:38:11 +00:00
Merge pull request #522 from trezor/andrewkozlik/fido2-mgmt
FIDO2 resident credential management
This commit is contained in:
commit
0677a8e570
@ -17,6 +17,8 @@ for fn in sorted(glob(os.path.join(MYDIR, "messages-*.proto"))):
|
|||||||
continue
|
continue
|
||||||
if prefix == "Nem":
|
if prefix == "Nem":
|
||||||
prefix = "NEM"
|
prefix = "NEM"
|
||||||
|
elif prefix == "Webauthn":
|
||||||
|
prefix = "WebAuthn"
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip().split(" ")
|
line = line.strip().split(" ")
|
||||||
if line[0] not in ["enum", "message"]:
|
if line[0] not in ["enum", "message"]:
|
||||||
|
56
common/protob/messages-webauthn.proto
Normal file
56
common/protob/messages-webauthn.proto
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -237,4 +237,10 @@ enum MessageType {
|
|||||||
MessageType_BinanceOrderMsg = 707 [(wire_in) = true];
|
MessageType_BinanceOrderMsg = 707 [(wire_in) = true];
|
||||||
MessageType_BinanceCancelMsg = 708 [(wire_in) = true];
|
MessageType_BinanceCancelMsg = 708 [(wire_in) = true];
|
||||||
MessageType_BinanceSignedTx = 709 [(wire_out) = 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];
|
||||||
}
|
}
|
||||||
|
@ -10,37 +10,48 @@ _RESIDENT_CREDENTIAL_START_KEY = const(1)
|
|||||||
_MAX_RESIDENT_CREDENTIALS = const(16)
|
_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]
|
creds = [] # type: List[Credential]
|
||||||
for i in range(
|
for i in range(_MAX_RESIDENT_CREDENTIALS):
|
||||||
_RESIDENT_CREDENTIAL_START_KEY,
|
cred = get_resident_credential(i, rp_id_hash)
|
||||||
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
|
if cred is not None:
|
||||||
):
|
creds.append(cred)
|
||||||
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)
|
|
||||||
|
|
||||||
return creds
|
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:
|
def store_resident_credential(cred: Fido2Credential) -> bool:
|
||||||
slot = None
|
slot = None
|
||||||
for i in range(
|
for i in range(_MAX_RESIDENT_CREDENTIALS):
|
||||||
_RESIDENT_CREDENTIAL_START_KEY,
|
stored_cred_data = common.get(
|
||||||
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
|
common.APP_FIDO2, i + _RESIDENT_CREDENTIAL_START_KEY
|
||||||
):
|
)
|
||||||
stored_cred_data = common._get(common._APP_FIDO2, i)
|
|
||||||
if stored_cred_data is None:
|
if stored_cred_data is None:
|
||||||
if slot is None:
|
if slot is None:
|
||||||
slot = i
|
slot = i
|
||||||
@ -66,13 +77,21 @@ def store_resident_credential(cred: Fido2Credential) -> bool:
|
|||||||
if slot is None:
|
if slot is None:
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
def erase_resident_credentials() -> None:
|
def erase_resident_credentials() -> None:
|
||||||
for i in range(
|
for i in range(_MAX_RESIDENT_CREDENTIALS):
|
||||||
_RESIDENT_CREDENTIAL_START_KEY,
|
common.delete(common.APP_FIDO2, i + _RESIDENT_CREDENTIAL_START_KEY)
|
||||||
_RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS,
|
|
||||||
):
|
|
||||||
common._delete(common._APP_FIDO2, i)
|
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
|
||||||
|
@ -3,11 +3,12 @@ import ustruct
|
|||||||
import utime
|
import utime
|
||||||
from micropython import const
|
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 import aes, der, hashlib, hmac, random
|
||||||
from trezor.crypto.curve import nist256p1
|
from trezor.crypto.curve import nist256p1
|
||||||
|
from trezor.messages import MessageType
|
||||||
from trezor.ui.confirm import CONFIRMED, Confirm, ConfirmPageable, Pageable
|
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 import cbor, storage
|
||||||
from apps.common.storage.webauthn import (
|
from apps.common.storage.webauthn import (
|
||||||
@ -15,6 +16,7 @@ from apps.common.storage.webauthn import (
|
|||||||
get_resident_credentials,
|
get_resident_credentials,
|
||||||
store_resident_credential,
|
store_resident_credential,
|
||||||
)
|
)
|
||||||
|
from apps.webauthn.confirm import ConfirmContent, ConfirmInfo
|
||||||
from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential
|
from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
@ -464,6 +466,19 @@ def send_cmd_sync(cmd: Cmd, iface: io.HID) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def boot(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))
|
loop.schedule(handle_reports(iface))
|
||||||
|
|
||||||
|
|
||||||
@ -526,64 +541,6 @@ async def confirm(*args: Any, **kwargs: Any) -> bool:
|
|||||||
return await dialog is CONFIRMED
|
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:
|
class State:
|
||||||
def __init__(self, cid: int, iface: io.HID) -> None:
|
def __init__(self, cid: int, iface: io.HID) -> None:
|
||||||
self.cid = cid
|
self.cid = cid
|
||||||
|
54
core/src/apps/webauthn/add_resident_credential.py
Normal file
54
core/src/apps/webauthn/add_resident_credential.py
Normal 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.")
|
62
core/src/apps/webauthn/confirm.py
Normal file
62
core/src/apps/webauthn/confirm.py
Normal 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
|
@ -30,6 +30,7 @@ _U2F_KEY_PATH = const(0x80553246)
|
|||||||
|
|
||||||
class Credential:
|
class Credential:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
self.index = None # type Optional[int]
|
||||||
self.id = b"" # type: bytes
|
self.id = b"" # type: bytes
|
||||||
self.rp_id = "" # type: str
|
self.rp_id = "" # type: str
|
||||||
self.rp_id_hash = b"" # type: bytes
|
self.rp_id_hash = b"" # type: bytes
|
||||||
@ -104,7 +105,9 @@ class Fido2Credential(Credential):
|
|||||||
self.id = _CRED_ID_VERSION + iv + ciphertext + tag
|
self.id = _CRED_ID_VERSION + iv + ciphertext + tag
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
if len(cred_id) < _CRED_ID_MIN_LENGTH or cred_id[0:4] != _CRED_ID_VERSION:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -114,6 +117,16 @@ class Fido2Credential(Credential):
|
|||||||
iv = cred_id[4:16]
|
iv = cred_id[4:16]
|
||||||
ciphertext = cred_id[16:-16]
|
ciphertext = cred_id[16:-16]
|
||||||
tag = cred_id[-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 = chacha20poly1305(key, iv)
|
||||||
ctx.auth(rp_id_hash)
|
ctx.auth(rp_id_hash)
|
||||||
data = ctx.decrypt(ciphertext)
|
data = ctx.decrypt(ciphertext)
|
||||||
|
38
core/src/apps/webauthn/list_resident_credentials.py
Normal file
38
core/src/apps/webauthn/list_resident_credentials.py
Normal 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)
|
48
core/src/apps/webauthn/remove_resident_credential.py
Normal file
48
core/src/apps/webauthn/remove_resident_credential.py
Normal 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")
|
@ -180,3 +180,7 @@ if not utils.BITCOIN_ONLY:
|
|||||||
BinanceOrderMsg = 707
|
BinanceOrderMsg = 707
|
||||||
BinanceCancelMsg = 708
|
BinanceCancelMsg = 708
|
||||||
BinanceSignedTx = 709
|
BinanceSignedTx = 709
|
||||||
|
WebAuthnListResidentCredentials = 800
|
||||||
|
WebAuthnCredentials = 801
|
||||||
|
WebAuthnAddResidentCredential = 802
|
||||||
|
WebAuthnRemoveResidentCredential = 803
|
||||||
|
26
core/src/trezor/messages/WebAuthnAddResidentCredential.py
Normal file
26
core/src/trezor/messages/WebAuthnAddResidentCredential.py
Normal 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),
|
||||||
|
}
|
49
core/src/trezor/messages/WebAuthnCredential.py
Normal file
49
core/src/trezor/messages/WebAuthnCredential.py
Normal 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),
|
||||||
|
}
|
28
core/src/trezor/messages/WebAuthnCredentials.py
Normal file
28
core/src/trezor/messages/WebAuthnCredentials.py
Normal 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),
|
||||||
|
}
|
14
core/src/trezor/messages/WebAuthnListResidentCredentials.py
Normal file
14
core/src/trezor/messages/WebAuthnListResidentCredentials.py
Normal 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
|
26
core/src/trezor/messages/WebAuthnRemoveResidentCredential.py
Normal file
26
core/src/trezor/messages/WebAuthnRemoveResidentCredential.py
Normal 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),
|
||||||
|
}
|
@ -2,7 +2,7 @@ ifneq ($(V),1)
|
|||||||
Q := @
|
Q := @
|
||||||
endif
|
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)
|
ifeq ($(BITCOIN_ONLY), 1)
|
||||||
SKIPPED_MESSAGES += Ethereum Lisk NEM Stellar
|
SKIPPED_MESSAGES += Ethereum Lisk NEM Stellar
|
||||||
|
@ -82,4 +82,7 @@ Use the following command to see all options:
|
|||||||
tezos-sign-tx Sign Tezos transaction.
|
tezos-sign-tx Sign Tezos transaction.
|
||||||
verify-message Verify message.
|
verify-message Verify message.
|
||||||
version Show version of trezorctl/trezorlib.
|
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.
|
wipe-device Reset device to factory defaults and remove all private data.
|
||||||
|
@ -54,6 +54,7 @@ from trezorlib import (
|
|||||||
tezos,
|
tezos,
|
||||||
tools,
|
tools,
|
||||||
ui,
|
ui,
|
||||||
|
webauthn,
|
||||||
)
|
)
|
||||||
from trezorlib.client import TrezorClient
|
from trezorlib.client import TrezorClient
|
||||||
from trezorlib.transport import enumerate_devices, get_transport
|
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))
|
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
|
# Main
|
||||||
#
|
#
|
||||||
|
@ -177,3 +177,7 @@ BinanceTransferMsg = 706
|
|||||||
BinanceOrderMsg = 707
|
BinanceOrderMsg = 707
|
||||||
BinanceCancelMsg = 708
|
BinanceCancelMsg = 708
|
||||||
BinanceSignedTx = 709
|
BinanceSignedTx = 709
|
||||||
|
WebAuthnListResidentCredentials = 800
|
||||||
|
WebAuthnCredentials = 801
|
||||||
|
WebAuthnAddResidentCredential = 802
|
||||||
|
WebAuthnRemoveResidentCredential = 803
|
||||||
|
@ -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),
|
||||||
|
}
|
49
python/src/trezorlib/messages/WebAuthnCredential.py
Normal file
49
python/src/trezorlib/messages/WebAuthnCredential.py
Normal 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),
|
||||||
|
}
|
28
python/src/trezorlib/messages/WebAuthnCredentials.py
Normal file
28
python/src/trezorlib/messages/WebAuthnCredentials.py
Normal 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),
|
||||||
|
}
|
@ -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
|
@ -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),
|
||||||
|
}
|
@ -247,6 +247,11 @@ from .TxRequest import TxRequest
|
|||||||
from .TxRequestDetailsType import TxRequestDetailsType
|
from .TxRequestDetailsType import TxRequestDetailsType
|
||||||
from .TxRequestSerializedType import TxRequestSerializedType
|
from .TxRequestSerializedType import TxRequestSerializedType
|
||||||
from .VerifyMessage import VerifyMessage
|
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 .WipeDevice import WipeDevice
|
||||||
from .WordAck import WordAck
|
from .WordAck import WordAck
|
||||||
from .WordRequest import WordRequest
|
from .WordRequest import WordRequest
|
||||||
|
34
python/src/trezorlib/webauthn.py
Normal file
34
python/src/trezorlib/webauthn.py
Normal 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))
|
193
tests/device_tests/test_msg_webauthn.py
Normal file
193
tests/device_tests/test_msg_webauthn.py
Normal 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
|
@ -20,6 +20,7 @@ CORE_PROTOBUF_SOURCES="\
|
|||||||
$PROTOB/messages-ripple.proto \
|
$PROTOB/messages-ripple.proto \
|
||||||
$PROTOB/messages-stellar.proto \
|
$PROTOB/messages-stellar.proto \
|
||||||
$PROTOB/messages-tezos.proto \
|
$PROTOB/messages-tezos.proto \
|
||||||
|
$PROTOB/messages-webauthn.proto \
|
||||||
"
|
"
|
||||||
|
|
||||||
PYTHON_PROTOBUF_SOURCES=$PROTOB/*.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"/Capability.py
|
||||||
sed -i "3ifrom trezor import utils\n" "$DESTDIR"/MessageType.py
|
sed -i "3ifrom trezor import utils\n" "$DESTDIR"/MessageType.py
|
||||||
sed -i "/^EthereumGetPublicKey/iif not utils.BITCOIN_ONLY:" "$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"/Capability.py
|
||||||
sed -i "s:^$altcoin: $altcoin:" "$DESTDIR"/MessageType.py
|
sed -i "s:^$altcoin: $altcoin:" "$DESTDIR"/MessageType.py
|
||||||
done
|
done
|
||||||
|
Loading…
Reference in New Issue
Block a user