mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-15 19:08:07 +00:00
core/webauthn: Implement SLIP-0022 FIDO2 credential ID format.
This commit is contained in:
parent
f658958057
commit
de183849b9
196
core/src/apps/webauthn/credential.py
Normal file
196
core/src/apps/webauthn/credential.py
Normal file
@ -0,0 +1,196 @@
|
||||
import ustruct
|
||||
from micropython import const
|
||||
from ubinascii import hexlify
|
||||
|
||||
from trezor import log, utils
|
||||
from trezor.crypto import bip32, chacha20poly1305, hashlib, hmac, random
|
||||
|
||||
from apps.common import HARDENED, cbor, seed, storage
|
||||
|
||||
if False:
|
||||
from typing import Optional
|
||||
|
||||
# Credential ID values
|
||||
_CRED_ID_VERSION = b"\xf1\xd0\x02\x00"
|
||||
_CRED_ID_MIN_LENGTH = const(33)
|
||||
|
||||
# Credential ID keys
|
||||
_CRED_ID_RP_ID = const(0x01)
|
||||
_CRED_ID_RP_NAME = const(0x02)
|
||||
_CRED_ID_USER_ID = const(0x03)
|
||||
_CRED_ID_USER_NAME = const(0x04)
|
||||
_CRED_ID_USER_DISPLAY_NAME = const(0x05)
|
||||
_CRED_ID_CREATION_TIME = const(0x06)
|
||||
_CRED_ID_HMAC_SECRET = const(0x07)
|
||||
|
||||
|
||||
class Credential:
|
||||
def __init__(self) -> None:
|
||||
self.id = b"" # type: bytes
|
||||
self.rp_id = "" # type: str
|
||||
self.rp_id_hash = b"" # type: bytes
|
||||
self.user_id = None # type: Optional[bytes]
|
||||
|
||||
def app_name(self) -> str:
|
||||
return ""
|
||||
|
||||
def account_name(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
def private_key(self) -> bytes:
|
||||
return b""
|
||||
|
||||
def hmac_secret_key(self) -> Optional[bytes]:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes, rp_id_hash: bytes) -> Optional["Credential"]:
|
||||
cred = Fido2Credential.from_cred_id(
|
||||
data, rp_id_hash
|
||||
) # type: Optional[Credential]
|
||||
if cred is None:
|
||||
cred = U2fCredential.from_key_handle(data, rp_id_hash)
|
||||
return cred
|
||||
|
||||
|
||||
# SLIP-0022: FIDO2 credential ID format for HD wallets
|
||||
class Fido2Credential(Credential):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.rp_name = None # type: Optional[str]
|
||||
self.user_name = None # type: Optional[str]
|
||||
self.user_display_name = None # type: Optional[str]
|
||||
self._creation_time = 0 # type: int
|
||||
self.hmac_secret = False # type: bool
|
||||
|
||||
def __lt__(self, other: Credential) -> bool:
|
||||
# Sort FIDO2 credentials newest first amongst each other.
|
||||
if isinstance(other, Fido2Credential):
|
||||
return self._creation_time > other._creation_time
|
||||
|
||||
# Sort FIDO2 credentials before U2F credentials.
|
||||
return True
|
||||
|
||||
def generate_id(self) -> None:
|
||||
self._creation_time = storage.device.next_u2f_counter() or 0
|
||||
|
||||
data = cbor.encode(
|
||||
{
|
||||
key: value
|
||||
for key, value in (
|
||||
(_CRED_ID_RP_ID, self.rp_id),
|
||||
(_CRED_ID_RP_NAME, self.rp_name),
|
||||
(_CRED_ID_USER_ID, self.user_id),
|
||||
(_CRED_ID_USER_NAME, self.user_name),
|
||||
(_CRED_ID_USER_DISPLAY_NAME, self.user_display_name),
|
||||
(_CRED_ID_CREATION_TIME, self._creation_time),
|
||||
(_CRED_ID_HMAC_SECRET, self.hmac_secret),
|
||||
)
|
||||
if value
|
||||
}
|
||||
)
|
||||
key = seed.derive_slip21_node_without_passphrase(
|
||||
[b"SLIP-0022", _CRED_ID_VERSION, b"Encryption key"]
|
||||
).key()
|
||||
iv = random.bytes(12)
|
||||
ctx = chacha20poly1305(key, iv)
|
||||
ctx.auth(self.rp_id_hash)
|
||||
ciphertext = ctx.encrypt(data)
|
||||
tag = ctx.finish()
|
||||
self.id = _CRED_ID_VERSION + iv + ciphertext + tag
|
||||
|
||||
@staticmethod
|
||||
def from_cred_id(cred_id: bytes, rp_id_hash: bytes) -> Optional["Fido2Credential"]:
|
||||
if len(cred_id) < _CRED_ID_MIN_LENGTH or cred_id[0:4] != _CRED_ID_VERSION:
|
||||
return None
|
||||
|
||||
key = seed.derive_slip21_node_without_passphrase(
|
||||
[b"SLIP-0022", cred_id[0:4], b"Encryption key"]
|
||||
).key()
|
||||
iv = cred_id[4:16]
|
||||
ciphertext = cred_id[16:-16]
|
||||
tag = cred_id[-16:]
|
||||
ctx = chacha20poly1305(key, iv)
|
||||
ctx.auth(rp_id_hash)
|
||||
data = ctx.decrypt(ciphertext)
|
||||
if not utils.consteq(ctx.finish(), tag):
|
||||
return None
|
||||
|
||||
try:
|
||||
data = cbor.decode(data)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
|
||||
cred = Fido2Credential()
|
||||
cred.rp_id = data.get(_CRED_ID_RP_ID, None)
|
||||
cred.rp_id_hash = rp_id_hash
|
||||
cred.rp_name = data.get(_CRED_ID_RP_NAME, None)
|
||||
cred.user_id = data.get(_CRED_ID_USER_ID, None)
|
||||
cred.user_name = data.get(_CRED_ID_USER_NAME, None)
|
||||
cred.user_display_name = data.get(_CRED_ID_USER_DISPLAY_NAME, None)
|
||||
cred._creation_time = data.get(_CRED_ID_CREATION_TIME, 0)
|
||||
cred.hmac_secret = data.get(_CRED_ID_HMAC_SECRET, False)
|
||||
cred.id = cred_id
|
||||
|
||||
if (
|
||||
not cred.check_required_fields()
|
||||
or not cred.check_data_types()
|
||||
or hashlib.sha256(cred.rp_id).digest() != rp_id_hash
|
||||
):
|
||||
return None
|
||||
|
||||
return cred
|
||||
|
||||
def check_required_fields(self) -> bool:
|
||||
return (
|
||||
self.rp_id is not None
|
||||
and self.user_id is not None
|
||||
and self._creation_time is not None
|
||||
)
|
||||
|
||||
def check_data_types(self) -> bool:
|
||||
return (
|
||||
isinstance(self.rp_id, str)
|
||||
and isinstance(self.rp_name, (str, type(None)))
|
||||
and isinstance(self.user_id, (bytes, bytearray))
|
||||
and isinstance(self.user_name, (str, type(None)))
|
||||
and isinstance(self.user_display_name, (str, type(None)))
|
||||
and isinstance(self.hmac_secret, bool)
|
||||
and isinstance(self._creation_time, (int, type(None)))
|
||||
and isinstance(self.id, (bytes, bytearray))
|
||||
)
|
||||
|
||||
def app_name(self) -> str:
|
||||
return self.rp_id
|
||||
|
||||
def account_name(self) -> Optional[str]:
|
||||
if self.user_name:
|
||||
return self.user_name
|
||||
elif self.user_display_name:
|
||||
return self.user_display_name
|
||||
elif self.user_id:
|
||||
return hexlify(self.user_id).decode()
|
||||
else:
|
||||
return None
|
||||
|
||||
def private_key(self) -> bytes:
|
||||
path = [HARDENED | 10022, HARDENED | int.from_bytes(self.id[:4], "big")] + [
|
||||
HARDENED | i for i in ustruct.unpack(">4L", self.id[-16:])
|
||||
]
|
||||
node = seed.derive_node_without_passphrase(path, "nist256p1")
|
||||
return node.private_key()
|
||||
|
||||
def hmac_secret_key(self) -> Optional[bytes]:
|
||||
# Returns the symmetric key for the hmac-secret extension also known as CredRandom.
|
||||
|
||||
if not self.hmac_secret:
|
||||
return None
|
||||
|
||||
node = seed.derive_slip21_node_without_passphrase(
|
||||
[b"SLIP-0022", self.id[0:4], b"hmac-secret", self.id]
|
||||
)
|
||||
|
||||
return node.key()
|
Loading…
Reference in New Issue
Block a user