1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-10 23:40:58 +00:00

core/webauthn: Implement support for Ed25519 signatures in FIDO2.

This commit is contained in:
Andrew Kozlik 2020-02-27 18:58:21 +01:00 committed by Andrew Kozlik
parent f359e36273
commit e378820f7f
4 changed files with 225 additions and 133 deletions

View File

@ -0,0 +1,21 @@
from micropython import const
# COSE Key Object Parameter labels
COSE_KEY_KTY = const(1) # identification of the key type
COSE_KEY_ALG = const(3) # algorithm to be used with the key
COSE_KEY_CRV = const(-1) # elliptic curve identifier
COSE_KEY_X = const(-2) # x coordinate of the public key or encoded public key
COSE_KEY_Y = const(-3) # y coordinate of the public key
# COSE Algorithm values
COSE_ALG_ES256 = const(-7) # ECDSA with SHA-256
COSE_ALG_EDDSA = const(-8) # EdDSA
COSE_ALG_ECDH_ES_HKDF_256 = const(-25) # ephemeral-static ECDH with HKDF SHA-256
# COSE Key Type values
COSE_KEYTYPE_OKP = const(1) # octet key pair
COSE_KEYTYPE_EC2 = const(2) # elliptic curve keys with x- and y-coordinate pair
# COSE Elliptic Curve values
COSE_CURVE_P256 = const(1) # NIST P-256 curve
COSE_CURVE_ED25519 = const(6) # Ed25519 curve

View File

