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:
parent
f359e36273
commit
e378820f7f
21
core/src/apps/webauthn/common.py
Normal file
21
core/src/apps/webauthn/common.py
Normal 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
|
@ -4,12 +4,14 @@ from ubinascii import hexlify
|
|||||||
|
|
||||||
import storage.device
|
import storage.device
|
||||||
from trezor import log, utils
|
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.common import HARDENED, cbor, seed
|
||||||
|
from apps.webauthn import common
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
from typing import Optional
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
# Credential ID values
|
# Credential ID values
|
||||||
_CRED_ID_VERSION = b"\xf1\xd0\x02\x00"
|
_CRED_ID_VERSION = b"\xf1\xd0\x02\x00"
|
||||||
@ -17,14 +19,26 @@ _CRED_ID_MIN_LENGTH = const(33)
|
|||||||
_KEY_HANDLE_LENGTH = const(64)
|
_KEY_HANDLE_LENGTH = const(64)
|
||||||
|
|
||||||
# Credential ID keys
|
# Credential ID keys
|
||||||
_CRED_ID_RP_ID = const(0x01)
|
_CRED_ID_RP_ID = const(1)
|
||||||
_CRED_ID_RP_NAME = const(0x02)
|
_CRED_ID_RP_NAME = const(2)
|
||||||
_CRED_ID_USER_ID = const(0x03)
|
_CRED_ID_USER_ID = const(3)
|
||||||
_CRED_ID_USER_NAME = const(0x04)
|
_CRED_ID_USER_NAME = const(4)
|
||||||
_CRED_ID_USER_DISPLAY_NAME = const(0x05)
|
_CRED_ID_USER_DISPLAY_NAME = const(5)
|
||||||
_CRED_ID_CREATION_TIME = const(0x06)
|
_CRED_ID_CREATION_TIME = const(6)
|
||||||
_CRED_ID_HMAC_SECRET = const(0x07)
|
_CRED_ID_HMAC_SECRET = const(7)
|
||||||
_CRED_ID_USE_SIGN_COUNT = const(0x08)
|
_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
|
# Key paths
|
||||||
_U2F_KEY_PATH = const(0x80553246)
|
_U2F_KEY_PATH = const(0x80553246)
|
||||||
@ -39,13 +53,29 @@ class Credential:
|
|||||||
self.user_id = None # type: Optional[bytes]
|
self.user_id = None # type: Optional[bytes]
|
||||||
|
|
||||||
def app_name(self) -> str:
|
def app_name(self) -> str:
|
||||||
return ""
|
raise NotImplementedError
|
||||||
|
|
||||||
def account_name(self) -> Optional[str]:
|
def account_name(self) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def private_key(self) -> bytes:
|
def public_key(self) -> bytes:
|
||||||
return b""
|
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]:
|
def hmac_secret_key(self) -> Optional[bytes]:
|
||||||
return None
|
return None
|
||||||
@ -71,6 +101,8 @@ class Fido2Credential(Credential):
|
|||||||
self.creation_time = 0 # type: int
|
self.creation_time = 0 # type: int
|
||||||
self.hmac_secret = False # type: bool
|
self.hmac_secret = False # type: bool
|
||||||
self.use_sign_count = 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:
|
def __lt__(self, other: Credential) -> bool:
|
||||||
# Sort FIDO2 credentials newest first amongst each other.
|
# Sort FIDO2 credentials newest first amongst each other.
|
||||||
@ -83,29 +115,35 @@ class Fido2Credential(Credential):
|
|||||||
def generate_id(self) -> None:
|
def generate_id(self) -> None:
|
||||||
self.creation_time = storage.device.next_u2f_counter() or 0
|
self.creation_time = storage.device.next_u2f_counter() or 0
|
||||||
|
|
||||||
data = cbor.encode(
|
if not self.check_required_fields():
|
||||||
{
|
raise AssertionError
|
||||||
key: value
|
|
||||||
for key, value in (
|
data = {
|
||||||
(_CRED_ID_RP_ID, self.rp_id),
|
key: value
|
||||||
(_CRED_ID_RP_NAME, self.rp_name),
|
for key, value in (
|
||||||
(_CRED_ID_USER_ID, self.user_id),
|
(_CRED_ID_RP_ID, self.rp_id),
|
||||||
(_CRED_ID_USER_NAME, self.user_name),
|
(_CRED_ID_RP_NAME, self.rp_name),
|
||||||
(_CRED_ID_USER_DISPLAY_NAME, self.user_display_name),
|
(_CRED_ID_USER_ID, self.user_id),
|
||||||
(_CRED_ID_CREATION_TIME, self.creation_time),
|
(_CRED_ID_USER_NAME, self.user_name),
|
||||||
(_CRED_ID_HMAC_SECRET, self.hmac_secret),
|
(_CRED_ID_USER_DISPLAY_NAME, self.user_display_name),
|
||||||
(_CRED_ID_USE_SIGN_COUNT, self.use_sign_count),
|
(_CRED_ID_CREATION_TIME, self.creation_time),
|
||||||
)
|
(_CRED_ID_HMAC_SECRET, self.hmac_secret),
|
||||||
if value
|
(_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(
|
key = seed.derive_slip21_node_without_passphrase(
|
||||||
[b"SLIP-0022", _CRED_ID_VERSION, b"Encryption key"]
|
[b"SLIP-0022", _CRED_ID_VERSION, b"Encryption key"]
|
||||||
).key()
|
).key()
|
||||||
iv = random.bytes(12)
|
iv = random.bytes(12)
|
||||||
ctx = chacha20poly1305(key, iv)
|
ctx = chacha20poly1305(key, iv)
|
||||||
ctx.auth(self.rp_id_hash)
|
ctx.auth(self.rp_id_hash)
|
||||||
ciphertext = ctx.encrypt(data)
|
ciphertext = ctx.encrypt(cbor.encode(data))
|
||||||
tag = ctx.finish()
|
tag = ctx.finish()
|
||||||
self.id = _CRED_ID_VERSION + iv + ciphertext + tag
|
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.creation_time = data.get(_CRED_ID_CREATION_TIME, 0)
|
||||||
cred.hmac_secret = data.get(_CRED_ID_HMAC_SECRET, False)
|
cred.hmac_secret = data.get(_CRED_ID_HMAC_SECRET, False)
|
||||||
cred.use_sign_count = data.get(_CRED_ID_USE_SIGN_COUNT, 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
|
cred.id = cred_id
|
||||||
|
|
||||||
if (
|
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 not cred.check_data_types()
|
||||||
or hashlib.sha256(cred.rp_id).digest() != rp_id_hash
|
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.hmac_secret, bool)
|
||||||
and isinstance(self.use_sign_count, bool)
|
and isinstance(self.use_sign_count, bool)
|
||||||
and isinstance(self.creation_time, (int, type(None)))
|
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))
|
and isinstance(self.id, (bytes, bytearray))
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -200,13 +243,67 @@ class Fido2Credential(Credential):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def private_key(self) -> bytes:
|
def _private_key(self) -> bytes:
|
||||||
path = [HARDENED | 10022, HARDENED | int.from_bytes(self.id[:4], "big")] + [
|
path = [HARDENED | 10022, HARDENED | int.from_bytes(self.id[:4], "big")] + [
|
||||||
HARDENED | i for i in ustruct.unpack(">4L", self.id[-16:])
|
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()
|
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]:
|
def hmac_secret_key(self) -> Optional[bytes]:
|
||||||
# Returns the symmetric key for the hmac-secret extension also known as CredRandom.
|
# 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.
|
# Sort U2F credentials lexicographically amongst each other.
|
||||||
return self.id < other.id
|
return self.id < other.id
|
||||||
|
|
||||||
def private_key(self) -> bytes:
|
def _private_key(self) -> bytes:
|
||||||
if self.node is None:
|
if self.node is None:
|
||||||
return b""
|
return b""
|
||||||
return self.node.private_key()
|
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:
|
def generate_key_handle(self) -> None:
|
||||||
# derivation path is m/U2F'/r'/r'/r'/r'/r'/r'/r'/r'
|
# derivation path is m/U2F'/r'/r'/r'/r'/r'/r'/r'/r'
|
||||||
path = [HARDENED | random.uniform(0x80000000) for _ in range(0, 8)]
|
path = [HARDENED | random.uniform(0x80000000) for _ in range(0, 8)]
|
||||||
|
@ -13,6 +13,7 @@ from trezor.ui.popup import Popup
|
|||||||
from trezor.ui.text import Text
|
from trezor.ui.text import Text
|
||||||
|
|
||||||
from apps.common import cbor
|
from apps.common import cbor
|
||||||
|
from apps.webauthn import common
|
||||||
from apps.webauthn.confirm import ConfirmContent, ConfirmInfo
|
from apps.webauthn.confirm import ConfirmContent, ConfirmInfo
|
||||||
from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential
|
from apps.webauthn.credential import Credential, Fido2Credential, U2fCredential
|
||||||
from apps.webauthn.resident_credentials import (
|
from apps.webauthn.resident_credentials import (
|
||||||
@ -21,7 +22,7 @@ from apps.webauthn.resident_credentials import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if False:
|
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
|
_CID_BROADCAST = const(0xFFFFFFFF) # broadcast channel id
|
||||||
|
|
||||||
@ -122,17 +123,6 @@ _U2F_CONFIRM_TIMEOUT_MS = const(
|
|||||||
_FIDO2_CONFIRM_TIMEOUT_MS = const(60 * 1000)
|
_FIDO2_CONFIRM_TIMEOUT_MS = const(60 * 1000)
|
||||||
_POPUP_TIMEOUT_MS = const(4 * 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
|
# hid error codes
|
||||||
_ERR_NONE = const(0x00) # no error
|
_ERR_NONE = const(0x00) # no error
|
||||||
_ERR_INVALID_CMD = const(0x01) # invalid command
|
_ERR_INVALID_CMD = const(0x01) # invalid command
|
||||||
@ -181,7 +171,6 @@ _BOGUS_APPID_CHROME = b"A" * 32
|
|||||||
_BOGUS_APPID_FIREFOX = b"\0" * 32
|
_BOGUS_APPID_FIREFOX = b"\0" * 32
|
||||||
_BOGUS_APPIDS = (_BOGUS_APPID_CHROME, _BOGUS_APPID_FIREFOX)
|
_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")
|
_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
|
# authentication control byte
|
||||||
_AUTH_ENFORCE = const(0x03) # enforce user presence and sign
|
_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)
|
return Cmd(req.cid, _CMD_MSG, buf)
|
||||||
|
|
||||||
|
|
||||||
def msg_register_sign(challenge: bytes, cred: U2fCredential) -> bytes:
|
def basic_attestation_sign(data: Iterable[bytes]) -> bytes:
|
||||||
pubkey = nist256p1.publickey(cred.private_key(), False)
|
|
||||||
|
|
||||||
# hash the request data together with keyhandle and pubkey
|
|
||||||
dig = hashlib.sha256()
|
dig = hashlib.sha256()
|
||||||
dig.update(b"\x00") # uint8_t reserved;
|
for segment in data:
|
||||||
dig.update(cred.rp_id_hash) # uint8_t appId[32];
|
dig.update(segment)
|
||||||
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
|
|
||||||
sig = nist256p1.sign(_U2F_ATT_PRIV_KEY, dig.digest(), False)
|
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
|
# pack to a response
|
||||||
buf, resp = make_struct(
|
buf, resp = make_struct(
|
||||||
@ -1272,16 +1259,8 @@ def msg_authenticate_sign(
|
|||||||
ctr = cred.next_signature_counter()
|
ctr = cred.next_signature_counter()
|
||||||
ctrbuf = ustruct.pack(">L", ctr)
|
ctrbuf = ustruct.pack(">L", ctr)
|
||||||
|
|
||||||
# hash input data together with counter
|
# sign the input data together with counter
|
||||||
dig = hashlib.sha256()
|
sig = cred.sign((rp_id_hash, flags, ctrbuf, challenge))
|
||||||
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:]))
|
|
||||||
|
|
||||||
# pack to a response
|
# pack to a response
|
||||||
buf, resp = make_struct(resp_cmd_authenticate(len(sig)))
|
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 cmd_error(req.cid, _ERR_CHANNEL_BUSY)
|
||||||
return None
|
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]
|
pub_key_cred_params = param[_MAKECRED_CMD_PUB_KEY_CRED_PARAMS]
|
||||||
if _COSE_ALG_ES256 not in algorithms_from_pub_key_cred_params(
|
for alg in algorithms_from_pub_key_cred_params(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)
|
return cbor_error(req.cid, _ERR_UNSUPPORTED_ALGORITHM)
|
||||||
|
|
||||||
# Get options.
|
# Get options.
|
||||||
@ -1465,31 +1451,26 @@ def cbor_make_credential(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]:
|
|||||||
return None
|
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(
|
def cbor_make_credential_sign(
|
||||||
client_data_hash: bytes, cred: Fido2Credential, user_verification: bool
|
client_data_hash: bytes, cred: Fido2Credential, user_verification: bool
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
from apps.webauthn import knownapps
|
|
||||||
|
|
||||||
privkey = cred.private_key()
|
|
||||||
pubkey = nist256p1.publickey(privkey, False)
|
|
||||||
|
|
||||||
flags = _AUTH_FLAG_UP | _AUTH_FLAG_AT
|
flags = _AUTH_FLAG_UP | _AUTH_FLAG_AT
|
||||||
if user_verification:
|
if user_verification:
|
||||||
flags |= _AUTH_FLAG_UV
|
flags |= _AUTH_FLAG_UV
|
||||||
|
|
||||||
# Encode the authenticator data (Credential ID, its public key and extensions).
|
# 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 = (
|
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""
|
extensions = b""
|
||||||
@ -1507,27 +1488,18 @@ def cbor_make_credential_sign(
|
|||||||
+ extensions
|
+ extensions
|
||||||
)
|
)
|
||||||
|
|
||||||
app = knownapps.by_rp_id_hash(cred.rp_id_hash)
|
if use_self_attestation(cred.rp_id_hash):
|
||||||
if app is not None and app.use_self_attestation is not None:
|
sig = cred.sign((authenticator_data, client_data_hash))
|
||||||
use_self_attestation = app.use_self_attestation
|
attestation_statement = {"alg": cred.algorithm, "sig": sig}
|
||||||
else:
|
else:
|
||||||
use_self_attestation = _DEFAULT_USE_SELF_ATTESTATION
|
sig = basic_attestation_sign((authenticator_data, client_data_hash))
|
||||||
|
attestation_statement = {
|
||||||
# Compute the attestation signature of the authenticator data.
|
"alg": common.COSE_ALG_ES256,
|
||||||
if not use_self_attestation:
|
"sig": sig,
|
||||||
privkey = _U2F_ATT_PRIV_KEY
|
"x5c": _U2F_ATT_CERT,
|
||||||
|
}
|
||||||
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:]))
|
|
||||||
|
|
||||||
# Encode the authenticatorMakeCredential response data.
|
# 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(
|
return cbor.encode(
|
||||||
{
|
{
|
||||||
_MAKECRED_RESP_FMT: "packed",
|
_MAKECRED_RESP_FMT: "packed",
|
||||||
@ -1657,16 +1629,16 @@ def cbor_get_assertion_hmac_secret(
|
|||||||
cred: Credential, hmac_secret: dict
|
cred: Credential, hmac_secret: dict
|
||||||
) -> Optional[bytes]:
|
) -> Optional[bytes]:
|
||||||
key_agreement = hmac_secret[1] # The public key of platform key agreement key.
|
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.
|
# because there is currently no valid value which describes the actual key agreement algorithm.
|
||||||
if (
|
if (
|
||||||
key_agreement[_COSE_KEY_TYPE_KEY] != _COSE_KEY_TYPE_EC2
|
key_agreement[common.COSE_KEY_KTY] != common.COSE_KEYTYPE_EC2
|
||||||
or key_agreement[_COSE_CURVE_KEY] != _COSE_CURVE_P256
|
or key_agreement[common.COSE_KEY_CRV] != common.COSE_CURVE_P256
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
x = key_agreement[_COSE_X_COORD_KEY]
|
x = key_agreement[common.COSE_KEY_X]
|
||||||
y = key_agreement[_COSE_Y_COORD_KEY]
|
y = key_agreement[common.COSE_KEY_Y]
|
||||||
salt_enc = hmac_secret[2] # The encrypted salt.
|
salt_enc = hmac_secret[2] # The encrypted salt.
|
||||||
salt_auth = hmac_secret[3] # The HMAC of the encrypted salt.
|
salt_auth = hmac_secret[3] # The HMAC of the encrypted salt.
|
||||||
if (
|
if (
|
||||||
@ -1739,16 +1711,11 @@ def cbor_get_assertion_sign(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Sign the authenticator data and the client data hash.
|
# Sign the authenticator data and the client data hash.
|
||||||
dig = hashlib.sha256()
|
|
||||||
dig.update(authenticator_data)
|
|
||||||
dig.update(client_data_hash)
|
|
||||||
if user_presence:
|
if user_presence:
|
||||||
privkey = cred.private_key()
|
sig = cred.sign((authenticator_data, client_data_hash))
|
||||||
else:
|
else:
|
||||||
# Spec deviation: Use a bogus key during silent authentication.
|
# Spec deviation: Use a bogus signature during silent authentication.
|
||||||
privkey = _BOGUS_PRIV_KEY
|
sig = cred.bogus_signature()
|
||||||
sig = nist256p1.sign(privkey, dig.digest(), False)
|
|
||||||
sig = der.encode_seq((sig[1:33], sig[33:]))
|
|
||||||
|
|
||||||
# Encode the authenticatorGetAssertion response data.
|
# Encode the authenticatorGetAssertion response data.
|
||||||
response = {
|
response = {
|
||||||
@ -1796,16 +1763,16 @@ def cbor_client_pin(req: Cmd) -> Cmd:
|
|||||||
return cbor_error(req.cid, _ERR_UNSUPPORTED_OPTION)
|
return cbor_error(req.cid, _ERR_UNSUPPORTED_OPTION)
|
||||||
|
|
||||||
# Encode the public key of the authenticator key agreement key.
|
# 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
|
# 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
|
# key agreement algorithm as specified, but COSE_ALG_ECDH_ES_HKDF_256 is allegedly
|
||||||
# recommended by the latest draft of the CTAP2 spec.
|
# recommended by the latest draft of the CTAP2 spec.
|
||||||
response_data = {
|
response_data = {
|
||||||
_CLIENTPIN_RESP_KEY_AGREEMENT: {
|
_CLIENTPIN_RESP_KEY_AGREEMENT: {
|
||||||
_COSE_ALG_KEY: _COSE_ALG_ECDH_ES_HKDF_256,
|
common.COSE_KEY_ALG: common.COSE_ALG_ECDH_ES_HKDF_256,
|
||||||
_COSE_KEY_TYPE_KEY: _COSE_KEY_TYPE_EC2,
|
common.COSE_KEY_KTY: common.COSE_KEYTYPE_EC2,
|
||||||
_COSE_CURVE_KEY: _COSE_CURVE_P256,
|
common.COSE_KEY_CRV: common.COSE_CURVE_P256,
|
||||||
_COSE_X_COORD_KEY: _KEY_AGREEMENT_PUBKEY[1:33],
|
common.COSE_KEY_X: _KEY_AGREEMENT_PUBKEY[1:33],
|
||||||
_COSE_Y_COORD_KEY: _KEY_AGREEMENT_PUBKEY[33:],
|
common.COSE_KEY_Y: _KEY_AGREEMENT_PUBKEY[33:],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,9 +31,9 @@ class TestCredential(unittest.TestCase):
|
|||||||
creation_time = 2
|
creation_time = 2
|
||||||
|
|
||||||
public_key = (
|
public_key = (
|
||||||
b"0451f0d4c307bc737c90ac605c6279f7d01e451798aa7b74df550fdb43a7760c"
|
b"a501020326200121582051f0d4c307bc737c90ac605c6279f7d01e451798aa7b"
|
||||||
b"7c02b5107fef42094d00f52a9b1e90afb90e1b9decbf15a6f13d4f882de857e2"
|
b"74df550fdb43a7760c7c22582002b5107fef42094d00f52a9b1e90afb90e1b9d"
|
||||||
b"f4"
|
b"ecbf15a6f13d4f882de857e2f4"
|
||||||
)
|
)
|
||||||
|
|
||||||
cred_random = (
|
cred_random = (
|
||||||
@ -57,9 +57,7 @@ class TestCredential(unittest.TestCase):
|
|||||||
|
|
||||||
# Check credential keys.
|
# Check credential keys.
|
||||||
self.assertEqual(hexlify(cred.hmac_secret_key()), cred_random)
|
self.assertEqual(hexlify(cred.hmac_secret_key()), cred_random)
|
||||||
|
self.assertEqual(hexlify(cred.public_key()), public_key)
|
||||||
cred_public_key = nist256p1.publickey(cred.private_key(), False)
|
|
||||||
self.assertEqual(hexlify(cred_public_key), public_key)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Loading…
Reference in New Issue
Block a user