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