You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/apps/webauthn/credential.py

332 lines
11 KiB

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
from apps.common.storage import device as storage_device
if False:
from typing import Optional
# Credential ID values
_CRED_ID_VERSION = b"\xf1\xd0\x02\x00"
_CRED_ID_MIN_LENGTH = const(33)
_KEY_HANDLE_LENGTH = const(64)
# 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)
_CRED_ID_USE_SIGN_COUNT = const(0x08)
# Key paths
_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
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
def next_signature_counter(self) -> int:
return storage_device.next_u2f_counter() or 0
@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
self.use_sign_count = 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),
(_CRED_ID_USE_SIGN_COUNT, self.use_sign_count),
)
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: Optional[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:]
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)
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.use_sign_count = data.get(_CRED_ID_USE_SIGN_COUNT, 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.use_sign_count, 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()
def next_signature_counter(self) -> int:
if not self.use_sign_count:
return 0
return super().next_signature_counter()
class U2fCredential(Credential):
def __init__(self) -> None:
super().__init__()
self.node = None # type: Optional[bip32.HDNode]
def __lt__(self, other: "Credential") -> bool:
# Sort U2F credentials after FIDO2 credentials.
if isinstance(other, Fido2Credential):
return False
# Sort U2F credentials lexicographically amongst each other.
return self.id < other.id
def private_key(self) -> bytes:
if self.node is None:
return b""
return self.node.private_key()
def generate_key_handle(self) -> None:
# derivation path is m/U2F'/r'/r'/r'/r'/r'/r'/r'/r'
path = [HARDENED | random.uniform(0x80000000) for _ in range(0, 8)]
nodepath = [_U2F_KEY_PATH] + path
# prepare signing key from random path, compute decompressed public key
self.node = seed.derive_node_without_passphrase(nodepath, "nist256p1")
# first half of keyhandle is keypath
keypath = ustruct.pack("<8L", *path)
# second half of keyhandle is a hmac of rp_id_hash and keypath
mac = hmac.Hmac(self.node.private_key(), self.rp_id_hash, hashlib.sha256)
mac.update(keypath)
self.id = keypath + mac.digest()
def app_name(self) -> str:
from apps.webauthn.knownapps import knownapps
app_name = knownapps.get(self.rp_id_hash, {}).get(
"label", None
) # type: Optional[str]
if app_name is None:
app_name = "%s...%s" % (
hexlify(self.rp_id_hash[:4]).decode(),
hexlify(self.rp_id_hash[-4:]).decode(),
)
return app_name
@staticmethod
def from_key_handle(
key_handle: bytes, rp_id_hash: bytes
) -> Optional["U2fCredential"]:
if len(key_handle) != _KEY_HANDLE_LENGTH:
return None
# check the keyHandle and generate the signing key
node = U2fCredential._node_from_key_handle(rp_id_hash, key_handle, "<8L")
if node is None:
# prior to firmware version 2.0.8, keypath was serialized in a
# big-endian manner, instead of little endian, like in trezor-mcu.
# try to parse it as big-endian now and check the HMAC.
node = U2fCredential._node_from_key_handle(rp_id_hash, key_handle, ">8L")
if node is None:
# specific error logged in msg_authenticate_genkey
return None
cred = U2fCredential()
cred.id = key_handle
cred.rp_id_hash = rp_id_hash
cred.node = node
return cred
@staticmethod
def _node_from_key_handle(
rp_id_hash: bytes, keyhandle: bytes, pathformat: str
) -> Optional[bip32.HDNode]:
# unpack the keypath from the first half of keyhandle
keypath = keyhandle[:32]
path = ustruct.unpack(pathformat, keypath)
# check high bit for hardened keys
for i in path:
if not i & HARDENED:
if __debug__:
log.warning(__name__, "invalid key path")
return None
# derive the signing key
nodepath = [_U2F_KEY_PATH] + list(path)
node = seed.derive_node_without_passphrase(nodepath, "nist256p1")
# second half of keyhandle is a hmac of rp_id_hash and keypath
mac = hmac.Hmac(node.private_key(), rp_id_hash, hashlib.sha256)
mac.update(keypath)
# verify the hmac
if not utils.consteq(mac.digest(), keyhandle[32:]):
if __debug__:
log.warning(__name__, "invalid key handle")
return None
return node