@ -4,12 +4,14 @@ from ubinascii import hexlify
import storage.device
from trezor import log, utils
from trezor.crypto import bip32, chacha20poly1305, hashlib, hmac, random
from trezor.crypto import bip32, chacha20poly1305, der, hashlib, hmac, random
from trezor.crypto.curve import ed25519, nist256p1
from apps.common import HARDENED, cbor, seed
from apps.webauthn import common
if False:
from typing import Optional
from typing import Iterable, Optional
# Credential ID values
_CRED_ID_VERSION = b"\xf1\xd0\x02\x00"
@ -17,14 +19,26 @@ _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)
_CRED_ID_RP_ID = const(1)
_CRED_ID_RP_NAME = const(2)
_CRED_ID_USER_ID = const(3)
_CRED_ID_USER_NAME = const(4)
_CRED_ID_USER_DISPLAY_NAME = const(5)
_CRED_ID_CREATION_TIME = const(6)
_CRED_ID_HMAC_SECRET = const(7)
_CRED_ID_USE_SIGN_COUNT = const(8)
_CRED_ID_ALGORITHM = const(9)
_CRED_ID_CURVE = const(10)
# Defaults
_DEFAULT_ALGORITHM = common.COSE_ALG_ES256
_DEFAULT_CURVE = common.COSE_CURVE_P256
# Curves
_CURVE_NAME = {
common.COSE_CURVE_ED25519: "ed25519",
common.COSE_CURVE_P256: "nist256p1",
}
# Key paths
_U2F_KEY_PATH = const(0x80553246)
@ -39,13 +53,29 @@ class Credential:
self.user_id = None # type: Optional[bytes]
def app_name(self) -> str:
return ""
raise NotImplementedError
def account_name(self) -> Optional[str]:
return None
def private_key(self) -> bytes:
return b""
def public_key(self) -> bytes:
raise NotImplementedError
def _private_key(self) -> bytes:
raise NotImplementedError
def sign(self, data: Iterable[bytes]) -> bytes:
raise NotImplementedError
def _u2f_sign(self, data: Iterable[bytes]) -> bytes:
dig = hashlib.sha256()
for segment in data:
dig.update(segment)
sig = nist256p1.sign(self._private_key(), dig.digest(), False)
return der.encode_seq((sig[1:33], sig[33:]))
def bogus_signature(self) -> bytes:
raise NotImplementedError
def hmac_secret_key(self) -> Optional[bytes]:
return None
@ -71,6 +101,8 @@ class Fido2Credential(Credential):
self.creation_time = 0 # type: int
self.hmac_secret = False # type: bool
self.use_sign_count = False # type: bool
self.algorithm = _DEFAULT_ALGORITHM # type: int
self.curve = _DEFAULT_CURVE # type: int
def __lt__(self, other: Credential) -> bool:
# Sort FIDO2 credentials newest first amongst each other.
@ -83,29 +115,35 @@ class Fido2Credential(Credential):
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
}
)
if not self.check_required_fields():
raise AssertionError
data = {
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
}
if self.algorithm != _DEFAULT_ALGORITHM or self.curve != _DEFAULT_CURVE:
data[_CRED_ID_ALGORITHM] = self.algorithm
data[_CRED_ID_CURVE] = self.curve
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)
ciphertext = ctx.encrypt(cbor.encode(data))
tag = ctx.finish()
self.id = _CRED_ID_VERSION + iv + ciphertext + tag
@ -156,10 +194,13 @@ class Fido2Credential(Credential):
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.algorithm = data.get(_CRED_ID_ALGORITHM, _DEFAULT_ALGORITHM)
cred.curve = data.get(_CRED_ID_CURVE, _DEFAULT_CURVE)
cred.id = cred_id
if (
not cred.check_required_fields()
(_CRED_ID_ALGORITHM in data) != (_CRED_ID_CURVE in data)
or not cred.check_required_fields()
or not cred.check_data_types()
or hashlib.sha256(cred.rp_id).digest() != rp_id_hash
):
@ -184,6 +225,8 @@ class Fido2Credential(Credential):
and isinstance(self.hmac_secret, bool)
and isinstance(self.use_sign_count, bool)
and isinstance(self.creation_time, (int, type(None)))
and isinstance(self.algorithm, (int, type(None)))
and isinstance(self.curve, (int, type(None)))
and isinstance(self.id, (bytes, bytearray))
)
@ -200,13 +243,67 @@ class Fido2Credential(Credential):
else:
return None
def private_key(self) -> bytes:
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")
node = seed.derive_node_without_passphrase(path, _CURVE_NAME[self.curve])
return node.private_key()
def public_key(self) -> bytes:
if self.curve == common.COSE_CURVE_P256:
pubkey = nist256p1.publickey(self._private_key(), False)
return cbor.encode(
{
common.COSE_KEY_ALG: self.algorithm,
common.COSE_KEY_KTY: common.COSE_KEYTYPE_EC2,
common.COSE_KEY_CRV: self.curve,
common.COSE_KEY_X: pubkey[1:33],
common.COSE_KEY_Y: pubkey[33:],
}
)
elif self.curve == common.COSE_CURVE_ED25519:
pubkey = ed25519.publickey(self._private_key())
return cbor.encode(
{
common.COSE_KEY_ALG: self.algorithm,
common.COSE_KEY_KTY: common.COSE_KEYTYPE_OKP,
common.COSE_KEY_CRV: self.curve,
common.COSE_KEY_X: pubkey,
}
)
raise TypeError
def sign(self, data: Iterable[bytes]) -> bytes:
if (self.algorithm, self.curve) == (
common.COSE_ALG_ES256,
common.COSE_CURVE_P256,
):
return self._u2f_sign(data)
elif (self.algorithm, self.curve) == (
common.COSE_ALG_EDDSA,
common.COSE_CURVE_ED25519,
):
return ed25519.sign(
self._private_key(), b"".join(bytes(segment) for segment in data)
)
raise TypeError
def bogus_signature(self) -> bytes:
if (self.algorithm, self.curve) == (
common.COSE_ALG_ES256,
common.COSE_CURVE_P256,
):
return der.encode_seq((b"\x0a" * 32, b"\x0a" * 32))
elif (self.algorithm, self.curve) == (
common.COSE_ALG_EDDSA,
common.COSE_CURVE_ED25519,
):
return b"\x0a" * 64
raise TypeError
def hmac_secret_key(self) -> Optional[bytes]:
# Returns the symmetric key for the hmac-secret extension also known as CredRandom.
@ -238,11 +335,20 @@ class U2fCredential(Credential):
# Sort U2F credentials lexicographically amongst each other.
return self.id < other.id
def private_key(self) -> bytes:
def _private_key(self) -> bytes:
if self.node is None:
return b""
return self.node.private_key()
def public_key(self) -> bytes:
return nist256p1.publickey(self._private_key(), False)
def sign(self, data: Iterable[bytes]) -> bytes:
return self._u2f_sign(data)
def bogus_signature(self) -> bytes:
return der.encode_seq((b"\x0a" * 32, b"\x0a" * 32))
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)]

View File

