1
0
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:
Andrew Kozlik 2019-08-08 16:25:09 +02:00
parent f658958057
commit de183849b9

View 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()