feat (core): Implement FIDO2 credential management. WIP [skip ci]

andrewkozlik/fido2-credmgmt
Andrew Kozlik 3 years ago
parent 97974c8d5b
commit 1a32615246

@ -277,6 +277,17 @@ class Fido2Credential(Credential):
else:
return None
def user(self) -> dict:
return {
key: value
for key, value in (
("id", self.user_id),
("name", self.user_name),
("displayName", self.user_display_name),
)
if value is not 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:])

@ -18,7 +18,13 @@ from apps.common import cbor
from . import common
from .confirm import ConfirmContent, ConfirmInfo
from .credential import CRED_ID_MAX_LENGTH, Credential, Fido2Credential, U2fCredential
from .resident_credentials import find_by_rp_id_hash, store_resident_credential
from .resident_credentials import (
MAX_RESIDENT_CREDENTIALS,
find_all,
find_by_cred_id,
find_by_rp_id_hash,
store_resident_credential,
)
if False:
from typing import (
@ -70,6 +76,7 @@ _CBOR_GET_INFO = const(0x04) # report AAGUID and device capabilities
_CBOR_CLIENT_PIN = const(0x06) # PIN and pinToken management
_CBOR_RESET = const(0x07) # factory reset, invalidating all generated credentials
_CBOR_GET_NEXT_ASSERTION = const(0x08) # obtain the next per-credential signature
_CBOR_CREDENTIAL_MANAGEMENT = const(0x0A) # manage resident credentials
# CBOR MakeCredential command parameter keys
_MAKECRED_CMD_CLIENT_DATA_HASH = const(0x01) # bytes, required
@ -114,11 +121,58 @@ _GETINFO_RESP_MAX_CRED_ID_LEN = const(0x08) # int, optional
# CBOR ClientPin command parameter keys
_CLIENTPIN_CMD_PIN_PROTOCOL = const(0x01) # unsigned int, required
_CLIENTPIN_CMD_SUBCOMMAND = const(0x02) # unsigned int, required
_CLIENTPIN_CMD_KEY_AGREEMENT = const(0x03) # platform key agreement key
_CLIENTPIN_CMD_PERMISSIONS = const(0x09) # unsigned int
_CLIENTPIN_CMD_PERMISSIONS_RPID = const(0x0A) # unsigned int
_CLIENTPIN_SUBCMD_GET_KEY_AGREEMENT = const(0x02)
_CLIENTPIN_SUBCMD_GET_TOKEN_USING_UV = const(0x06)
# CBOR ClientPin response member keys
_CLIENTPIN_RESP_KEY_AGREEMENT = const(0x01) # COSE_Key, optional
# pinUvAuthToken permission flags
_PERM_FLAG_MC = const(0x01) # MakeCredential
_PERM_FLAG_GA = const(0x02) # GetAssertion
_PERM_FLAG_CM = const(0x04) # Credential Management
_PERM_FLAG_BE = const(0x08) # Bio Enrollment
_PERM_FLAG_LBW = const(0x10) # Large Blob Write
_PERM_FLAG_ACFG = const(0x20) # Authenticator Configuration
# CBOR CredentialManagement command parameter keys
_CREDMGMT_CMD_SUBCOMMAND = const(0x01) # unsigned int
_CREDMGMT_CMD_SUBCOMMAND_PARAMS = const(0x02) # map
_CREDMGMT_CMD_PIN_PROTOCOL = const(0x03) # unsigned int
_CREDMGMT_CMD_PIN_AUTH = const(0x04) # bytes
_CREDMGMT_SUBCMD_CRED_METADATA = const(0x01)
_CREDMGMT_SUBCMD_RP_BEGIN = const(0x02)
_CREDMGMT_SUBCMD_RP_NEXT = const(0x03)
_CREDMGMT_SUBCMD_RK_BEGIN = const(0x04)
_CREDMGMT_SUBCMD_RK_NEXT = const(0x05)
_CREDMGMT_SUBCMD_DELETE_CRED = const(0x06)
_CREDMGMT_SUBCMD_UPDATE_CRED = const(0x07)
_CREDMGMT_PARAMS_RPID_HASH = const(0x01) # bytes
_CREDMGMT_PARAMS_CRED_ID = const(0x02) # map (PublicKeyCredentialDescriptor)
_CREDMGMT_PARAMS_USER = const(0x03) # map
# CBOR CredentialManagement response member keys
_CREDMGMT_RESP_RESIDENT_CRED_COUNT = const(
0x01
) # unsigned int, number of occupied slots
_CREDMGMT_RESP_MAX_REM_RESIDENT_CRED_COUNT = const(
0x02
) # unsigned int, number of free slots
_CREDMGMT_RESP_RP = const(0x03) # PublicKeyCredentialRpEntity, RP Information
_CREDMGMT_RESP_RPID_HASH = const(0x04) # bytes, RPID SHA-256 hash
_CREDMGMT_RESP_RP_COUNT = const(0x05) # unsigned int, total number of RPs stored
_CREDMGMT_RESP_USER = const(0x06) # PublicKeyCredentialUserEntity, user information
_CREDMGMT_RESP_CRED_ID = const(0x07) # PublicKeyCredentialDescriptor
_CREDMGMT_RESP_CRED_PUBLIC_KEY = const(0x08) # COSE_Key
_CREDMGMT_RESP_RP_CRED_COUNT = const(
0x09
) # unsigned int, number of credentials stored for the RP
_CREDMGMT_RESP_CRED_PROTECT = const(0x0A) # unsigned int, credential protection policy
_CREDMGMT_RESP_LARGE_BLOB_KEY = const(0x0B) # bytes, large blob encryption key
# status codes for the keepalive cmd
_KEEPALIVE_STATUS_NONE = const(0x00)
_KEEPALIVE_STATUS_PROCESSING = const(0x01) # still processing the current request
@ -159,6 +213,10 @@ _ERR_KEEPALIVE_CANCEL = const(0x2D) # pending keep alive was cancelled
_ERR_NO_CREDENTIALS = const(0x2E) # no valid credentials provided
_ERR_NOT_ALLOWED = const(0x30) # continuation command not allowed
_ERR_PIN_AUTH_INVALID = const(0x33) # pinAuth verification failed
_ERR_PUAT_REQUIRED = const(0x36) # pinUvAuthToken required for the operation
_ERR_UNAUTHORIZED_PERMISSION = const(
0x40
) # permissions parameter contains an unauthorized permission
_ERR_OTHER = const(0x7F) # other unspecified error
_ERR_EXTENSION_FIRST = const(0xE0) # extension specific error
@ -235,6 +293,20 @@ _last_wink_cid = 0
_last_good_auth_check_cid = 0
class AuthTokenState:
def __init__(self):
self.permissions_rp_id = None
self.permissions = 0
self.user_present_limit_seconds = 30
self.max_usage_time_seconds = 600
self.user_verified = False
self.user_present = False
# pinUvAuthTokens
pinUvAuthTokens = {} # Dict[bytes, AuthTokenState]
class CborError(Exception):
def __init__(self, code: int):
super().__init__()
@ -1162,6 +1234,10 @@ def dispatch_cmd(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]:
if __debug__:
log.debug(__name__, "_CBOR_GET_NEXT_ASSERTION")
return cbor_error(req.cid, _ERR_NOT_ALLOWED)
elif req.data[0] == _CBOR_CREDENTIAL_MANAGEMENT:
if __debug__:
log.debug(__name__, "_CBOR_CREDENTIAL_MANAGEMENT")
return cbor_cred_mgmt(req)
else:
if __debug__:
log.warning(__name__, "_ERR_INVALID_CMD _CMD_CBOR %d", req.data[0])
@ -1870,13 +1946,15 @@ def cbor_get_info(req: Cmd) -> Cmd:
# Note: We claim that the PIN is set even when it's not, because otherwise
# login.live.com shows an error, but doesn't instruct the user to set a PIN.
response_data = {
_GETINFO_RESP_VERSIONS: ["U2F_V2", "FIDO_2_0"],
_GETINFO_RESP_VERSIONS: ["U2F_V2", "FIDO_2_0", "FIDO_2_1"],
_GETINFO_RESP_EXTENSIONS: ["hmac-secret"],
_GETINFO_RESP_AAGUID: _AAGUID,
_GETINFO_RESP_OPTIONS: {
"rk": _ALLOW_RESIDENT_CREDENTIALS,
"up": True,
"uv": True,
"pinUvAuthToken": True,
"credMgmt": True,
},
_GETINFO_RESP_PIN_PROTOCOLS: [1],
_GETINFO_RESP_MAX_CRED_COUNT_IN_LIST: _MAX_CRED_COUNT_IN_LIST,
@ -1894,27 +1972,45 @@ def cbor_client_pin(req: Cmd) -> Cmd:
return cbor_error(req.cid, _ERR_INVALID_CBOR)
if pin_protocol != 1:
return cbor_error(req.cid, _ERR_PIN_AUTH_INVALID)
return cbor_error(req.cid, _ERR_INVALID_PAR)
# We only support the get key agreement command which is required for the hmac-secret extension.
if subcommand != _CLIENTPIN_SUBCMD_GET_KEY_AGREEMENT:
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_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: {
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:],
if subcommand == _CLIENTPIN_SUBCMD_GET_KEY_AGREEMENT:
# Encode the public key of the authenticator key agreement key.
# 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: {
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:],
}
}
}
return Cmd(req.cid, _CMD_CBOR, bytes([_ERR_NONE]) + cbor.encode(response_data))
return Cmd(req.cid, _CMD_CBOR, bytes([_ERR_NONE]) + cbor.encode(response_data))
elif subcommand == _CLIENTPIN_SUBCMD_GET_TOKEN_USING_UV:
try:
key_agreement = param[_CLIENTPIN_CMD_KEY_AGREEMENT]
permissions = param[_CLIENTPIN_CMD_PERMISSIONS]
except Exception:
return cbor_error(req.cid, _ERR_MISSING_PARAMETER)
permissions_rp_id = param.get(_CLIENTPIN_CMD_PERMISSIONS_RPID, None)
if (
permissions & (_PERM_FLAG_CM | _PERM_FLAG_GA)
) and permissions_rp_id is None:
return cbor_error(req.cid, _ERR_MISSING_PARAMETER)
if permissions == 0:
return cbor_error(req.cid, _ERR_INVALID_PAR)
if permissions & (_PERM_FLAG_BE | _PERM_FLAG_LBW | _PERM_FLAG_ACFG):
return cbor_error(req.cid, _ERR_UNAUTHORIZED_PERMISSION)
if not config.has_pin():
pass # TODO Fido2ConfirmNoPin and return _ERR_NOT_ALLOWED
# TODO request user consent for the requested permissions
# TODO perform user verification repeatedly
else:
return cbor_error(req.cid, _ERR_UNSUPPORTED_OPTION)
def cbor_reset(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]:
@ -1929,5 +2025,90 @@ def cbor_reset(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]:
return None
def cbor_cred_mgmt(req: Cmd) -> Cmd:
global _rp_iter, _cred_iter
try:
param = cbor.decode(req.data[1:])
subcommand = param[_CREDMGMT_CMD_SUBCOMMAND]
except Exception:
return cbor_error(req.cid, _ERR_INVALID_CBOR)
if subcommand == _CREDMGMT_SUBCMD_CRED_METADATA:
count = sum(1 for _ in find_all())
response_data = {
_CREDMGMT_RESP_RESIDENT_CRED_COUNT: count,
_CREDMGMT_RESP_MAX_REM_RESIDENT_CRED_COUNT: MAX_RESIDENT_CREDENTIALS
- count,
}
elif subcommand == _CREDMGMT_SUBCMD_RP_BEGIN:
rp_ids = set((cred.rp_id, cred.rp_id_hash) for cred in find_all())
if rp_ids:
_rp_iter = iter(rp_ids)
rp_id, rp_id_hash = next(_rp_iter)
response_data = {
_CREDMGMT_RESP_RP: {"id": rp_id},
_CREDMGMT_RESP_RPID_HASH: rp_id_hash,
_CREDMGMT_RESP_RP_COUNT: len(rp_ids),
}
else:
response_data = {_CREDMGMT_RESP_RP_COUNT: 0}
elif subcommand == _CREDMGMT_SUBCMD_RP_NEXT:
try:
rp_id, rp_id_hash = next(_rp_iter)
response_data = {
_CREDMGMT_RESP_RP: {"id": rp_id},
_CREDMGMT_RESP_RPID_HASH: rp_id_hash,
}
except Exception:
return cbor_error(req.cid, _ERR_NOT_ALLOWED)
elif subcommand == _CREDMGMT_SUBCMD_RK_BEGIN:
rp_id_hash = param[_CREDMGMT_CMD_SUBCOMMAND_PARAMS][_CREDMGMT_PARAMS_RPID_HASH]
if not isinstance(rp_id_hash, bytes):
raise TypeError
creds = set(cred for cred in find_by_rp_id_hash(rp_id_hash))
if creds:
_cred_iter = iter(creds)
cred = next(_cred_iter)
response_data = {
_CREDMGMT_RESP_USER: cred.user(),
_CREDMGMT_RESP_CRED_ID: {"type": "public-key", "id": cred.id},
_CREDMGMT_RESP_CRED_PUBLIC_KEY: cred.public_key(), # TODO might need to be cbor-decoded
_CREDMGMT_RESP_RP_CRED_COUNT: len(creds),
}
else:
return cbor_error(req.cid, _ERR_NO_CREDENTIALS)
elif subcommand == _CREDMGMT_SUBCMD_RK_NEXT:
try:
cred = next(_cred_iter)
response_data = {
_CREDMGMT_RESP_USER: cred.user(),
_CREDMGMT_RESP_CRED_ID: {"type": "public-key", "id": cred.id},
_CREDMGMT_RESP_CRED_PUBLIC_KEY: cred.public_key(), # TODO might need to be cbor-decoded
}
except Exception:
return cbor_error(req.cid, _ERR_NOT_ALLOWED)
elif subcommand == _CREDMGMT_SUBCMD_DELETE_CRED:
credential_descriptor = param[_CREDMGMT_CMD_SUBCOMMAND_PARAMS][
_CREDMGMT_PARAMS_CRED_ID
]
credential_type = credential_descriptor["type"]
if not isinstance(credential_type, str) or credential_type != "public-key":
raise TypeError
credential_id = credential_descriptor["id"]
if not isinstance(credential_id, bytes):
raise TypeError
cred = find_by_cred_id(credential_id)
if cred is None:
return cbor_error(req.cid, _ERR_NO_CREDENTIALS)
assert cred.index is not None
storage.resident_credentials.delete(cred.index)
else:
return cbor_error(req.cid, _ERR_UNSUPPORTED_OPTION)
return Cmd(req.cid, _CMD_CBOR, bytes([_ERR_NONE]) + cbor.encode(response_data))
def cmd_keepalive(cid: int, status: int) -> Cmd:
return Cmd(cid, _CMD_KEEPALIVE, bytes([status]))

@ -42,6 +42,14 @@ def find_by_rp_id_hash(rp_id_hash: bytes) -> Iterator[Fido2Credential]:
yield _credential_from_data(index, data)
def find_by_cred_id(cred_id: bytes) -> Optional[Fido2Credential]:
for index in range(MAX_RESIDENT_CREDENTIALS):
data = storage.resident_credentials.get(index)
if data is not None and data[RP_ID_HASH_LENGTH:] == cred_id:
return _credential_from_data(index, data)
return None
def get_resident_credential(index: int) -> Optional[Fido2Credential]:
if not (0 <= index < MAX_RESIDENT_CREDENTIALS):
return None

Loading…
Cancel
Save