@ -13,6 +13,7 @@ from trezor.ui.popup import Popup
from trezor.ui.text import Text
from apps.common import cbor
from apps.webauthn import common
from apps.webauthn.confirm import ConfirmContent, ConfirmInfo
from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential
from apps.webauthn.resident_credentials import (
@ -21,7 +22,7 @@ from apps.webauthn.resident_credentials import (
)
if False:
from typing import Any, Coroutine, List, Optional, Tuple
from typing import Any, Coroutine, Iterable, List, Optional, Tuple
_CID_BROADCAST = const(0xFFFFFFFF) # broadcast channel id
@ -122,17 +123,6 @@ _U2F_CONFIRM_TIMEOUT_MS = const(
_FIDO2_CONFIRM_TIMEOUT_MS = const(60 * 1000)
_POPUP_TIMEOUT_MS = const(4 * 1000)
# CBOR object signing and encryption algorithms and keys
_COSE_ALG_KEY = const(3)
_COSE_ALG_ES256 = const(-7) # ECDSA P-256 with SHA-256
_COSE_ALG_ECDH_ES_HKDF_256 = const(-25) # Ephemeral-static ECDH with HKDF SHA-256
_COSE_KEY_TYPE_KEY = const(1)
_COSE_KEY_TYPE_EC2 = const(2) # elliptic curve keys with x- and y-coordinate pair
_COSE_CURVE_KEY = const(-1) # elliptic curve identifier
_COSE_CURVE_P256 = const(1) # P-256 curve
_COSE_X_COORD_KEY = const(-2) # x coordinate of the public key
_COSE_Y_COORD_KEY = const(-3) # y coordinate of the public key
# hid error codes
_ERR_NONE = const(0x00) # no error
_ERR_INVALID_CMD = const(0x01) # invalid command
@ -181,7 +171,6 @@ _BOGUS_APPID_CHROME = b"A" * 32
_BOGUS_APPID_FIREFOX = b"\0" * 32
_BOGUS_APPIDS = (_BOGUS_APPID_CHROME, _BOGUS_APPID_FIREFOX)
_AAGUID = b"\xd6\xd0\xbd\xc3b\xee\xc4\xdb\xde\x8dzenJD\x87" # First 16 bytes of SHA-256("TREZOR 2")
_BOGUS_PRIV_KEY = b"\xAA" * 32
# authentication control byte
_AUTH_ENFORCE = const(0x03) # enforce user presence and sign
@ -1167,20 +1156,18 @@ def msg_register(req: Msg, dialog_mgr: DialogManager) -> Cmd:
return Cmd(req.cid, _CMD_MSG, buf)
def msg_register_sign(challenge: bytes, cred: U2fCredential) -> bytes:
pubkey = nist256p1.publickey(cred.private_key(), False)
# hash the request data together with keyhandle and pubkey
def basic_attestation_sign(data: Iterable[bytes]) -> bytes:
dig = hashlib.sha256()
dig.update(b"\x00") # uint8_t reserved;
dig.update(cred.rp_id_hash) # uint8_t appId[32];
dig.update(challenge) # uint8_t chal[32];
dig.update(cred.id) # uint8_t keyHandle[64];
dig.update(pubkey) # uint8_t pubKey[65];
# sign the digest and convert to der
for segment in data:
dig.update(segment)
sig = nist256p1.sign(_U2F_ATT_PRIV_KEY, dig.digest(), False)
sig = der.encode_seq((sig[1:33], sig[33:]))
return der.encode_seq((sig[1:33], sig[33:]))
def msg_register_sign(challenge: bytes, cred: U2fCredential) -> bytes:
pubkey = cred.public_key()
sig = basic_attestation_sign((b"\x00", cred.rp_id_hash, challenge, cred.id, pubkey))
# pack to a response
buf, resp = make_struct(
@ -1272,16 +1259,8 @@ def msg_authenticate_sign(
ctr = cred.next_signature_counter()
ctrbuf = ustruct.pack(">L", ctr)
# hash input data together with counter
dig = hashlib.sha256()
dig.update(rp_id_hash) # uint8_t appId[32];
dig.update(flags) # uint8_t flags;
dig.update(ctrbuf) # uint8_t ctr[4];
dig.update(challenge) # uint8_t chal[32];
# sign the digest and convert to der
sig = nist256p1.sign(cred.private_key(), dig.digest(), False)
sig = der.encode_seq((sig[1:33], sig[33:]))
# sign the input data together with counter
sig = cred.sign((rp_id_hash, flags, ctrbuf, challenge))
# pack to a response
buf, resp = make_struct(resp_cmd_authenticate(len(sig)))
@ -1384,11 +1363,18 @@ def cbor_make_credential(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]:
return cmd_error(req.cid, _ERR_CHANNEL_BUSY)
return None
# Check that the relying party supports ECDSA P-256 with SHA-256. We don't support any other algorithms.
# Check that the relying party supports ECDSA with SHA-256 or EdDSA. We don't support any other algorithms.
pub_key_cred_params = param[_MAKECRED_CMD_PUB_KEY_CRED_PARAMS]
if _COSE_ALG_ES256 not in algorithms_from_pub_key_cred_params(
pub_key_cred_params
):
for alg in algorithms_from_pub_key_cred_params(pub_key_cred_params):
if alg == common.COSE_ALG_ES256:
cred.algorithm = alg
cred.curve = common.COSE_CURVE_P256
break
elif alg == common.COSE_ALG_EDDSA:
cred.algorithm = alg
cred.curve = common.COSE_CURVE_ED25519
break
else:
return cbor_error(req.cid, _ERR_UNSUPPORTED_ALGORITHM)
# Get options.
@ -1465,31 +1451,26 @@ def cbor_make_credential(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]:
return None
def use_self_attestation(rp_id_hash: bytes) -> bool:
from apps.webauthn import knownapps
app = knownapps.by_rp_id_hash(rp_id_hash)
if app is not None and app.use_self_attestation is not None:
return app.use_self_attestation
else:
return _DEFAULT_USE_SELF_ATTESTATION
def cbor_make_credential_sign(
client_data_hash: bytes, cred: Fido2Credential, user_verification: bool
) -> bytes:
from apps.webauthn import knownapps
privkey = cred.private_key()
pubkey = nist256p1.publickey(privkey, False)
flags = _AUTH_FLAG_UP | _AUTH_FLAG_AT
if user_verification:
flags |= _AUTH_FLAG_UV
# Encode the authenticator data (Credential ID, its public key and extensions).
credential_pub_key = cbor.encode(
{
_COSE_ALG_KEY: _COSE_ALG_ES256,
_COSE_KEY_TYPE_KEY: _COSE_KEY_TYPE_EC2,
_COSE_CURVE_KEY: _COSE_CURVE_P256,
_COSE_X_COORD_KEY: pubkey[1:33],
_COSE_Y_COORD_KEY: pubkey[33:],
}
)
att_cred_data = (
_AAGUID + len(cred.id).to_bytes(2, "big") + cred.id + credential_pub_key
_AAGUID + len(cred.id).to_bytes(2, "big") + cred.id + cred.public_key()
)
extensions = b""
@ -1507,27 +1488,18 @@ def cbor_make_credential_sign(
+ extensions
)
app = knownapps.by_rp_id_hash(cred.rp_id_hash)
if app is not None and app.use_self_attestation is not None:
use_self_attestation = app.use_self_attestation
if use_self_attestation(cred.rp_id_hash):
sig = cred.sign((authenticator_data, client_data_hash))
attestation_statement = {"alg": cred.algorithm, "sig": sig}
else:
use_self_attestation = _DEFAULT_USE_SELF_ATTESTATION
# Compute the attestation signature of the authenticator data.
if not use_self_attestation:
privkey = _U2F_ATT_PRIV_KEY
dig = hashlib.sha256()
dig.update(authenticator_data)
dig.update(client_data_hash)
sig = nist256p1.sign(privkey, dig.digest(), False)
sig = der.encode_seq((sig[1:33], sig[33:]))
sig = basic_attestation_sign((authenticator_data, client_data_hash))
attestation_statement = {
"alg": common.COSE_ALG_ES256,
"sig": sig,
"x5c": _U2F_ATT_CERT,
}
# Encode the authenticatorMakeCredential response data.
attestation_statement = {"alg": _COSE_ALG_ES256, "sig": sig}
if not use_self_attestation:
attestation_statement["x5c"] = [_U2F_ATT_CERT]
return cbor.encode(
{
_MAKECRED_RESP_FMT: "packed",
@ -1657,16 +1629,16 @@ def cbor_get_assertion_hmac_secret(
cred: Credential, hmac_secret: dict
) -> Optional[bytes]:
key_agreement = hmac_secret[1] # The public key of platform key agreement key.
# NOTE: We should check the key_agreement[_COSE_ALG_KEY] here, but to avoid compatibility issues we don't,
# NOTE: We should check the key_agreement[COSE_KEY_ALG] here, but to avoid compatibility issues we don't,
# because there is currently no valid value which describes the actual key agreement algorithm.
if (
key_agreement[_COSE_KEY_TYPE_KEY] != _COSE_KEY_TYPE_EC2
or key_agreement[_COSE_CURVE_KEY] != _COSE_CURVE_P256
key_agreement[common.COSE_KEY_KTY] != common.COSE_KEYTYPE_EC2
or key_agreement[common.COSE_KEY_CRV] != common.COSE_CURVE_P256
):
return None
x = key_agreement[_COSE_X_COORD_KEY]
y = key_agreement[_COSE_Y_COORD_KEY]
x = key_agreement[common.COSE_KEY_X]
y = key_agreement[common.COSE_KEY_Y]
salt_enc = hmac_secret[2] # The encrypted salt.
salt_auth = hmac_secret[3] # The HMAC of the encrypted salt.
if (
@ -1739,16 +1711,11 @@ def cbor_get_assertion_sign(
)
# Sign the authenticator data and the client data hash.
dig = hashlib.sha256()
dig.update(authenticator_data)
dig.update(client_data_hash)
if user_presence:
privkey = cred.private_key()
sig = cred.sign((authenticator_data, client_data_hash))
else:
# Spec deviation: Use a bogus key during silent authentication.
privkey = _BOGUS_PRIV_KEY
sig = nist256p1.sign(privkey, dig.digest(), False)
sig = der.encode_seq((sig[1:33], sig[33:]))
# Spec deviation: Use a bogus signature during silent authentication.
sig = cred.bogus_signature()
# Encode the authenticatorGetAssertion response data.
response = {
@ -1796,16 +1763,16 @@ def cbor_client_pin(req: Cmd) -> Cmd:
return cbor_error(req.cid, _ERR_UNSUPPORTED_OPTION)
# Encode the public key of the authenticator key agreement key.
# NOTE: There is currently no valid value for _COSE_ALG_KEY which describes the actual
# key agreement algorithm as specified, but _COSE_ALG_ECDH_ES_HKDF_256 is allegedly
# NOTE: There is currently no valid value for COSE_KEY_ALG which describes the actual
# key agreement algorithm as specified, but COSE_ALG_ECDH_ES_HKDF_256 is allegedly
# recommended by the latest draft of the CTAP2 spec.
response_data = {
_CLIENTPIN_RESP_KEY_AGREEMENT: {
_COSE_ALG_KEY: _COSE_ALG_ECDH_ES_HKDF_256,
_COSE_KEY_TYPE_KEY: _COSE_KEY_TYPE_EC2,
_COSE_CURVE_KEY: _COSE_CURVE_P256,
_COSE_X_COORD_KEY: _KEY_AGREEMENT_PUBKEY[1:33],
_COSE_Y_COORD_KEY: _KEY_AGREEMENT_PUBKEY[33:],
common.COSE_KEY_ALG: common.COSE_ALG_ECDH_ES_HKDF_256,
common.COSE_KEY_KTY: common.COSE_KEYTYPE_EC2,
common.COSE_KEY_CRV: common.COSE_CURVE_P256,
common.COSE_KEY_X: _KEY_AGREEMENT_PUBKEY[1:33],
common.COSE_KEY_Y: _KEY_AGREEMENT_PUBKEY[33:],
}
}

View File

@ -31,9 +31,9 @@ class TestCredential(unittest.TestCase):
creation_time = 2
public_key = (
b"0451f0d4c307bc737c90ac605c6279f7d01e451798aa7b74df550fdb43a7760c"
b"7c02b5107fef42094d00f52a9b1e90afb90e1b9decbf15a6f13d4f882de857e2"
b"f4"
b"a501020326200121582051f0d4c307bc737c90ac605c6279f7d01e451798aa7b"
b"74df550fdb43a7760c7c22582002b5107fef42094d00f52a9b1e90afb90e1b9d"
b"ecbf15a6f13d4f882de857e2f4"
)
cred_random = (
@ -57,9 +57,7 @@ class TestCredential(unittest.TestCase):
# Check credential keys.
self.assertEqual(hexlify(cred.hmac_secret_key()), cred_random)
cred_public_key = nist256p1.publickey(cred.private_key(), False)
self.assertEqual(hexlify(cred_public_key), public_key)
self.assertEqual(hexlify(cred.public_key()), public_key)
if __name__ == '__main__':
unittest.main()