diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 07222587..c443193a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: run: | sudo apt-add-repository ppa:yubico/stable sudo apt-get update - sudo apt-get install -q -y git gcc g++ cmake swig psmisc procps pcscd pcsc-tools yubico-piv-tool libhidapi-dev libassuan-dev libgcrypt20-dev libksba-dev libnpth0-dev opensc openssl openssh-server libpcsclite-dev libudev-dev libcmocka-dev python3-pip python3-setuptools python3-wheel lcov yubikey-manager + sudo apt-get install -q -y git gcc g++ cmake swig psmisc procps pcscd pcsc-tools yubico-piv-tool libhidapi-dev libassuan-dev libgcrypt20-dev libksba-dev libnpth0-dev opensc openssl openssh-server libpcsclite-dev libudev-dev libcmocka-dev python3-pip python3-setuptools python3-wheel lcov yubikey-manager libcbor-dev pip3 install --upgrade pip - name: Set up Go 1.13 @@ -24,7 +24,7 @@ jobs: submodules: recursive - name: Cache GO Modules - uses: actions/cache@v1 + uses: actions/cache@v3 env: cache-name: go_mod with: @@ -32,20 +32,34 @@ jobs: key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('./go.mod') }} - name: Cache Patched GPG - uses: actions/cache@v1 + uses: actions/cache@v3 env: cache-name: cache_gpg_binary with: path: gnupg key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./test-via-pcsc/build_gpg.sh') }} + - name: Cache FIDO Tools + uses: actions/cache@v3 + env: + cache-name: cache_fido_tools + with: + path: | + u2f-ref-code + libfido2 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./test-via-pcsc/build_fido_tests.sh') }} + - name: Build Patched GPG run: | ./test-via-pcsc/build_gpg.sh gpg --version - name: Build FIDO Tests - run: ./test-via-pcsc/build_fido_tests.sh + run: | + ./test-via-pcsc/build_fido_tests.sh + sudo ldconfig + which fido2-token + ldd $(which fido2-token) - name: Build for Test run: | @@ -92,10 +106,14 @@ jobs: - name: Test the FIDO2 run: | - echo 1 >/tmp/canokey-test-nfc # Emulate the NFC mode + #echo 1 >/tmp/canokey-test-nfc # Emulate the NFC mode + #pushd test-real && ./test-libfido2.sh && popd cd fido2-tests - ~/.local/bin/pytest --color=yes --nfc tests/standard/ - ~/.local/bin/pytest --color=yes --nfc tests/vendor/canokeys/ --capture=no + #../build/fido-hid-over-udp & + git pull + ~/.local/bin/pytest --color=yes --vendor canokeys --nfc tests/standard/ + ~/.local/bin/pytest --color=yes --vendor canokeys --nfc tests/vendor/canokeys/ + #kill %1 - name: Test the U2F run: | diff --git a/applets/ctap/ctap-errors.h b/applets/ctap/ctap-errors.h index fa64cd05..f3dfb8e1 100644 --- a/applets/ctap/ctap-errors.h +++ b/applets/ctap/ctap-errors.h @@ -15,7 +15,8 @@ #define CTAP2_ERR_INVALID_CBOR 0x12 #define CTAP2_ERR_MISSING_PARAMETER 0x14 #define CTAP2_ERR_LIMIT_EXCEEDED 0x15 -#define CTAP2_ERR_UNSUPPORTED_EXTENSION 0x16 +#define CTAP2_ERR_FP_DATABASE_FULL 0x17 +#define CTAP2_ERR_LARGE_BLOB_STORAGE_FULL 0x18 #define CTAP2_ERR_CREDENTIAL_EXCLUDED 0x19 #define CTAP2_ERR_PROCESSING 0x21 #define CTAP2_ERR_INVALID_CREDENTIAL 0x22 @@ -25,7 +26,6 @@ #define CTAP2_ERR_UNSUPPORTED_ALGORITHM 0x26 #define CTAP2_ERR_OPERATION_DENIED 0x27 #define CTAP2_ERR_KEY_STORE_FULL 0x28 -#define CTAP2_ERR_NO_OPERATION_PENDING 0x2A #define CTAP2_ERR_UNSUPPORTED_OPTION 0x2B #define CTAP2_ERR_INVALID_OPTION 0x2C #define CTAP2_ERR_KEEPALIVE_CANCEL 0x2D @@ -37,12 +37,16 @@ #define CTAP2_ERR_PIN_AUTH_INVALID 0x33 #define CTAP2_ERR_PIN_AUTH_BLOCKED 0x34 #define CTAP2_ERR_PIN_NOT_SET 0x35 -#define CTAP2_ERR_PIN_REQUIRED 0x36 +#define CTAP2_ERR_PUAT_REQUIRED 0x36 #define CTAP2_ERR_PIN_POLICY_VIOLATION 0x37 -#define CTAP2_ERR_PIN_TOKEN_EXPIRED 0x38 #define CTAP2_ERR_REQUEST_TOO_LARGE 0x39 #define CTAP2_ERR_ACTION_TIMEOUT 0x3A #define CTAP2_ERR_UP_REQUIRED 0x3B +#define CTAP2_ERR_UV_BLOCKED 0x3C +#define CTAP2_ERR_INTEGRITY_FAILURE 0x3D +#define CTAP2_ERR_INVALID_SUBCOMMAND 0x3E +#define CTAP2_ERR_UV_INVALID 0x3F +#define CTAP2_ERR_UNAUTHORIZED_PERMISSION 0x40 #define CTAP1_ERR_OTHER 0x7F #define CTAP2_ERR_SPEC_LAST 0xDF #define CTAP2_ERR_EXTENSION_FIRST 0xE0 diff --git a/applets/ctap/ctap-internal.h b/applets/ctap/ctap-internal.h index 463c10d9..85df4c9e 100644 --- a/applets/ctap/ctap-internal.h +++ b/applets/ctap/ctap-internal.h @@ -5,209 +5,365 @@ #include #include #include +#include #include #include -#define CTAP_CERT_FILE "ctap_cert" -#define KEY_ATTR 0x00 -#define SIGN_CTR_ATTR 0x01 -#define PIN_ATTR 0x02 -#define PIN_CTR_ATTR 0x03 -#define KH_KEY_ATTR 0x04 -#define HE_KEY_ATTR 0x05 -#define RK_FILE "ctap_rk" - -#define CTAP_INS_MSG 0x10 - -#define CTAP_MAKE_CREDENTIAL 0x01 -#define CTAP_GET_ASSERTION 0x02 -#define CTAP_GET_INFO 0x04 -#define CTAP_CLIENT_PIN 0x06 -#define CTAP_RESET 0x07 -#define CTAP_GET_NEXT_ASSERTION 0x08 - -#define PARAM_clientDataHash (1 << 0) -#define PARAM_rpId (1 << 1) -#define PARAM_user (1 << 2) -#define PARAM_pubKeyCredParams (1 << 3) -#define PARAM_extensions (1 << 4) -#define PARAM_options (1 << 5) -#define PARAM_pinAuth (1 << 6) -#define PARAM_pinProtocol (1 << 7) -#define PARAM_subCommand (1 << 8) -#define PARAM_keyAgreement (1 << 9) -#define PARAM_newPinEnc (1 << 10) -#define PARAM_pinHashEnc (1 << 11) -#define PARAM_hmacSecret (1 << 12) - -#define MC_requiredMask (PARAM_clientDataHash | PARAM_rpId | PARAM_user | PARAM_pubKeyCredParams) -#define GA_requiredMask (PARAM_clientDataHash | PARAM_rpId) -#define CP_requiredMask (PARAM_pinProtocol | PARAM_subCommand) - -#define MC_clientDataHash 0x01 -#define MC_rp 0x02 -#define MC_user 0x03 -#define MC_pubKeyCredParams 0x04 -#define MC_excludeList 0x05 -#define MC_extensions 0x06 -#define MC_options 0x07 -#define MC_pinAuth 0x08 -#define MC_pinProtocol 0x09 - -#define GA_rpId 0x01 -#define GA_clientDataHash 0x02 -#define GA_allowList 0x03 -#define GA_extensions 0x04 -#define GA_options 0x05 -#define GA_pinAuth 0x06 -#define GA_pinProtocol 0x07 - -#define HMAC_SECRET_keyAgreement 0x01 -#define HMAC_SECRET_saltEnc 0x02 -#define HMAC_SECRET_saltAuth 0x03 - -#define CP_pinProtocol 0x01 -#define CP_subCommand 0x02 -#define CP_cmdGetRetries 0x01 -#define CP_cmdGetKeyAgreement 0x02 -#define CP_cmdSetPin 0x03 -#define CP_cmdChangePin 0x04 -#define CP_cmdGetPinToken 0x05 -#define CP_keyAgreement 0x03 -#define CP_pinAuth 0x04 -#define CP_newPinEnc 0x05 -#define CP_pinHashEnc 0x06 - -#define RESP_versions 0x1 -#define RESP_extensions 0x2 -#define RESP_aaguid 0x3 -#define RESP_options 0x4 -#define RESP_maxMsgSize 0x5 -#define RESP_pinProtocols 0x6 - -#define RESP_fmt 0x01 -#define RESP_authData 0x02 -#define RESP_attStmt 0x03 - -#define RESP_credential 0x01 -#define RESP_signature 0x03 -#define RESP_publicKeyCredentialUserEntity 0x04 -#define RESP_numberOfCredentials 0x05 - -#define RESP_keyAgreement 0x01 -#define RESP_pinToken 0x02 -#define RESP_retries 0x03 +#define FIRMWARE_VERSION 201 + +// Filesystem Meta +#define CTAP_CERT_FILE "ctap_cert" +#define KEY_ATTR 0x00 +#define SIGN_CTR_ATTR 0x01 +#define PIN_ATTR 0x02 +#define PIN_CTR_ATTR 0x03 +#define KH_KEY_ATTR 0x04 +#define HE_KEY_ATTR 0x05 +#define DC_FILE "ctap_dc" +#define DC_GENERAL_ATTR 0x00 +#define DC_META_FILE "ctap_dm" +#define LB_FILE "ctap_lb" +#define LB_FILE_TMP "ctap_lbt" + +// Commands +#define CTAP_INS_MSG 0x10 + +#define CTAP_MAKE_CREDENTIAL 0x01 +#define CTAP_GET_ASSERTION 0x02 +#define CTAP_GET_INFO 0x04 +#define CTAP_CLIENT_PIN 0x06 +#define CTAP_RESET 0x07 +#define CTAP_GET_NEXT_ASSERTION 0x08 +#define CTAP_CREDENTIAL_MANAGEMENT 0x0A +#define CTAP_SELECTION 0x0B +#define CTAP_LARGE_BLOBS 0x0C +#define CTAP_CONFIG 0x0D +#define CTAP_CRED_MANAGE_LEGACY 0x41 + +// Parsed params +#define PARAM_CLIENT_DATA_HASH (1 << 0) +#define PARAM_RP (1 << 1) +#define PARAM_USER (1 << 2) +#define PARAM_PUB_KEY_CRED_PARAMS (1 << 3) +#define PARAM_EXTENSIONS (1 << 4) +#define PARAM_OPTIONS (1 << 5) +#define PARAM_PIN_UV_AUTH_PARAM (1 << 6) +#define PARAM_PIN_UV_AUTH_PROTOCOL (1 << 7) +#define PARAM_SUB_COMMAND (1 << 8) +#define PARAM_KEY_AGREEMENT (1 << 9) +#define PARAM_NEW_PIN_ENC (1 << 10) +#define PARAM_PIN_HASH_ENC (1 << 11) +#define PARAM_HMAC_SECRET (1 << 12) +#define PARAM_ENTERPRISE_ATTESTATION (1 << 13) +#define PARAM_PERMISSIONS (1 << 14) +#define PARAM_CREDENTIAL_ID (1 << 15) +#define PARAM_GET (1 << 16) +#define PARAM_SET (1 << 17) +#define PARAM_OFFSET (1 << 18) +#define PARAM_LENGTH (1 << 19) + +#define MC_REQUIRED_MASK (PARAM_CLIENT_DATA_HASH | PARAM_RP | PARAM_USER | PARAM_PUB_KEY_CRED_PARAMS) +#define GA_REQUIRED_MASK (PARAM_CLIENT_DATA_HASH | PARAM_RP) +#define CP_REQUIRED_MASK (PARAM_SUB_COMMAND) +#define CM_REQUIRED_MASK (PARAM_SUB_COMMAND) + +#define OPTION_FALSE 0x0 +#define OPTION_TRUE 0x1 +#define OPTION_ABSENT 0x2 #define FLAGS_UP (1) #define FLAGS_UV (1 << 2) #define FLAGS_AT (1 << 6) #define FLAGS_ED (1 << 7) -#define KH_KEY_SIZE 32 -#define HE_KEY_SIZE 32 -#define PRI_KEY_SIZE 32 -#define PUB_KEY_SIZE 64 -#define SHARED_SECRET_SIZE 32 -#define MAX_COSE_KEY_SIZE 78 -#define MAX_PIN_SIZE 63 -#define PIN_HASH_SIZE 16 -#define MAX_CERT_SIZE 1152 -#define AAGUID_SIZE 16 -#define PIN_AUTH_SIZE 16 -#define PIN_TOKEN_SIZE 16 -#define HMAC_SECRET_SALT_SIZE 64 -#define HMAC_SECRET_SALT_AUTH_SIZE 16 -#define CREDENTIAL_TAG_SIZE 16 -#define CLIENT_DATA_HASH_SIZE 32 -#define CREDENTIAL_NONCE_SIZE 16 -#define DOMAIN_NAME_MAX_SIZE 254 -#define USER_ID_MAX_SIZE 64 -#define USER_NAME_LIMIT 65 // Must be minimum of 64 bytes but can be more. -#define DISPLAY_NAME_LIMIT 65 // Must be minimum of 64 bytes but can be more. -#define ICON_LIMIT 129 // Must be minimum of 64 bytes but can be more. -#define MAX_RK_NUM 64 +#define CRED_PROTECT_ABSENT 0x00 +#define CRED_PROTECT_VERIFICATION_OPTIONAL 0x01 +#define CRED_PROTECT_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST 0x02 +#define CRED_PROTECT_VERIFICATION_REQUIRED 0x03 + +// Params for each command +#define MC_REQ_CLIENT_DATA_HASH 0x01 +#define MC_REQ_RP 0x02 +#define MC_REQ_USER 0x03 +#define MC_REQ_PUB_KEY_CRED_PARAMS 0x04 +#define MC_REQ_EXCLUDE_LIST 0x05 +#define MC_REQ_EXTENSIONS 0x06 +#define MC_REQ_OPTIONS 0x07 +#define MC_REQ_PIN_UV_AUTH_PARAM 0x08 +#define MC_REQ_PIN_PROTOCOL 0x09 +#define MC_REQ_ENTERPRISE_ATTESTATION 0x0A +#define MC_RESP_FMT 0x01 +#define MC_RESP_AUTH_DATA 0x02 +#define MC_RESP_ATT_STMT 0x03 +#define MC_RESP_EP_ATT 0x04 +#define MC_RESP_LARGE_BLOB_KEY 0x05 + +#define GA_REQ_RP_ID 0x01 +#define GA_REQ_CLIENT_DATA_HASH 0x02 +#define GA_REQ_ALLOW_LIST 0x03 +#define GA_REQ_EXTENSIONS 0x04 +#define GA_REQ_OPTIONS 0x05 +#define GA_REQ_PIN_UV_AUTH_PARAM 0x06 +#define GA_REQ_PIN_UV_AUTH_PROTOCOL 0x07 +#define GA_REQ_HMAC_SECRET_KEY_AGREEMENT 0x01 +#define GA_REQ_HMAC_SECRET_SALT_ENC 0x02 +#define GA_REQ_HMAC_SECRET_SALT_AUTH 0x03 +#define GA_REQ_HMAC_SECRET_PIN_PROTOCOL 0x04 +#define GA_RESP_CREDENTIAL 0x01 +#define GA_RESP_AUTH_DATA 0x02 +#define GA_RESP_SIGNATURE 0x03 +#define GA_RESP_PUBLIC_KEY_CREDENTIAL_USER_ENTITY 0x04 +#define GA_RESP_NUMBER_OF_CREDENTIALS 0x05 +#define GA_RESP_LARGE_BLOB_KEY 0x07 + +#define GI_RESP_VERSIONS 0x01 +#define GI_RESP_EXTENSIONS 0x02 +#define GI_RESP_AAGUID 0x03 +#define GI_RESP_OPTIONS 0x04 +#define GI_RESP_MAX_MSG_SIZE 0x05 +#define GI_RESP_PIN_UV_AUTH_PROTOCOLS 0x06 +#define GI_RESP_MAX_CREDENTIAL_COUNT_IN_LIST 0x07 +#define GI_RESP_MAX_CREDENTIAL_ID_LENGTH 0x08 +#define GI_RESP_TRANSPORTS 0x09 +#define GI_RESP_ALGORITHMS 0x0A +#define GI_RESP_MAX_SERIALIZED_LARGE_BLOB_ARRAY 0x0B +#define GI_RESP_FIRMWARE_VERSION 0x0E +#define GI_RESP_MAX_CRED_BLOB_LENGTH 0x0F + +#define CP_REQ_PIN_UV_AUTH_PROTOCOL 0x01 +#define CP_REQ_SUB_COMMAND 0x02 +#define CP_REQ_KEY_AGREEMENT 0x03 +#define CP_REQ_PIN_UV_AUTH_PARAM 0x04 +#define CP_REQ_NEW_PIN_ENC 0x05 +#define CP_REQ_PIN_HASH_ENC 0x06 +#define CP_REQ_PERMISSIONS 0x09 +#define CP_REQ_RP_ID 0x0A +#define CP_CMD_GET_PIN_RETRIES 0x01 +#define CP_CMD_GET_KEY_AGREEMENT 0x02 +#define CP_CMD_SET_PIN 0x03 +#define CP_CMD_CHANGE_PIN 0x04 +#define CP_CMD_GET_PIN_TOKEN 0x05 +#define CP_CMD_GET_PIN_UV_AUTH_TOKEN_USING_PIN_WITH_PERMISSIONS 0x09 +#define CP_RESP_KEY_AGREEMENT 0x01 +#define CP_RESP_PIN_UV_AUTH_TOKEN 0x02 +#define CP_RESP_PIN_RETRIES 0x03 +#define CP_PERMISSION_MC 0x01 +#define CP_PERMISSION_GA 0x02 +#define CP_PERMISSION_CM 0x04 +#define CP_PERMISSION_BE 0x08 +#define CP_PERMISSION_LBW 0x10 +#define CP_PERMISSION_ACFG 0x20 + +#define CM_REQ_SUB_COMMAND 0x01 +#define CM_REQ_SUB_COMMAND_PARAMS 0x02 +#define CM_REQ_PIN_UV_AUTH_PROTOCOL 0x03 +#define CM_REQ_PIN_UV_AUTH_PARAM 0x04 +#define CM_CMD_GET_CREDS_METADATA 0x01 +#define CM_CMD_ENUMERATE_RPS_BEGIN 0x02 +#define CM_CMD_ENUMERATE_RPS_GET_NEXT_RP 0x03 +#define CM_CMD_ENUMERATE_CREDENTIALS_BEGIN 0x04 +#define CM_CMD_ENUMERATE_CREDENTIALS_GET_NEXT_CREDENTIAL 0x05 +#define CM_CMD_DELETE_CREDENTIAL 0x06 +#define CM_CMD_UPDATE_USER_INFORMATION 0x07 +#define CM_PARAM_RP_ID_HASH 0x01 +#define CM_PARAM_CREDENTIAL_ID 0x02 +#define CM_PARAM_USER 0x03 +#define CM_RESP_EXISTING_RESIDENT_CREDENTIALS_COUNT 0x01 +#define CM_RESP_MAX_POSSIBLE_REMAINING_RESIDENT_CREDENTIALS_COUNT 0x02 +#define CM_RESP_RP 0x03 +#define CM_RESP_RP_ID_HASH 0x04 +#define CM_RESP_TOTAL_RPS 0x05 +#define CM_RESP_USER 0x06 +#define CM_RESP_CREDENTIAL_ID 0x07 +#define CM_RESP_PUBLIC_KEY 0x08 +#define CM_RESP_TOTAL_CREDENTIALS 0x09 +#define CM_RESP_CRED_PROTECT 0x0A +#define CM_RESP_LARGE_BLOB_KEY 0x0B + +#define LB_REQ_GET 0x01 +#define LB_REQ_SET 0x02 +#define LB_REQ_OFFSET 0x03 +#define LB_REQ_LENGTH 0x04 +#define LB_REQ_PIN_UV_AUTH_PARAM 0x05 +#define LB_REQ_PIN_UV_AUTH_PROTOCOL 0x06 +#define LB_RESP_CONFIG 0x01 + +// Size limits +#define KH_KEY_SIZE 32 +#define HE_KEY_SIZE 32 +#define PRI_KEY_SIZE 32 +#define PUB_KEY_SIZE 64 +#define SHARED_SECRET_SIZE 32 +#define MAX_COSE_KEY_SIZE 78 +#define PIN_ENC_SIZE_P1 64 +#define PIN_ENC_SIZE_P2 80 +#define PIN_HASH_SIZE_P1 16 +#define PIN_HASH_SIZE_P2 32 +#define MAX_CERT_SIZE 1152 +#define AAGUID_SIZE 16 +#define PIN_AUTH_SIZE_P1 16 +#define PIN_TOKEN_SIZE 32 +#define HMAC_SECRET_SALT_SIZE 64 +#define HMAC_SECRET_SALT_AUTH_SIZE 16 +#define CREDENTIAL_TAG_SIZE 16 +#define CLIENT_DATA_HASH_SIZE 32 +#define CREDENTIAL_NONCE_SIZE 16 +#define CREDENTIAL_NONCE_DC_POS 16 +#define CREDENTIAL_NONCE_CP_POS 17 +#define DOMAIN_NAME_MAX_SIZE 254 +#define USER_ID_MAX_SIZE 64 +#define DISPLAY_NAME_LIMIT 65 +#define USER_NAME_LIMIT 65 +#define MAX_DC_NUM 64 +#define MAX_STORED_RPID_LENGTH 32 +#define MAX_EXTENSION_SIZE_IN_AUTH 51 +#define MAX_CREDENTIAL_COUNT_IN_LIST 8 +#define MAX_CRED_BLOB_LENGTH 32 +#define LARGE_BLOB_KEY_SIZE 32 +#define LARGE_BLOB_SIZE_LIMIT 4096 +#define MAX_FRAGMENT_LENGTH (MAX_CTAP_BUFSIZE - 64) typedef struct { uint8_t id[USER_ID_MAX_SIZE]; uint8_t id_size; - uint8_t name[USER_NAME_LIMIT]; - uint8_t displayName[DISPLAY_NAME_LIMIT]; - uint8_t icon[ICON_LIMIT]; -} __packed UserEntity; + char name[USER_NAME_LIMIT]; + char display_name[DISPLAY_NAME_LIMIT]; +} __packed user_entity; typedef struct { uint8_t tag[CREDENTIAL_TAG_SIZE]; - uint8_t nonce[CREDENTIAL_NONCE_SIZE]; - uint8_t rpIdHash[SHA256_DIGEST_LENGTH]; + uint8_t nonce[CREDENTIAL_NONCE_SIZE + 2]; // 16-byte random nonce + 1-byte dc + 1-byte cp + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; int32_t alg_type; -} __packed CredentialId; +} __packed credential_id; + +typedef struct { + credential_id credential_id; + user_entity user; + bool deleted; + bool has_large_blob_key; + uint8_t large_blob_key[LARGE_BLOB_KEY_SIZE]; + uint8_t cred_blob_len; + uint8_t cred_blob[MAX_CRED_BLOB_LENGTH]; +} __packed CTAP_discoverable_credential; typedef struct { - CredentialId credential_id; - UserEntity user; -} __packed CTAP_residentKey; + uint8_t numbers; + uint8_t index; // enough when MAX_DC_NUM == 64 + uint8_t pending_add: 1; + uint8_t pending_delete: 1; +} __packed CTAP_dc_general_attr; + +typedef struct { + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; + uint8_t rp_id[MAX_STORED_RPID_LENGTH]; + size_t rp_id_len; + uint64_t slots; +} __packed CTAP_rp_meta; typedef struct { uint8_t aaguid[AAGUID_SIZE]; - uint16_t credentialIdLength; - CredentialId credentialId; - uint8_t publicKey[MAX_COSE_KEY_SIZE]; // public key in cose_key format + uint16_t credential_id_length; + credential_id credential_id; + uint8_t public_key[MAX_COSE_KEY_SIZE]; // public key in cose_key format // https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples -} __packed CTAP_attestedData; +} __packed CTAP_attested_data; typedef struct { - uint8_t rpIdHash[SHA256_DIGEST_LENGTH]; + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; uint8_t flags; - uint32_t signCount; - CTAP_attestedData at; - uint8_t extensions[14]; -} __packed CTAP_authData; + uint32_t sign_count; + CTAP_attested_data at; + uint8_t extensions[MAX_EXTENSION_SIZE_IN_AUTH]; +} __packed CTAP_auth_data; typedef struct { - uint16_t parsedParams; - uint8_t clientDataHash[CLIENT_DATA_HASH_SIZE]; - uint8_t rpIdHash[SHA256_DIGEST_LENGTH]; - UserEntity user; + uint8_t up: 2; + uint8_t uv: 2; + uint8_t rk: 2; +} CTAP_options; + +typedef struct { + uint32_t parsed_params; + uint8_t client_data_hash[CLIENT_DATA_HASH_SIZE]; + uint8_t rp_id[MAX_STORED_RPID_LENGTH]; + size_t rp_id_len; + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; + user_entity user; int32_t alg_type; - CborValue excludeList; - size_t excludeListSize; - uint8_t rk; - uint8_t extension_hmac_secret; - uint8_t uv; - uint8_t pinAuth[PIN_AUTH_SIZE]; - size_t pinAuthLength; -} CTAP_makeCredential; + CborValue exclude_list; + size_t exclude_list_size; + CTAP_options options; + uint8_t pin_uv_auth_param[SHA256_DIGEST_LENGTH]; + size_t pin_uv_auth_param_len; + uint8_t pin_uv_auth_protocol; + bool ext_hmac_secret; + bool ext_large_blob_key; + uint8_t ext_cred_protect; + uint8_t ext_cred_blob[MAX_CRED_BLOB_LENGTH]; + uint8_t ext_has_cred_blob: 1; + uint8_t ext_cred_blob_len: 7; +} CTAP_make_credential; + +typedef struct { + uint32_t parsed_params; + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; + uint8_t client_data_hash[CLIENT_DATA_HASH_SIZE]; + CborValue allow_list; + size_t allow_list_size; + CTAP_options options; + uint8_t pin_uv_auth_param[SHA256_DIGEST_LENGTH]; + size_t pin_uv_auth_param_len; + uint8_t pin_uv_auth_protocol; + uint8_t ext_hmac_secret_key_agreement[PUB_KEY_SIZE]; + uint8_t ext_hmac_secret_salt_enc[HMAC_SECRET_SALT_SIZE]; + uint8_t ext_hmac_secret_salt_auth[HMAC_SECRET_SALT_AUTH_SIZE]; + uint8_t ext_hmac_secret_salt_len; + uint8_t ext_hmac_secret_pin_protocol; + bool ext_large_blob_key; + bool ext_cred_blob; +} CTAP_get_assertion; + +typedef struct { + uint32_t parsed_params; + uint8_t sub_command; + uint8_t pin_uv_auth_protocol; + uint8_t key_agreement[PUB_KEY_SIZE]; + uint8_t pin_uv_auth_param[SHA256_DIGEST_LENGTH]; + uint8_t new_pin_enc[PIN_ENC_SIZE_P2]; + uint8_t pin_hash_enc[PIN_HASH_SIZE_P2]; + uint8_t permissions; + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; +} CTAP_client_pin; typedef struct { - uint16_t parsedParams; - uint8_t rpIdHash[SHA256_DIGEST_LENGTH]; - uint8_t clientDataHash[CLIENT_DATA_HASH_SIZE]; - CborValue allowList; - size_t allowListSize; - uint8_t up; - uint8_t uv; - uint8_t pinAuth[PIN_AUTH_SIZE]; - size_t pinAuthLength; - uint8_t hmacSecretKeyAgreement[PUB_KEY_SIZE]; - uint8_t hmacSecretSaltEnc[HMAC_SECRET_SALT_SIZE]; - uint8_t hmacSecretSaltAuth[HMAC_SECRET_SALT_AUTH_SIZE]; - uint8_t hmacSecretSaltLen; -} CTAP_getAssertion; + uint32_t parsed_params; + uint8_t sub_command; + uint8_t *sub_command_params_ptr; + size_t param_len; + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; + credential_id credential_id; + user_entity user; + uint8_t pin_uv_auth_protocol; + uint8_t pin_uv_auth_param[SHA256_DIGEST_LENGTH]; +} CTAP_credential_management; typedef struct { - uint16_t parsedParams; - uint8_t subCommand; - uint8_t keyAgreement[PUB_KEY_SIZE]; - uint8_t pinAuth[PIN_AUTH_SIZE]; - uint8_t newPinEnc[MAX_PIN_SIZE + 1]; - uint8_t pinHashEnc[PIN_HASH_SIZE]; -} CTAP_clientPin; + uint32_t parsed_params; + uint16_t get; + uint8_t *set; + size_t set_len; + uint16_t offset; + uint16_t length; + uint8_t pin_uv_auth_protocol; + uint8_t pin_uv_auth_param[SHA256_DIGEST_LENGTH]; +} CTAP_large_blobs; int u2f_register(const CAPDU *capdu, RAPDU *rapdu); int u2f_authenticate(const CAPDU *capdu, RAPDU *rapdu); int u2f_version(const CAPDU *capdu, RAPDU *rapdu); int u2f_select(const CAPDU *capdu, RAPDU *rapdu); -uint8_t ctap_make_auth_data(uint8_t *rpIdHash, uint8_t *buf, uint8_t flags, uint8_t extensionSize, - const uint8_t *extension, size_t *len, int32_t alg_type); +uint8_t ctap_make_auth_data(uint8_t *rp_id_hash, uint8_t *buf, uint8_t flags, const uint8_t *extension, + uint8_t extension_size, size_t *len, int32_t alg_type, bool dc, uint8_t cred_protect); #endif diff --git a/applets/ctap/ctap-parser.c b/applets/ctap/ctap-parser.c index 7b5aa5cc..06d25729 100644 --- a/applets/ctap/ctap-parser.c +++ b/applets/ctap/ctap-parser.c @@ -10,17 +10,51 @@ if (ret > 0) DBG_MSG("CHECK_PARSER_RET %#x\n", ret); \ if (ret > 0) return ret; \ } while (0) + #define CHECK_CBOR_RET(ret) \ do { \ if (ret != CborNoError) DBG_MSG("CHECK_CBOR_RET %#x\n", ret); \ if (ret != CborNoError) return CTAP2_ERR_INVALID_CBOR; \ } while (0) -uint8_t parse_rp(uint8_t *rpIdHash, CborValue *val) { +static void maybe_truncate_rpid(uint8_t stored_rpid[MAX_STORED_RPID_LENGTH], size_t *stored_len, const uint8_t *rpid, + size_t rpid_len) { + if (rpid_len <= MAX_STORED_RPID_LENGTH) { + memcpy(stored_rpid, rpid, rpid_len); + *stored_len = rpid_len; + return; + } + + size_t used = 0; + const uint8_t *colon_position = memchr(rpid, ':', rpid_len); + if (colon_position != NULL) { + const size_t protocol_len = colon_position - rpid + 1; + const size_t to_copy = protocol_len <= MAX_STORED_RPID_LENGTH ? protocol_len : MAX_STORED_RPID_LENGTH; + memcpy(stored_rpid, rpid, to_copy); + used += to_copy; + } + + if (MAX_STORED_RPID_LENGTH - used < 3) { + *stored_len = used; + return; + } + + // U+2026, horizontal ellipsis. + stored_rpid[used++] = 0xe2; + stored_rpid[used++] = 0x80; + stored_rpid[used++] = 0xa6; + + const size_t to_copy = MAX_STORED_RPID_LENGTH - used; + memcpy(&stored_rpid[used], rpid + rpid_len - to_copy, to_copy); + assert(used + to_copy == MAX_STORED_RPID_LENGTH); + *stored_len = MAX_STORED_RPID_LENGTH; +} + +static uint8_t parse_rp(CTAP_make_credential *mc, CborValue *val) { if (cbor_value_get_type(val) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; CborValue map; - char key[4], domain[DOMAIN_NAME_MAX_SIZE]; + char key[4], domain[DOMAIN_NAME_MAX_SIZE + 1]; size_t map_length, len; int ret = cbor_value_enter_container(val, &map); @@ -42,9 +76,10 @@ uint8_t parse_rp(uint8_t *rpIdHash, CborValue *val) { len = DOMAIN_NAME_MAX_SIZE; ret = cbor_value_copy_text_string(&map, domain, &len, NULL); CHECK_CBOR_RET(ret); - domain[DOMAIN_NAME_MAX_SIZE - 1] = 0; - DBG_MSG("rpId: %s\n", domain); - sha256_raw((uint8_t *)domain, len, rpIdHash); + domain[len] = 0; + DBG_MSG("rp_id: %s\n", domain); + maybe_truncate_rpid(mc->rp_id, &mc->rp_id_len, (const uint8_t *) domain, len); + sha256_raw((uint8_t *) domain, len, mc->rp_id_hash); } ret = cbor_value_advance(&map); @@ -53,7 +88,7 @@ uint8_t parse_rp(uint8_t *rpIdHash, CborValue *val) { return 0; } -uint8_t parse_user(UserEntity *user, CborValue *val) { +uint8_t parse_user(user_entity *user, CborValue *val) { if (cbor_value_get_type(val) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; CborValue map; @@ -83,27 +118,23 @@ uint8_t parse_user(UserEntity *user, CborValue *val) { user->id_size = len; DBG_MSG("id: "); PRINT_HEX(user->id, len); - } else if (strcmp(key, "name") == 0) { + } else if (strcmp(key, "displayName") == 0) { if (cbor_value_get_type(&map) != CborTextStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = USER_NAME_LIMIT; - ret = cbor_value_copy_text_string(&map, (char *)user->name, &len, NULL); + len = DISPLAY_NAME_LIMIT - 1; + ret = cbor_value_copy_text_string(&map, (char *) user->display_name, &len, NULL); CHECK_CBOR_RET(ret); - user->name[USER_NAME_LIMIT - 1] = 0; - DBG_MSG("name: %s\n", user->name); - } else if (strcmp(key, "displayName") == 0) { + user->display_name[len] = 0; + DBG_MSG("displayName: %s\n", user->display_name); + } else if (strcmp(key, "name") == 0) { if (cbor_value_get_type(&map) != CborTextStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = DISPLAY_NAME_LIMIT; - ret = cbor_value_copy_text_string(&map, (char *)user->displayName, &len, NULL); + len = USER_NAME_LIMIT - 1; + ret = cbor_value_copy_text_string(&map, (char *) user->name, &len, NULL); CHECK_CBOR_RET(ret); - user->displayName[DISPLAY_NAME_LIMIT - 1] = 0; - DBG_MSG("displayName: %s\n", user->displayName); + user->name[len] = 0; + DBG_MSG("name: %s\n", user->name); } else if (strcmp(key, "icon") == 0) { + // We do not store it if (cbor_value_get_type(&map) != CborTextStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = ICON_LIMIT; - ret = cbor_value_copy_text_string(&map, (char *)user->icon, &len, NULL); - CHECK_CBOR_RET(ret); - user->icon[ICON_LIMIT - 1] = 0; - DBG_MSG("icon: %s\n", user->icon); } ret = cbor_value_advance(&map); @@ -131,7 +162,7 @@ static uint8_t parse_pub_key_cred_param(CborValue *val, int32_t *alg_type) { // required by FIDO Conformance Tool if (!is_public_key) return CTAP2_ERR_UNSUPPORTED_ALGORITHM; - ret = cbor_value_get_int_checked(&alg, (int *)alg_type); + ret = cbor_value_get_int_checked(&alg, (int *) alg_type); CHECK_CBOR_RET(ret); return 0; } @@ -153,7 +184,7 @@ uint8_t parse_verify_pub_key_cred_params(CborValue *val, int32_t *alg_type) { ret = parse_pub_key_cred_param(&arr, &cur_alg_type); CHECK_PARSER_RET(ret); if (ret == 0 && (cur_alg_type == COSE_ALG_ES256 || cur_alg_type == COSE_ALG_EDDSA)) { - // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorMakeCredential // // > This sequence is ordered from most preferred (by the RP) to least preferred. @@ -177,7 +208,7 @@ uint8_t parse_credential_descriptor(CborValue *arr, uint8_t *id) { int ret = cbor_value_map_find_value(arr, "id", &val); CHECK_CBOR_RET(ret); if (cbor_value_get_type(&val) != CborByteStringType) return CTAP2_ERR_MISSING_PARAMETER; - size_t len = sizeof(CredentialId); + size_t len = sizeof(credential_id); if (id) { ret = cbor_value_copy_byte_string(&val, id, &len, NULL); CHECK_CBOR_RET(ret); @@ -187,11 +218,6 @@ uint8_t parse_credential_descriptor(CborValue *arr, uint8_t *id) { CHECK_CBOR_RET(ret); if (cbor_value_get_type(&val) != CborTextStringType) return CTAP2_ERR_MISSING_PARAMETER; - // char type_str[10]; - // len = sizeof(type_str); - // ret = cbor_value_copy_text_string(&val, type_str, &len, NULL); - // CHECK_CBOR_RET(ret); - return 0; } @@ -214,7 +240,7 @@ uint8_t parse_public_key_credential_list(CborValue *lst) { return 0; } -uint8_t parse_options(uint8_t *rk, uint8_t *uv, uint8_t *up, CborValue *val) { +uint8_t parse_options(CTAP_options *options, CborValue *val) { size_t map_length; CborValue map; @@ -245,24 +271,22 @@ uint8_t parse_options(uint8_t *rk, uint8_t *uv, uint8_t *up, CborValue *val) { if (memcmp(key, "rk", 2) == 0) { ret = cbor_value_get_boolean(&map, &b); CHECK_CBOR_RET(ret); - DBG_MSG("rk: %d\n", b); - if (rk) *rk = b; + options->rk = b; } else if (memcmp(key, "uv", 2) == 0) { ret = cbor_value_get_boolean(&map, &b); CHECK_CBOR_RET(ret); - DBG_MSG("uv: %d\n", b); - if (uv) *uv = b; + options->uv = b; } else if (memcmp(key, "up", 2) == 0) { ret = cbor_value_get_boolean(&map, &b); CHECK_CBOR_RET(ret); - DBG_MSG("up: %d\n", b); - if (up) *up = b; + options->up = b; } else { DBG_MSG("ignoring option specified %c%c\n", key[0], key[1]); } ret = cbor_value_advance(&map); CHECK_CBOR_RET(ret); } + DBG_MSG("up: %hhu, uv: %hhu, rk: %hhu\n", options->up, options->uv, options->rk); return 0; } @@ -286,50 +310,50 @@ uint8_t parse_cose_key(CborValue *val, uint8_t *public_key) { CHECK_CBOR_RET(ret); switch (key) { - case COSE_KEY_LABEL_ALG: - if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_int_checked(&map, &key); - CHECK_CBOR_RET(ret); - if (key != COSE_ALG_ES256 && key != COSE_ALG_ECDH_ES_HKDF_256) return CTAP2_ERR_UNHANDLED_REQUEST; - ++parsed_keys; - break; - - case COSE_KEY_LABEL_KTY: - if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_int_checked(&map, &key); - CHECK_CBOR_RET(ret); - if (key != COSE_KEY_KTY_EC2) return CTAP2_ERR_UNHANDLED_REQUEST; - ++parsed_keys; - break; - - case COSE_KEY_LABEL_CRV: - if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_int_checked(&map, &key); - CHECK_CBOR_RET(ret); - if (key != COSE_KEY_CRV_P256) return CTAP2_ERR_UNHANDLED_REQUEST; - ++parsed_keys; - break; + case COSE_KEY_LABEL_ALG: + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + if (key != COSE_ALG_ES256 && key != COSE_ALG_ECDH_ES_HKDF_256) return CTAP2_ERR_UNHANDLED_REQUEST; + ++parsed_keys; + break; - case COSE_KEY_LABEL_X: - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = PRI_KEY_SIZE; - ret = cbor_value_copy_byte_string(&map, public_key, &len, NULL); - CHECK_CBOR_RET(ret); - if (len != PRI_KEY_SIZE) return CTAP2_ERR_UNHANDLED_REQUEST; - ++parsed_keys; - break; + case COSE_KEY_LABEL_KTY: + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + if (key != COSE_KEY_KTY_EC2) return CTAP2_ERR_UNHANDLED_REQUEST; + ++parsed_keys; + break; - case COSE_KEY_LABEL_Y: - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = PRI_KEY_SIZE; - ret = cbor_value_copy_byte_string(&map, public_key + PRI_KEY_SIZE, &len, NULL); - CHECK_CBOR_RET(ret); - if (len != PRI_KEY_SIZE) return CTAP2_ERR_UNHANDLED_REQUEST; - ++parsed_keys; - break; + case COSE_KEY_LABEL_CRV: + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + if (key != COSE_KEY_CRV_P256) return CTAP2_ERR_UNHANDLED_REQUEST; + ++parsed_keys; + break; + + case COSE_KEY_LABEL_X: + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + len = PRI_KEY_SIZE; + ret = cbor_value_copy_byte_string(&map, public_key, &len, NULL); + CHECK_CBOR_RET(ret); + if (len != PRI_KEY_SIZE) return CTAP2_ERR_UNHANDLED_REQUEST; + ++parsed_keys; + break; + + case COSE_KEY_LABEL_Y: + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + len = PRI_KEY_SIZE; + ret = cbor_value_copy_byte_string(&map, public_key + PRI_KEY_SIZE, &len, NULL); + CHECK_CBOR_RET(ret); + if (len != PRI_KEY_SIZE) return CTAP2_ERR_UNHANDLED_REQUEST; + ++parsed_keys; + break; - default: - DBG_MSG("Unknown cose key label: %d\n", key); + default: + DBG_MSG("Unknown cose key label: %d\n", key); } ret = cbor_value_advance(&map); @@ -342,10 +366,14 @@ uint8_t parse_cose_key(CborValue *val, uint8_t *public_key) { return 0; } -uint8_t parse_mc_extensions(uint8_t *hmac_secret, CborValue *val) { +uint8_t parse_mc_extensions(CTAP_make_credential *mc, CborValue *val) { if (cbor_value_get_type(val) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - size_t map_length; + CborValue map; + char key[13]; + size_t map_length, len; + int tmp; + int ret = cbor_value_enter_container(val, &map); CHECK_CBOR_RET(ret); ret = cbor_value_get_map_length(val, &map_length); @@ -353,21 +381,47 @@ uint8_t parse_mc_extensions(uint8_t *hmac_secret, CborValue *val) { for (size_t i = 0; i < map_length; ++i) { if (cbor_value_get_type(&map) != CborTextStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - bool is_hmac_secret; - ret = cbor_value_text_string_equals(&map, "hmac-secret", &is_hmac_secret); + len = sizeof(key); + ret = cbor_value_copy_text_string(&map, key, &len, NULL); + if (ret == CborErrorOutOfMemory) return CTAP2_ERR_LIMIT_EXCEEDED; CHECK_CBOR_RET(ret); ret = cbor_value_advance(&map); CHECK_CBOR_RET(ret); - if (cbor_value_get_type(&map) != CborBooleanType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - if (is_hmac_secret) { - bool b; - ret = cbor_value_get_boolean(&map, &b); + if (strcmp(key, "credProtect") == 0) { + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &tmp); CHECK_CBOR_RET(ret); - DBG_MSG("hmac-secret: %d\n", b); - if (hmac_secret) *hmac_secret = b; - } else { - DBG_MSG("ignoring option specified\n"); + if (tmp < 1 || tmp > 3) return CTAP2_ERR_INVALID_OPTION; + mc->ext_cred_protect = tmp; + DBG_MSG("credProtect: %d\n", tmp); + } else if (strcmp(key, "credBlob") == 0) { + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + mc->ext_has_cred_blob = 1; + len = MAX_CRED_BLOB_LENGTH; + ret = cbor_value_copy_byte_string(&map, mc->ext_cred_blob, &len, NULL); + if (ret == CborErrorOutOfMemory) { + ERR_MSG("credBlob is too long\n"); + // use this value to mark that credBlob is too long + mc->ext_cred_blob_len = MAX_CRED_BLOB_LENGTH + 1; + // return CTAP2_ERR_LIMIT_EXCEEDED; + } else { + CHECK_CBOR_RET(ret); + mc->ext_cred_blob_len = len; + DBG_MSG("credBlob: "); + PRINT_HEX(mc->ext_cred_blob, len); + } + } else if (strcmp(key, "largeBlobKey") == 0) { + if (cbor_value_get_type(&map) != CborBooleanType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_boolean(&map, &mc->ext_large_blob_key); + CHECK_CBOR_RET(ret); + DBG_MSG("largeBlobKey: %d\n", mc->ext_large_blob_key); + if (!mc->ext_large_blob_key) return CTAP2_ERR_INVALID_OPTION; + } else if (strcmp(key, "hmac-secret") == 0) { + if (cbor_value_get_type(&map) != CborBooleanType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_boolean(&map, &mc->ext_hmac_secret); + CHECK_CBOR_RET(ret); + DBG_MSG("hmac-secret: %d\n", mc->ext_hmac_secret); } ret = cbor_value_advance(&map); @@ -376,10 +430,14 @@ uint8_t parse_mc_extensions(uint8_t *hmac_secret, CborValue *val) { return 0; } -uint8_t parse_ga_extensions(CTAP_getAssertion *ga, CborValue *val) { +uint8_t parse_ga_extensions(CTAP_get_assertion *ga, CborValue *val) { if (cbor_value_get_type(val) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - size_t map_length; + CborValue map; + char key[13]; + size_t map_length, len; + int tmp; + int ret = cbor_value_enter_container(val, &map); CHECK_CBOR_RET(ret); ret = cbor_value_get_map_length(val, &map_length); @@ -387,16 +445,16 @@ uint8_t parse_ga_extensions(CTAP_getAssertion *ga, CborValue *val) { for (size_t i = 0; i < map_length; ++i) { if (cbor_value_get_type(&map) != CborTextStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - - bool is_hmac_secret; - ret = cbor_value_text_string_equals(&map, "hmac-secret", &is_hmac_secret); + len = sizeof(key); + ret = cbor_value_copy_text_string(&map, key, &len, NULL); + if (ret == CborErrorOutOfMemory) return CTAP2_ERR_LIMIT_EXCEEDED; CHECK_CBOR_RET(ret); ret = cbor_value_advance(&map); CHECK_CBOR_RET(ret); - if (is_hmac_secret) { + if (strcmp(key, "hmac-secret") == 0) { DBG_MSG("hmac-secret found\n"); - ga->parsedParams |= PARAM_hmacSecret; + ga->ext_hmac_secret_pin_protocol = 1; // pinUvAuthProtocol(0x04): (optional) as selected when getting the shared secret. CTAP2.1 platforms MUST include this parameter if the value of pinUvAuthProtocol is not 1. if (cbor_value_get_type(&map) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; size_t hmac_map_length; CborValue hmac_map; @@ -406,62 +464,76 @@ uint8_t parse_ga_extensions(CTAP_getAssertion *ga, CborValue *val) { CHECK_CBOR_RET(ret); enum { GA_HS_MAP_ENTRY_NONE = 0, - GA_HS_MAP_ENTRY_keyAgreement = 0b001, - GA_HS_MAP_ENTRY_saltEnc = 0b010, - GA_HS_MAP_ENTRY_saltAuth = 0b100, + GA_HS_MAP_ENTRY_KEY_AGREEMENT = 0b001, + GA_HS_MAP_ENTRY_SALT_ENC = 0b010, + GA_HS_MAP_ENTRY_SALT_AUTH = 0b100, GA_HS_MAP_ENTRY_ALL_REQUIRED = 0b111, } map_has_entry = GA_HS_MAP_ENTRY_NONE; - if (hmac_map_length < 3) return CTAP2_ERR_MISSING_PARAMETER; for (size_t j = 0; j < hmac_map_length; ++j) { if (cbor_value_get_type(&hmac_map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - size_t len; int hmac_key; ret = cbor_value_get_int_checked(&hmac_map, &hmac_key); CHECK_CBOR_RET(ret); ret = cbor_value_advance(&hmac_map); CHECK_CBOR_RET(ret); switch (hmac_key) { - case HMAC_SECRET_keyAgreement: - ret = parse_cose_key(&hmac_map, ga->hmacSecretKeyAgreement); - CHECK_CBOR_RET(ret); - map_has_entry |= GA_HS_MAP_ENTRY_keyAgreement; - DBG_MSG("keyAgreement: "); - PRINT_HEX(ga->hmacSecretKeyAgreement, 64); - break; - case HMAC_SECRET_saltEnc: - if (cbor_value_get_type(&hmac_map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = sizeof(ga->hmacSecretSaltEnc); - ret = cbor_value_copy_byte_string(&hmac_map, ga->hmacSecretSaltEnc, &len, NULL); - if (ret == CborErrorOutOfMemory) return CTAP1_ERR_INVALID_LENGTH; - CHECK_CBOR_RET(ret); - if (len != HMAC_SECRET_SALT_SIZE && len != HMAC_SECRET_SALT_SIZE / 2) return CTAP1_ERR_INVALID_LENGTH; - ga->hmacSecretSaltLen = len; - map_has_entry |= GA_HS_MAP_ENTRY_saltEnc; - DBG_MSG("saltEnc: "); - PRINT_HEX(ga->hmacSecretSaltEnc, ga->hmacSecretSaltLen); - break; - case HMAC_SECRET_saltAuth: - if (cbor_value_get_type(&hmac_map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = sizeof(ga->hmacSecretSaltAuth); - ret = cbor_value_copy_byte_string(&hmac_map, ga->hmacSecretSaltAuth, &len, NULL); - CHECK_CBOR_RET(ret); - if (len != HMAC_SECRET_SALT_AUTH_SIZE) return CTAP1_ERR_INVALID_LENGTH; - map_has_entry |= GA_HS_MAP_ENTRY_saltAuth; - DBG_MSG("saltAuth: "); - PRINT_HEX(ga->hmacSecretSaltAuth, 16); - break; - default: - // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding - DBG_MSG("Ignoring unsupported entry %0x\n", hmac_key); - break; + case GA_REQ_HMAC_SECRET_KEY_AGREEMENT: + ret = parse_cose_key(&hmac_map, ga->ext_hmac_secret_key_agreement); + CHECK_CBOR_RET(ret); + map_has_entry |= GA_HS_MAP_ENTRY_KEY_AGREEMENT; + DBG_MSG("key_agreement: "); + PRINT_HEX(ga->ext_hmac_secret_key_agreement, 64); + break; + case GA_REQ_HMAC_SECRET_SALT_ENC: + if (cbor_value_get_type(&hmac_map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + len = sizeof(ga->ext_hmac_secret_salt_enc); + ret = cbor_value_copy_byte_string(&hmac_map, ga->ext_hmac_secret_salt_enc, &len, NULL); + if (ret == CborErrorOutOfMemory) return CTAP1_ERR_INVALID_LENGTH; + CHECK_CBOR_RET(ret); + if (len != HMAC_SECRET_SALT_SIZE && len != HMAC_SECRET_SALT_SIZE / 2) return CTAP1_ERR_INVALID_LENGTH; + ga->ext_hmac_secret_salt_len = len; + map_has_entry |= GA_HS_MAP_ENTRY_SALT_ENC; + DBG_MSG("salt_enc: "); + PRINT_HEX(ga->ext_hmac_secret_salt_enc, ga->ext_hmac_secret_salt_len); + break; + case GA_REQ_HMAC_SECRET_SALT_AUTH: + if (cbor_value_get_type(&hmac_map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + len = sizeof(ga->ext_hmac_secret_salt_auth); + ret = cbor_value_copy_byte_string(&hmac_map, ga->ext_hmac_secret_salt_auth, &len, NULL); + CHECK_CBOR_RET(ret); + if (len != HMAC_SECRET_SALT_AUTH_SIZE) return CTAP1_ERR_INVALID_LENGTH; + map_has_entry |= GA_HS_MAP_ENTRY_SALT_AUTH; + DBG_MSG("salt_auth: "); + PRINT_HEX(ga->ext_hmac_secret_salt_auth, 16); + break; + case GA_REQ_HMAC_SECRET_PIN_PROTOCOL: + if (cbor_value_get_type(&hmac_map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&hmac_map, &tmp); + CHECK_CBOR_RET(ret); + ga->ext_hmac_secret_pin_protocol = tmp; + DBG_MSG("pin_protocol: %d\n", tmp); + break; + default: + DBG_MSG("Ignoring unsupported entry %0x\n", hmac_key); + break; } ret = cbor_value_advance(&hmac_map); CHECK_CBOR_RET(ret); } if ((map_has_entry & GA_HS_MAP_ENTRY_ALL_REQUIRED) != GA_HS_MAP_ENTRY_ALL_REQUIRED) return CTAP2_ERR_MISSING_PARAMETER; - } else { - DBG_MSG("ignoring option specified\n"); + ga->parsed_params |= PARAM_HMAC_SECRET; + } else if (strcmp(key, "credBlob") == 0) { + if (cbor_value_get_type(&map) != CborBooleanType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_boolean(&map, &ga->ext_cred_blob); + CHECK_CBOR_RET(ret); + DBG_MSG("credBlob: %d\n", ga->ext_cred_blob); + } else if (strcmp(key, "largeBlobKey") == 0) { + if (cbor_value_get_type(&map) != CborBooleanType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_boolean(&map, &ga->ext_large_blob_key); + CHECK_CBOR_RET(ret); + DBG_MSG("largeBlobKey: %d\n", ga->ext_large_blob_key); + if (!ga->ext_large_blob_key) return CTAP2_ERR_INVALID_OPTION; } ret = cbor_value_advance(&map); @@ -470,14 +542,75 @@ uint8_t parse_ga_extensions(CTAP_getAssertion *ga, CborValue *val) { return 0; } -uint8_t parse_make_credential(CborParser *parser, CTAP_makeCredential *mc, const uint8_t *buf, size_t len) { +uint8_t parse_cm_params(CTAP_credential_management *cm, CborValue *val, size_t *total_length) { + *total_length = 0; + if (cbor_value_get_type(val) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + size_t map_length, len; + CborValue map; + int key; + int ret = cbor_value_enter_container(val, &map); + CHECK_CBOR_RET(ret); + ret = cbor_value_get_map_length(val, &map_length); + CHECK_CBOR_RET(ret); + + for (size_t i = 0; i < map_length; ++i) { + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + ret = cbor_value_advance(&map); + CHECK_CBOR_RET(ret); + + switch (key) { + case CM_PARAM_RP_ID_HASH: + DBG_MSG("rp_id_hash found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &len); + CHECK_CBOR_RET(ret); + if (len != SHA256_DIGEST_LENGTH) return CTAP2_ERR_INVALID_CBOR; + ret = cbor_value_copy_byte_string(&map, cm->rp_id_hash, &len, NULL); + CHECK_CBOR_RET(ret); + cm->parsed_params |= PARAM_RP; + break; + + case CM_PARAM_CREDENTIAL_ID: + DBG_MSG("credential_id found\n"); + ret = parse_credential_descriptor(&map, (uint8_t *) &cm->credential_id); + CHECK_CBOR_RET(ret); + cm->parsed_params |= PARAM_CREDENTIAL_ID; + break; + + case CM_PARAM_USER: + DBG_MSG("user found\n"); + ret = parse_user(&cm->user, &map); + CHECK_CBOR_RET(ret); + cm->parsed_params |= PARAM_USER; + break; + + default: + DBG_MSG("Unknown key: %d\n", key); + break; + } + + ret = cbor_value_advance(&map); + CHECK_CBOR_RET(ret); + } + + *total_length = map.source.ptr - val->source.ptr; + return 0; +} + +uint8_t parse_make_credential(CborParser *parser, CTAP_make_credential *mc, const uint8_t *buf, size_t len) { CborValue it, map; size_t map_length; - int key, pinProtocol; - uint8_t tmp_up; - memset(mc, 0, sizeof(CTAP_makeCredential)); + int key, pin_uv_auth_protocol; + memset(mc, 0, sizeof(CTAP_make_credential)); - int ret = cbor_parser_init(buf, len, CborValidateCanonicalFormat, parser, &it); + // options are absent by default + mc->options.rk = OPTION_ABSENT; + mc->options.uv = OPTION_ABSENT; + mc->options.up = OPTION_ABSENT; + + int ret = cbor_parser_init(buf, len, 0, parser, &it); CHECK_CBOR_RET(ret); if (cbor_value_get_type(&it) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; ret = cbor_value_enter_container(&it, &map); @@ -493,117 +626,137 @@ uint8_t parse_make_credential(CborParser *parser, CTAP_makeCredential *mc, const CHECK_CBOR_RET(ret); switch (key) { - case MC_clientDataHash: - DBG_MSG("clientDataHash found\n"); - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = CLIENT_DATA_HASH_SIZE; - ret = cbor_value_copy_byte_string(&map, mc->clientDataHash, &len, NULL); - CHECK_CBOR_RET(ret); - if (len != CLIENT_DATA_HASH_SIZE) return CTAP2_ERR_INVALID_CBOR; - DBG_MSG("clientDataHash: "); - PRINT_HEX(mc->clientDataHash, len); - mc->parsedParams |= PARAM_clientDataHash; - break; - - case MC_rp: - DBG_MSG("rpId found\n"); - ret = parse_rp(mc->rpIdHash, &map); - CHECK_PARSER_RET(ret); - DBG_MSG("rpIdHash: "); - PRINT_HEX(mc->rpIdHash, len); - mc->parsedParams |= PARAM_rpId; - break; - - case MC_user: - DBG_MSG("user found\n"); - ret = parse_user(&mc->user, &map); - CHECK_PARSER_RET(ret); - mc->parsedParams |= PARAM_user; - break; - - case MC_pubKeyCredParams: - DBG_MSG("pubKeyCredParams found\n"); - ret = parse_verify_pub_key_cred_params(&map, &mc->alg_type); - CHECK_PARSER_RET(ret); - if (mc->alg_type == COSE_ALG_ES256) DBG_MSG("EcDSA found\n"); - else if (mc->alg_type == COSE_ALG_EDDSA) DBG_MSG("EdDSA found\n"); - else DBG_MSG("Found other algorithm\n"); - mc->parsedParams |= PARAM_pubKeyCredParams; - break; - - case MC_excludeList: - DBG_MSG("excludeList found\n"); - ret = parse_public_key_credential_list(&map); - CHECK_PARSER_RET(ret); - ret = cbor_value_enter_container(&map, &mc->excludeList); - CHECK_CBOR_RET(ret); - ret = cbor_value_get_array_length(&map, &mc->excludeListSize); - CHECK_CBOR_RET(ret); - DBG_MSG("excludeList size: %d\n", (int)mc->excludeListSize); - break; - - case MC_extensions: - DBG_MSG("extensions found\n"); - ret = parse_mc_extensions(&mc->extension_hmac_secret, &map); - CHECK_PARSER_RET(ret); - mc->parsedParams |= PARAM_extensions; - break; - - case MC_options: - DBG_MSG("options found\n"); - tmp_up = false; - ret = parse_options(&mc->rk, &mc->uv, &tmp_up, &map); - CHECK_PARSER_RET(ret); - // required by FIDO Conformance Tool - if (tmp_up) return CTAP2_ERR_INVALID_OPTION; - mc->parsedParams |= PARAM_options; - break; - - case MC_pinAuth: - DBG_MSG("pinAuth found\n"); - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_string_length(&map, &mc->pinAuthLength); - CHECK_CBOR_RET(ret); - if (mc->pinAuthLength != 0 && mc->pinAuthLength != PIN_AUTH_SIZE) return CTAP2_ERR_PIN_AUTH_INVALID; - ret = cbor_value_copy_byte_string(&map, mc->pinAuth, &mc->pinAuthLength, NULL); - CHECK_CBOR_RET(ret); - DBG_MSG("pinAuth: "); - PRINT_HEX(mc->pinAuth, mc->pinAuthLength); - mc->parsedParams |= PARAM_pinAuth; - break; + case MC_REQ_CLIENT_DATA_HASH: + DBG_MSG("client_data_hash found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + len = CLIENT_DATA_HASH_SIZE; + ret = cbor_value_copy_byte_string(&map, mc->client_data_hash, &len, NULL); + CHECK_CBOR_RET(ret); + if (len != CLIENT_DATA_HASH_SIZE) return CTAP2_ERR_INVALID_CBOR; + DBG_MSG("client_data_hash: "); + PRINT_HEX(mc->client_data_hash, len); + mc->parsed_params |= PARAM_CLIENT_DATA_HASH; + break; + + case MC_REQ_RP: + DBG_MSG("rp_id found\n"); + ret = parse_rp(mc, &map); + CHECK_PARSER_RET(ret); + DBG_MSG("rp_id_hash: "); + PRINT_HEX(mc->rp_id_hash, len); + mc->parsed_params |= PARAM_RP; + break; + + case MC_REQ_USER: + DBG_MSG("user found\n"); + ret = parse_user(&mc->user, &map); + CHECK_PARSER_RET(ret); + mc->parsed_params |= PARAM_USER; + break; + + case MC_REQ_PUB_KEY_CRED_PARAMS: + DBG_MSG("pubKeyCredParams found\n"); + ret = parse_verify_pub_key_cred_params(&map, &mc->alg_type); + CHECK_PARSER_RET(ret); + if (mc->alg_type == COSE_ALG_ES256) DBG_MSG("EcDSA found\n"); + else if (mc->alg_type == COSE_ALG_EDDSA) DBG_MSG("EdDSA found\n"); + else + DBG_MSG("Found other algorithm\n"); + mc->parsed_params |= PARAM_PUB_KEY_CRED_PARAMS; + break; + + case MC_REQ_EXCLUDE_LIST: + DBG_MSG("exclude_list found\n"); + ret = parse_public_key_credential_list(&map); + CHECK_PARSER_RET(ret); + ret = cbor_value_enter_container(&map, &mc->exclude_list); + CHECK_CBOR_RET(ret); + ret = cbor_value_get_array_length(&map, &mc->exclude_list_size); + CHECK_CBOR_RET(ret); + DBG_MSG("exclude_list size: %d\n", (int) mc->exclude_list_size); + break; + + case MC_REQ_EXTENSIONS: + DBG_MSG("extensions found\n"); + ret = parse_mc_extensions(mc, &map); + CHECK_PARSER_RET(ret); + mc->parsed_params |= PARAM_EXTENSIONS; + break; + + case MC_REQ_OPTIONS: + DBG_MSG("options found\n"); + ret = parse_options(&mc->options, &map); + CHECK_PARSER_RET(ret); + mc->parsed_params |= PARAM_OPTIONS; + break; + + case MC_REQ_PIN_UV_AUTH_PARAM: + DBG_MSG("pin_uv_auth_param found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &mc->pin_uv_auth_param_len); + CHECK_CBOR_RET(ret); + if (mc->pin_uv_auth_param_len > SHA256_DIGEST_LENGTH) { + DBG_MSG("pin_uv_auth_param is too long\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + if (mc->pin_uv_auth_param_len > 0) { + ret = cbor_value_copy_byte_string(&map, mc->pin_uv_auth_param, &mc->pin_uv_auth_param_len, NULL); + CHECK_CBOR_RET(ret); + DBG_MSG("pin_uv_auth_param: "); + PRINT_HEX(mc->pin_uv_auth_param, mc->pin_uv_auth_param_len); + } + mc->parsed_params |= PARAM_PIN_UV_AUTH_PARAM; + break; - case MC_pinProtocol: - DBG_MSG("pinProtocol found\n"); - if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_int_checked(&map, &pinProtocol); - CHECK_CBOR_RET(ret); - DBG_MSG("pinProtocol: %d\n", pinProtocol); - if (pinProtocol != 1) return CTAP2_ERR_PIN_AUTH_INVALID; - mc->parsedParams |= PARAM_pinProtocol; - break; - - default: - DBG_MSG("Unknown key: %d\n", key); - break; + case MC_REQ_PIN_PROTOCOL: + DBG_MSG("pin_uv_auth_protocol found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &pin_uv_auth_protocol); + CHECK_CBOR_RET(ret); + DBG_MSG("pin_uv_auth_protocol: %d\n", pin_uv_auth_protocol); + if (pin_uv_auth_protocol != 1 && pin_uv_auth_protocol != 2) { + DBG_MSG("Unknown pin_uv_auth_protocol\n"); + return CTAP1_ERR_INVALID_PARAMETER; + } + mc->pin_uv_auth_protocol = pin_uv_auth_protocol; + mc->parsed_params |= PARAM_PIN_UV_AUTH_PROTOCOL; + break; + + case MC_REQ_ENTERPRISE_ATTESTATION: + DBG_MSG("enterpriseAttestation found\n"); + mc->parsed_params |= PARAM_ENTERPRISE_ATTESTATION; + // TODO: parse enterpriseAttestation + break; + + default: + DBG_MSG("Unknown key: %d\n", key); + break; } ret = cbor_value_advance(&map); CHECK_CBOR_RET(ret); } - if ((mc->parsedParams & MC_requiredMask) != MC_requiredMask) return CTAP2_ERR_MISSING_PARAMETER; + if ((mc->parsed_params & MC_REQUIRED_MASK) != MC_REQUIRED_MASK) { + DBG_MSG("Missing required params\n"); + return CTAP2_ERR_MISSING_PARAMETER; + } return 0; } -uint8_t parse_get_assertion(CborParser *parser, CTAP_getAssertion *ga, const uint8_t *buf, size_t len) { +uint8_t parse_get_assertion(CborParser *parser, CTAP_get_assertion *ga, const uint8_t *buf, size_t len) { CborValue it, map; size_t map_length; - int key, pinProtocol; + int key, pin_uv_auth_protocol; char domain[DOMAIN_NAME_MAX_SIZE]; - memset(ga, 0, sizeof(CTAP_getAssertion)); - ga->up = 1; + memset(ga, 0, sizeof(CTAP_get_assertion)); + + // options are absent by default + ga->options.rk = OPTION_ABSENT; + ga->options.uv = OPTION_ABSENT; + ga->options.up = OPTION_ABSENT; - int ret = cbor_parser_init(buf, len, CborValidateCanonicalFormat, parser, &it); + int ret = cbor_parser_init(buf, len, 0, parser, &it); CHECK_CBOR_RET(ret); if (cbor_value_get_type(&it) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; ret = cbor_value_enter_container(&it, &map); @@ -619,98 +772,106 @@ uint8_t parse_get_assertion(CborParser *parser, CTAP_getAssertion *ga, const uin CHECK_CBOR_RET(ret); switch (key) { - case GA_rpId: - DBG_MSG("rpId found\n"); - if (cbor_value_get_type(&map) != CborTextStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = DOMAIN_NAME_MAX_SIZE; - ret = cbor_value_copy_text_string(&map, domain, &len, NULL); - CHECK_CBOR_RET(ret); - domain[DOMAIN_NAME_MAX_SIZE - 1] = 0; - DBG_MSG("rpId: %s; hash: ", domain); - sha256_raw((uint8_t *)domain, len, ga->rpIdHash); - PRINT_HEX(ga->rpIdHash, SHA256_DIGEST_LENGTH); - ga->parsedParams |= PARAM_rpId; - break; - - case GA_clientDataHash: - DBG_MSG("clientDataHash found\n"); - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - len = CLIENT_DATA_HASH_SIZE; - ret = cbor_value_copy_byte_string(&map, ga->clientDataHash, &len, NULL); - CHECK_CBOR_RET(ret); - if (len != CLIENT_DATA_HASH_SIZE) return CTAP2_ERR_INVALID_CBOR; - DBG_MSG("clientDataHash: "); - PRINT_HEX(ga->clientDataHash, len); - ga->parsedParams |= PARAM_clientDataHash; - break; - - case GA_allowList: - DBG_MSG("allowList found\n"); - ret = parse_public_key_credential_list(&map); - CHECK_PARSER_RET(ret); - ret = cbor_value_enter_container(&map, &ga->allowList); - CHECK_CBOR_RET(ret); - ret = cbor_value_get_array_length(&map, &ga->allowListSize); - CHECK_CBOR_RET(ret); - DBG_MSG("allowList size: %d\n", (int)ga->allowListSize); - break; - - case GA_extensions: - DBG_MSG("extensions found\n"); - ret = parse_ga_extensions(ga, &map); - CHECK_PARSER_RET(ret); - break; - - case GA_options: - DBG_MSG("options found\n"); - ret = parse_options(NULL, &ga->uv, &ga->up, &map); - CHECK_PARSER_RET(ret); - ga->parsedParams |= PARAM_options; - break; - - case GA_pinAuth: - DBG_MSG("pinAuth found\n"); - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_string_length(&map, &ga->pinAuthLength); - CHECK_CBOR_RET(ret); - if (ga->pinAuthLength != 0 && ga->pinAuthLength != PIN_AUTH_SIZE) return CTAP2_ERR_PIN_AUTH_INVALID; - ret = cbor_value_copy_byte_string(&map, ga->pinAuth, &ga->pinAuthLength, NULL); - CHECK_CBOR_RET(ret); - DBG_MSG("pinAuth: "); - PRINT_HEX(ga->pinAuth, ga->pinAuthLength); - ga->parsedParams |= PARAM_pinAuth; - break; + case GA_REQ_RP_ID: + DBG_MSG("rp_id found\n"); + if (cbor_value_get_type(&map) != CborTextStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + len = DOMAIN_NAME_MAX_SIZE; + ret = cbor_value_copy_text_string(&map, domain, &len, NULL); + CHECK_CBOR_RET(ret); + domain[DOMAIN_NAME_MAX_SIZE - 1] = 0; + DBG_MSG("rp_id: %s; hash: ", domain); + sha256_raw((uint8_t *) domain, len, ga->rp_id_hash); + PRINT_HEX(ga->rp_id_hash, SHA256_DIGEST_LENGTH); + ga->parsed_params |= PARAM_RP; + break; + + case GA_REQ_CLIENT_DATA_HASH: + DBG_MSG("client_data_hash found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + len = CLIENT_DATA_HASH_SIZE; + ret = cbor_value_copy_byte_string(&map, ga->client_data_hash, &len, NULL); + CHECK_CBOR_RET(ret); + if (len != CLIENT_DATA_HASH_SIZE) return CTAP2_ERR_INVALID_CBOR; + DBG_MSG("client_data_hash: "); + PRINT_HEX(ga->client_data_hash, len); + ga->parsed_params |= PARAM_CLIENT_DATA_HASH; + break; + + case GA_REQ_ALLOW_LIST: + DBG_MSG("allow_list found\n"); + ret = parse_public_key_credential_list(&map); + CHECK_PARSER_RET(ret); + ret = cbor_value_enter_container(&map, &ga->allow_list); + CHECK_CBOR_RET(ret); + ret = cbor_value_get_array_length(&map, &ga->allow_list_size); + CHECK_CBOR_RET(ret); + DBG_MSG("allow_list size: %d\n", (int) ga->allow_list_size); + break; + + case GA_REQ_EXTENSIONS: + DBG_MSG("extensions found\n"); + ret = parse_ga_extensions(ga, &map); + CHECK_PARSER_RET(ret); + break; + + case GA_REQ_OPTIONS: + DBG_MSG("options found\n"); + ret = parse_options(&ga->options, &map); + CHECK_PARSER_RET(ret); + ga->parsed_params |= PARAM_OPTIONS; + break; + + case GA_REQ_PIN_UV_AUTH_PARAM: + DBG_MSG("pin_uv_auth_param found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &ga->pin_uv_auth_param_len); + CHECK_CBOR_RET(ret); + if (ga->pin_uv_auth_param_len > SHA256_DIGEST_LENGTH) + return CTAP2_ERR_PIN_AUTH_INVALID; + if (ga->pin_uv_auth_param_len > 0) { + ret = cbor_value_copy_byte_string(&map, ga->pin_uv_auth_param, &ga->pin_uv_auth_param_len, NULL); + CHECK_CBOR_RET(ret); + DBG_MSG("pin_uv_auth_param: "); + PRINT_HEX(ga->pin_uv_auth_param, ga->pin_uv_auth_param_len); + } + ga->parsed_params |= PARAM_PIN_UV_AUTH_PARAM; + break; - case GA_pinProtocol: - DBG_MSG("pinProtocol found\n"); - if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_int_checked(&map, &pinProtocol); - CHECK_CBOR_RET(ret); - DBG_MSG("pinProtocol: %d\n", pinProtocol); - if (pinProtocol != 1) return CTAP2_ERR_PIN_AUTH_INVALID; - ga->parsedParams |= PARAM_pinProtocol; - break; - - default: - DBG_MSG("Unknown key: %d\n", key); - break; + case GA_REQ_PIN_UV_AUTH_PROTOCOL: + DBG_MSG("pin_uv_auth_protocol found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &pin_uv_auth_protocol); + CHECK_CBOR_RET(ret); + DBG_MSG("pin_uv_auth_protocol: %d\n", pin_uv_auth_protocol); + if (pin_uv_auth_protocol != 1 && pin_uv_auth_protocol != 2) { + DBG_MSG("Unknown pin_uv_auth_protocol\n"); + return CTAP1_ERR_INVALID_PARAMETER; + } + ga->pin_uv_auth_protocol = pin_uv_auth_protocol; + ga->parsed_params |= PARAM_PIN_UV_AUTH_PROTOCOL; + break; + + default: + DBG_MSG("Unknown key: %d\n", key); + break; } ret = cbor_value_advance(&map); CHECK_CBOR_RET(ret); } - if ((ga->parsedParams & GA_requiredMask) != GA_requiredMask) return CTAP2_ERR_MISSING_PARAMETER; + if ((ga->parsed_params & GA_REQUIRED_MASK) != GA_REQUIRED_MASK) return CTAP2_ERR_MISSING_PARAMETER; return 0; } -uint8_t parse_client_pin(CborParser *parser, CTAP_clientPin *cp, const uint8_t *buf, size_t len) { +uint8_t parse_client_pin(CborParser *parser, CTAP_client_pin *cp, const uint8_t *buf, size_t len) { CborValue it, map; size_t map_length; - int key, pinProtocol; - memset(cp, 0, sizeof(CTAP_clientPin)); + int key; + char domain[DOMAIN_NAME_MAX_SIZE + 1]; + memset(cp, 0, sizeof(CTAP_client_pin)); - int ret = cbor_parser_init(buf, len, CborValidateCanonicalFormat, parser, &it); + int ret = cbor_parser_init(buf, len, 0, parser, &it); CHECK_CBOR_RET(ret); if (cbor_value_get_type(&it) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; ret = cbor_value_enter_container(&it, &map); @@ -726,86 +887,381 @@ uint8_t parse_client_pin(CborParser *parser, CTAP_clientPin *cp, const uint8_t * CHECK_CBOR_RET(ret); switch (key) { - case CP_pinProtocol: - DBG_MSG("pinProtocol found\n"); - if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_int_checked(&map, &pinProtocol); - CHECK_CBOR_RET(ret); - DBG_MSG("pinProtocol: %d\n", pinProtocol); - if (pinProtocol != 1) return CTAP2_ERR_PIN_AUTH_INVALID; - cp->parsedParams |= PARAM_pinProtocol; - break; + case CP_REQ_PIN_UV_AUTH_PROTOCOL: + DBG_MSG("pinProtocol found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + DBG_MSG("pinProtocol: %d\n", key); + if (key != 1 && key != 2) { + ERR_MSG("Invalid pinProtocol\n"); + return CTAP1_ERR_INVALID_PARAMETER; + } + cp->pin_uv_auth_protocol = key; + cp->parsed_params |= PARAM_PIN_UV_AUTH_PROTOCOL; + break; + + case CP_REQ_SUB_COMMAND: + DBG_MSG("sub_command found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + cp->sub_command = key; + DBG_MSG("sub_command: %d\n", cp->sub_command); + cp->parsed_params |= PARAM_SUB_COMMAND; + break; + + case CP_REQ_KEY_AGREEMENT: + DBG_MSG("key_agreement found\n"); + ret = parse_cose_key(&map, cp->key_agreement); + CHECK_PARSER_RET(ret); + DBG_MSG("key_agreement: "); + PRINT_HEX(cp->key_agreement, PUB_KEY_SIZE); + cp->parsed_params |= PARAM_KEY_AGREEMENT; + break; + + case CP_REQ_PIN_UV_AUTH_PARAM: + DBG_MSG("pin_uv_auth_param found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &len); + CHECK_CBOR_RET(ret); + if (len == 0 || len > SHA256_DIGEST_LENGTH) return CTAP2_ERR_PIN_AUTH_INVALID; + ret = cbor_value_copy_byte_string(&map, cp->pin_uv_auth_param, &len, NULL); + CHECK_CBOR_RET(ret); + cp->parsed_params |= PARAM_PIN_UV_AUTH_PARAM; + break; - case CP_subCommand: - DBG_MSG("subCommand found\n"); - if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_int_checked(&map, &pinProtocol); // use pinProtocol as a buffer - CHECK_CBOR_RET(ret); - cp->subCommand = pinProtocol; - DBG_MSG("subCommand: %d\n", cp->subCommand); - cp->parsedParams |= PARAM_subCommand; - break; - - case CP_keyAgreement: - DBG_MSG("keyAgreement found\n"); - ret = parse_cose_key(&map, cp->keyAgreement); - CHECK_PARSER_RET(ret); - cp->parsedParams |= PARAM_keyAgreement; - break; - - case CP_pinAuth: - DBG_MSG("pinAuth found\n"); - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_string_length(&map, &len); - CHECK_CBOR_RET(ret); - if (len != PIN_AUTH_SIZE) return CTAP2_ERR_INVALID_CBOR; - ret = cbor_value_copy_byte_string(&map, cp->pinAuth, &len, NULL); - CHECK_CBOR_RET(ret); - cp->parsedParams |= PARAM_pinAuth; - break; + case CP_REQ_NEW_PIN_ENC: + DBG_MSG("new_pin_enc found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &len); + CHECK_CBOR_RET(ret); + if ((cp->pin_uv_auth_protocol == 1 && len != PIN_ENC_SIZE_P1) || + (cp->pin_uv_auth_protocol == 2 && len != PIN_ENC_SIZE_P2)) { + ERR_MSG("Invalid new_pin_enc length\n"); + return CTAP2_ERR_INVALID_CBOR; + } + ret = cbor_value_copy_byte_string(&map, cp->new_pin_enc, &len, NULL); + CHECK_CBOR_RET(ret); + DBG_MSG("new_pin_enc: "); + PRINT_HEX(cp->new_pin_enc, len); + cp->parsed_params |= PARAM_NEW_PIN_ENC; + break; + + case CP_REQ_PIN_HASH_ENC: + DBG_MSG("pin_hash_enc found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &len); + CHECK_CBOR_RET(ret); + if ((cp->pin_uv_auth_protocol == 1 && len != PIN_HASH_SIZE_P1) || + (cp->pin_uv_auth_protocol == 2 && len != PIN_HASH_SIZE_P2)) { + ERR_MSG("Invalid pin_hash_enc length\n"); + return CTAP2_ERR_INVALID_CBOR; + } + ret = cbor_value_copy_byte_string(&map, cp->pin_hash_enc, &len, NULL); + CHECK_CBOR_RET(ret); + cp->parsed_params |= PARAM_PIN_HASH_ENC; + break; - case CP_newPinEnc: - DBG_MSG("newPinEnc found\n"); - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_string_length(&map, &len); - CHECK_CBOR_RET(ret); - if (len != MAX_PIN_SIZE + 1) return CTAP2_ERR_INVALID_CBOR; - ret = cbor_value_copy_byte_string(&map, cp->newPinEnc, &len, NULL); - CHECK_CBOR_RET(ret); - cp->parsedParams |= PARAM_newPinEnc; - break; + case CP_REQ_PERMISSIONS: + DBG_MSG("permissions found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + cp->permissions = key; + DBG_MSG("permissions: %d\n", cp->permissions); + if (cp->permissions == 0) { + ERR_MSG("Invalid permissions\n"); + return CTAP1_ERR_INVALID_PARAMETER; + } + if (cp->permissions & (CP_PERMISSION_BE | CP_PERMISSION_ACFG)) { + DBG_MSG("Unsupported permissions\n"); + return CTAP2_ERR_UNAUTHORIZED_PERMISSION; + } + cp->parsed_params |= PARAM_PERMISSIONS; + break; + + case CP_REQ_RP_ID: + DBG_MSG("rp id found\n"); + if (cbor_value_get_type(&map) != CborTextStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + len = DOMAIN_NAME_MAX_SIZE; + ret = cbor_value_copy_text_string(&map, domain, &len, NULL); + CHECK_CBOR_RET(ret); + domain[len] = 0; + DBG_MSG("rp_id: %s\n", domain); + sha256_raw((uint8_t *) domain, len, cp->rp_id_hash); + cp->parsed_params |= PARAM_RP; + break; + + default: + DBG_MSG("Unknown key: %d\n", key); + break; + } - case CP_pinHashEnc: - DBG_MSG("pinHashEnc found\n"); - if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; - ret = cbor_value_get_string_length(&map, &len); - CHECK_CBOR_RET(ret); - if (len != PIN_HASH_SIZE) return CTAP2_ERR_INVALID_CBOR; - ret = cbor_value_copy_byte_string(&map, cp->pinHashEnc, &len, NULL); - CHECK_CBOR_RET(ret); - cp->parsedParams |= PARAM_pinHashEnc; - break; + ret = cbor_value_advance(&map); + CHECK_CBOR_RET(ret); + } + + if ((cp->parsed_params & CP_REQUIRED_MASK) != CP_REQUIRED_MASK) return CTAP2_ERR_MISSING_PARAMETER; + + if (cp->sub_command == CP_CMD_GET_KEY_AGREEMENT && (cp->parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL) == 0) + return CTAP2_ERR_MISSING_PARAMETER; + + if (cp->sub_command == CP_CMD_SET_PIN && + ((cp->parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL) == 0 || + (cp->parsed_params & PARAM_KEY_AGREEMENT) == 0 || + (cp->parsed_params & PARAM_NEW_PIN_ENC) == 0 || + (cp->parsed_params & PARAM_PIN_UV_AUTH_PARAM) == 0)) + return CTAP2_ERR_MISSING_PARAMETER; + + if (cp->sub_command == CP_CMD_CHANGE_PIN && + ((cp->parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL) == 0 || + (cp->parsed_params & PARAM_KEY_AGREEMENT) == 0 || + (cp->parsed_params & PARAM_PIN_HASH_ENC) == 0 || + (cp->parsed_params & PARAM_NEW_PIN_ENC) == 0 || + (cp->parsed_params & PARAM_PIN_UV_AUTH_PARAM) == 0)) + return CTAP2_ERR_MISSING_PARAMETER; - default: - DBG_MSG("Unknown key: %d\n", key); - break; + if (cp->sub_command == CP_CMD_GET_PIN_TOKEN && + ((cp->parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL) == 0 || + (cp->parsed_params & PARAM_KEY_AGREEMENT) == 0 || + (cp->parsed_params & PARAM_PIN_HASH_ENC) == 0)) + return CTAP2_ERR_MISSING_PARAMETER; + if (cp->sub_command == CP_CMD_GET_PIN_TOKEN && + ((cp->parsed_params & PARAM_PERMISSIONS) != 0 || + (cp->parsed_params & PARAM_RP) != 0)) + return CTAP1_ERR_INVALID_PARAMETER; + + if (cp->sub_command == CP_CMD_GET_PIN_UV_AUTH_TOKEN_USING_PIN_WITH_PERMISSIONS && + ((cp->parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL) == 0 || + (cp->parsed_params & PARAM_KEY_AGREEMENT) == 0 || + (cp->parsed_params & PARAM_PIN_HASH_ENC) == 0 || + (cp->parsed_params & PARAM_PERMISSIONS) == 0)) + return CTAP2_ERR_MISSING_PARAMETER; + + return 0; +} + +uint8_t +parse_credential_management(CborParser *parser, CTAP_credential_management *cm, const uint8_t *buf, size_t len) { + CborValue it, map; + size_t map_length; + int key, tmp; + memset(cm, 0, sizeof(CTAP_credential_management)); + + int ret = cbor_parser_init(buf, len, 0, parser, &it); + CHECK_CBOR_RET(ret); + if (cbor_value_get_type(&it) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_enter_container(&it, &map); + CHECK_CBOR_RET(ret); + ret = cbor_value_get_map_length(&it, &map_length); + CHECK_CBOR_RET(ret); + + for (size_t i = 0; i < map_length; ++i) { + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + ret = cbor_value_advance(&map); + CHECK_CBOR_RET(ret); + + switch (key) { + case CM_REQ_SUB_COMMAND: + DBG_MSG("sub_command found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &tmp); + CHECK_CBOR_RET(ret); + cm->sub_command = tmp; + DBG_MSG("sub_command: %d\n", cm->sub_command); + cm->parsed_params |= PARAM_SUB_COMMAND; + break; + + case CM_REQ_SUB_COMMAND_PARAMS: + DBG_MSG("subCommandParams found\n"); + cm->sub_command_params_ptr = (uint8_t *) map.source.ptr; + ret = parse_cm_params(cm, &map, &cm->param_len); + DBG_MSG("sub_command_params (%zu): ", cm->param_len); + PRINT_HEX(cm->sub_command_params_ptr, cm->param_len); + CHECK_CBOR_RET(ret); + break; + + case CM_REQ_PIN_UV_AUTH_PROTOCOL: + DBG_MSG("pin_uv_auth_protocol found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &tmp); + CHECK_CBOR_RET(ret); + DBG_MSG("pin_uv_auth_protocol: %d\n", tmp); + if (tmp != 1 && tmp != 2) return CTAP1_ERR_INVALID_PARAMETER; + cm->pin_uv_auth_protocol = tmp; + cm->parsed_params |= PARAM_PIN_UV_AUTH_PROTOCOL; + break; + + case CM_REQ_PIN_UV_AUTH_PARAM: + DBG_MSG("pin_uv_auth_param found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &len); + CHECK_CBOR_RET(ret); + if (len == 0 || len > SHA256_DIGEST_LENGTH) return CTAP2_ERR_PIN_AUTH_INVALID; + ret = cbor_value_copy_byte_string(&map, cm->pin_uv_auth_param, &len, NULL); + CHECK_CBOR_RET(ret); + PRINT_HEX(cm->pin_uv_auth_param, len); + cm->parsed_params |= PARAM_PIN_UV_AUTH_PARAM; + break; + + default: + DBG_MSG("Unknown key: %d\n", key); + break; } ret = cbor_value_advance(&map); CHECK_CBOR_RET(ret); } - if ((cp->parsedParams & CP_requiredMask) != CP_requiredMask) return CTAP2_ERR_MISSING_PARAMETER; - if (cp->subCommand == CP_cmdSetPin && - ((cp->parsedParams & PARAM_keyAgreement) == 0 || (cp->parsedParams & PARAM_newPinEnc) == 0)) + if ((cm->parsed_params & CM_REQUIRED_MASK) != CM_REQUIRED_MASK) return CTAP2_ERR_MISSING_PARAMETER; + + if ((cm->sub_command == CM_CMD_GET_CREDS_METADATA || + cm->sub_command == CM_CMD_ENUMERATE_RPS_BEGIN || + cm->sub_command == CM_CMD_ENUMERATE_CREDENTIALS_BEGIN || + cm->sub_command == CM_CMD_DELETE_CREDENTIAL || + cm->sub_command == CM_CMD_UPDATE_USER_INFORMATION) && + (cm->parsed_params & PARAM_PIN_UV_AUTH_PARAM) == 0) + return CTAP2_ERR_PUAT_REQUIRED; // See Section 6.8.2, 6.8.3, 6.8.4, 6.8.5, 6.8.6 + if ((cm->sub_command == CM_CMD_GET_CREDS_METADATA || + cm->sub_command == CM_CMD_ENUMERATE_RPS_BEGIN || + cm->sub_command == CM_CMD_ENUMERATE_CREDENTIALS_BEGIN || + cm->sub_command == CM_CMD_DELETE_CREDENTIAL || + cm->sub_command == CM_CMD_UPDATE_USER_INFORMATION) && + (cm->parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL) == 0) + return CTAP2_ERR_MISSING_PARAMETER; // See Section 6.8.2, 6.8.3, 6.8.4, 6.8.5, 6.8.6 + if (cm->sub_command == CM_CMD_ENUMERATE_CREDENTIALS_BEGIN && (cm->parsed_params & PARAM_RP) == 0) return CTAP2_ERR_MISSING_PARAMETER; - if (cp->subCommand == CP_cmdChangePin && - ((cp->parsedParams & PARAM_keyAgreement) == 0 || (cp->parsedParams & PARAM_pinHashEnc) == 0 || - (cp->parsedParams & PARAM_newPinEnc) == 0 || (cp->parsedParams & PARAM_pinAuth) == 0)) + if (cm->sub_command == CM_CMD_DELETE_CREDENTIAL && (cm->parsed_params & PARAM_CREDENTIAL_ID) == 0) return CTAP2_ERR_MISSING_PARAMETER; - if (cp->subCommand == CP_cmdGetPinToken && - ((cp->parsedParams & PARAM_keyAgreement) == 0 || (cp->parsedParams & PARAM_pinHashEnc) == 0)) + if (cm->sub_command == CM_CMD_UPDATE_USER_INFORMATION && (cm->parsed_params & (PARAM_USER|PARAM_CREDENTIAL_ID)) != (PARAM_USER|PARAM_CREDENTIAL_ID)) return CTAP2_ERR_MISSING_PARAMETER; return 0; } + +uint8_t parse_large_blobs(CborParser *parser, CTAP_large_blobs *lb, const uint8_t *buf, size_t len) { + CborValue it, map; + size_t map_length; + int key, tmp; + memset(lb, 0, sizeof(CTAP_large_blobs)); + + int ret = cbor_parser_init(buf, len, 0, parser, &it); + CHECK_CBOR_RET(ret); + if (cbor_value_get_type(&it) != CborMapType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_enter_container(&it, &map); + CHECK_CBOR_RET(ret); + ret = cbor_value_get_map_length(&it, &map_length); + CHECK_CBOR_RET(ret); + + for (size_t i = 0; i < map_length; ++i) { + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &key); + CHECK_CBOR_RET(ret); + ret = cbor_value_advance(&map); + CHECK_CBOR_RET(ret); + + switch (key) { + case LB_REQ_GET: + DBG_MSG("get found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &tmp); + CHECK_CBOR_RET(ret); + DBG_MSG("get: %d\n", tmp); + if (tmp < 0) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; // should be unsigned integer + if (tmp > UINT16_MAX) tmp = UINT16_MAX; + lb->get = tmp; + lb->parsed_params |= PARAM_GET; + break; + + case LB_REQ_SET: + DBG_MSG("set found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &lb->set_len); + CHECK_CBOR_RET(ret); + lb->set = (uint8_t *) map.source.ptr + 1; + if (lb->set_len >= 24) ++lb->set; + if (lb->set_len >= 256) ++lb->set; + DBG_MSG("set(%zuB): ", lb->set_len); + PRINT_HEX(lb->set, lb->set_len < 17 ? lb->set_len : 17); + lb->parsed_params |= PARAM_SET; + break; + + case LB_REQ_OFFSET: + DBG_MSG("offset found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &tmp); + CHECK_CBOR_RET(ret); + DBG_MSG("offset: %d\n", tmp); + if (tmp < 0) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; // should be unsigned integer + if (tmp > UINT16_MAX) tmp = UINT16_MAX; + lb->offset = tmp; + lb->parsed_params |= PARAM_OFFSET; + break; + + case LB_REQ_LENGTH: + DBG_MSG("length found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &tmp); + CHECK_CBOR_RET(ret); + DBG_MSG("length: %d\n", tmp); + if (tmp < 0) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; // should be unsigned integer + if (tmp > UINT16_MAX) tmp = UINT16_MAX; + lb->length = tmp; + lb->parsed_params |= PARAM_LENGTH; + break; + + case LB_REQ_PIN_UV_AUTH_PROTOCOL: + DBG_MSG("pin_uv_auth_protocol found\n"); + if (cbor_value_get_type(&map) != CborIntegerType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_int_checked(&map, &tmp); + CHECK_CBOR_RET(ret); + DBG_MSG("pin_uv_auth_protocol: %d\n", tmp); + if (tmp != 1 && tmp != 2) return CTAP1_ERR_INVALID_PARAMETER; + lb->pin_uv_auth_protocol = tmp; + lb->parsed_params |= PARAM_PIN_UV_AUTH_PROTOCOL; + break; + + case LB_REQ_PIN_UV_AUTH_PARAM: + DBG_MSG("pin_uv_auth_param found\n"); + if (cbor_value_get_type(&map) != CborByteStringType) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + ret = cbor_value_get_string_length(&map, &len); + CHECK_CBOR_RET(ret); + if (len == 0 || len > SHA256_DIGEST_LENGTH) return CTAP2_ERR_PIN_AUTH_INVALID; + ret = cbor_value_copy_byte_string(&map, lb->pin_uv_auth_param, &len, NULL); + CHECK_CBOR_RET(ret); + lb->parsed_params |= PARAM_PIN_UV_AUTH_PARAM; + break; + + default: + DBG_MSG("Unknown key: %d\n", key); + break; + } + + ret = cbor_value_advance(&map); + CHECK_CBOR_RET(ret); + } + + if (!(lb->parsed_params & PARAM_OFFSET)) return CTAP1_ERR_INVALID_PARAMETER; + if (!((lb->parsed_params & PARAM_GET) ^ (lb->parsed_params & PARAM_SET))) return CTAP1_ERR_INVALID_PARAMETER; + if (lb->parsed_params & PARAM_GET) { + if (lb->parsed_params & PARAM_LENGTH) return CTAP1_ERR_INVALID_PARAMETER; + if ((lb->parsed_params & PARAM_PIN_UV_AUTH_PARAM) || (lb->parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL)) + return CTAP1_ERR_INVALID_PARAMETER; + if (lb->get > MAX_FRAGMENT_LENGTH) return CTAP1_ERR_INVALID_LENGTH; + } + if (lb->parsed_params & PARAM_SET) { + if (lb->set_len > MAX_FRAGMENT_LENGTH) return CTAP1_ERR_INVALID_LENGTH; + if (lb->offset == 0) { + if (!(lb->parsed_params & PARAM_LENGTH)) return CTAP1_ERR_INVALID_PARAMETER; + if (lb->length > LARGE_BLOB_SIZE_LIMIT) return CTAP2_ERR_LARGE_BLOB_STORAGE_FULL; + if (lb->length < 17) return CTAP1_ERR_INVALID_PARAMETER; + } else { + if (lb->parsed_params & PARAM_LENGTH) return CTAP1_ERR_INVALID_PARAMETER; + } + } + + return 0; +} diff --git a/applets/ctap/ctap-parser.h b/applets/ctap/ctap-parser.h index edc7d39d..cbd986f5 100644 --- a/applets/ctap/ctap-parser.h +++ b/applets/ctap/ctap-parser.h @@ -6,15 +6,16 @@ #include #include -uint8_t parse_rp(uint8_t *rpIdHash, CborValue *val); -uint8_t parse_user(UserEntity *user, CborValue *val); +uint8_t parse_user(user_entity *user, CborValue *val); uint8_t parse_verify_pub_key_cred_params(CborValue *val, int32_t *alg_type); uint8_t parse_credential_descriptor(CborValue *arr, uint8_t *id); uint8_t parse_public_key_credential_list(CborValue *lst); -uint8_t parse_options(uint8_t *rk, uint8_t *uv, uint8_t *up, CborValue *val); +uint8_t parse_options(CTAP_options *options, CborValue *val); uint8_t parse_cose_key(CborValue *val, uint8_t *public_key); -uint8_t parse_make_credential(CborParser *parser, CTAP_makeCredential *mc, const uint8_t *buf, size_t len); -uint8_t parse_get_assertion(CborParser *parser, CTAP_getAssertion *ga, const uint8_t *buf, size_t len); -uint8_t parse_client_pin(CborParser *parser, CTAP_clientPin *cp, const uint8_t *buf, size_t len); +uint8_t parse_make_credential(CborParser *parser, CTAP_make_credential *mc, const uint8_t *buf, size_t len); +uint8_t parse_get_assertion(CborParser *parser, CTAP_get_assertion *ga, const uint8_t *buf, size_t len); +uint8_t parse_client_pin(CborParser *parser, CTAP_client_pin *cp, const uint8_t *buf, size_t len); +uint8_t parse_credential_management(CborParser *parser, CTAP_credential_management *cm, const uint8_t *buf, size_t len); +uint8_t parse_large_blobs(CborParser *parser, CTAP_large_blobs *lb, const uint8_t *buf, size_t len); #endif // CANOKEY_CORE_FIDO2_CTAP_PARSER_H_ diff --git a/applets/ctap/ctap.c b/applets/ctap/ctap.c index bf058ef8..6fff9aad 100644 --- a/applets/ctap/ctap.c +++ b/applets/ctap/ctap.c @@ -12,20 +12,20 @@ #include #include #include -#include #include #include #include #define CHECK_PARSER_RET(ret) \ do { \ - if ((ret) != 0) ERR_MSG("CHECK_PARSER_RET %#x\n", ret); \ - if ((ret) > 0) return ret; \ + if ((ret) != 0) ERR_MSG("CHECK_PARSER_RET %#x\n", ret); \ + if ((ret) > 0) return ret; \ } while (0) + #define CHECK_CBOR_RET(ret) \ do { \ - if ((ret) != 0) ERR_MSG("CHECK_CBOR_RET %#x\n", ret); \ - if ((ret) != 0) return CTAP2_ERR_INVALID_CBOR; \ + if ((ret) != 0) ERR_MSG("CHECK_CBOR_RET %#x\n", ret); \ + if ((ret) != 0) return CTAP2_ERR_INVALID_CBOR; \ } while (0) #define SET_RESP() \ @@ -36,39 +36,35 @@ *resp_len = 1; \ } while (0) -#define WAIT() \ +#define WAIT(timeout_response) \ do { \ if (is_nfc()) break; \ switch (wait_for_user_presence(WAIT_ENTRY_CTAPHID)) { \ case USER_PRESENCE_CANCEL: \ return CTAP2_ERR_KEEPALIVE_CANCEL; \ case USER_PRESENCE_TIMEOUT: \ - return CTAP2_ERR_USER_ACTION_TIMEOUT; \ + return timeout_response; \ } \ } while (0) static const uint8_t aaguid[] = {0x24, 0x4e, 0xb2, 0x9e, 0xe0, 0x90, 0x4e, 0x49, 0x81, 0xfe, 0x1f, 0x20, 0xf8, 0xd3, 0xb8, 0xf4}; -// pin related -static ecc_key_t key_agreement_key; -static uint8_t pin_token[PIN_TOKEN_SIZE]; -static uint8_t consecutive_pin_counter; -// assertion related -static uint8_t credential_list[MAX_RK_NUM], credential_numbers, credential_idx, last_cmd; + +// pin & command states +static uint8_t consecutive_pin_counter, last_cmd; uint8_t ctap_install(uint8_t reset) { consecutive_pin_counter = 3; - credential_numbers = 0; - credential_idx = 0; last_cmd = 0xff; - random_buffer(pin_token, sizeof(pin_token)); - if (ecc_generate(SECP256R1, &key_agreement_key) < 0) { - ERR_MSG("Key agreement generation failed\n"); - return CTAP2_ERR_UNHANDLED_REQUEST; + cp_initialize(); + if (!reset && get_file_size(CTAP_CERT_FILE) >= 0) { + DBG_MSG("CTAP initialized\n"); + return 0; } - if (!reset && get_file_size(CTAP_CERT_FILE) >= 0) return 0; uint8_t kh_key[KH_KEY_SIZE] = {0}; - if (write_file(RK_FILE, NULL, 0, 0, 1) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (write_file(DC_FILE, NULL, 0, 0, 1) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (write_attr(DC_FILE, DC_GENERAL_ATTR, kh_key, sizeof(CTAP_dc_general_attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (write_file(DC_META_FILE, NULL, 0, 0, 1) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; if (write_file(CTAP_CERT_FILE, NULL, 0, 0, 0) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; if (write_attr(CTAP_CERT_FILE, SIGN_CTR_ATTR, kh_key, 4) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; if (write_attr(CTAP_CERT_FILE, PIN_ATTR, NULL, 0) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; @@ -76,7 +72,12 @@ uint8_t ctap_install(uint8_t reset) { if (write_attr(CTAP_CERT_FILE, KH_KEY_ATTR, kh_key, sizeof(kh_key)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; random_buffer(kh_key, sizeof(kh_key)); if (write_attr(CTAP_CERT_FILE, HE_KEY_ATTR, kh_key, sizeof(kh_key)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + memcpy(kh_key, + (uint8_t[]) {0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, 0x6d, + 0x3c}, 17); + if (write_file(LB_FILE, kh_key, 0, 17, 1) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; memzero(kh_key, sizeof(kh_key)); + DBG_MSG("CTAP reset and initialized\n"); return 0; } @@ -147,171 +148,506 @@ static void build_ed25519_cose_key(uint8_t *data) { memmove(data + 10, data, 32); data[0] = 0xa4; - data[1] = 0x01; data[2] = 0x01; - data[3] = 0x03; data[4] = 0x27; - data[5] = 0x20; data[6] = 0x06; - data[7] = 0x21; data[8] = 0x58; data[9] = 0x20; + data[1] = 0x01; + data[2] = 0x01; + data[3] = 0x03; + data[4] = 0x27; + data[5] = 0x20; + data[6] = 0x06; + data[7] = 0x21; + data[8] = 0x58; + data[9] = 0x20; } -static uint8_t get_shared_secret(uint8_t *pub_key) { - int ret = ecdh(SECP256R1, key_agreement_key.pri, pub_key, pub_key); - if (ret < 0) return 1; - sha256_raw(pub_key, PRI_KEY_SIZE, pub_key); +int ctap_consistency_check(void) { + CTAP_dc_general_attr attr; + if (read_attr(DC_FILE, DC_GENERAL_ATTR, &attr, sizeof(attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (attr.pending_add || attr.pending_delete) { + DBG_MSG("Rolling back credential operations\n"); + if (get_file_size(DC_FILE) >= ((int) attr.index + 1) * (int) sizeof(CTAP_discoverable_credential)) { + CTAP_discoverable_credential dc; + if (read_file(DC_FILE, &dc, attr.index * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential)) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + if (!dc.deleted) { + // delete the credential that had been written + DBG_MSG("Delete cred at %hhu\n", attr.index); + dc.deleted = true; + if (write_file(DC_FILE, &dc, attr.index * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential), 0) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + } + } + // delete the meta then + int nr_rp = get_file_size(DC_META_FILE); + if (nr_rp < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + nr_rp /= sizeof(CTAP_rp_meta); + for (int i = 0; i < nr_rp; ++i) { + CTAP_rp_meta meta; + int size = read_file(DC_META_FILE, &meta, i * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if ((meta.slots & (1ull << attr.index)) != 0) { + DBG_MSG("Orig slot bitmap: 0x%llx\n", meta.slots); + meta.slots &= ~(1ull << attr.index); + DBG_MSG("New slot bitmap: 0x%llx\n", meta.slots); + size = write_file(DC_META_FILE, &meta, i * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta), 0); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + break; + } + } + if (attr.pending_delete) + attr.numbers--; + + attr.pending_add = 0; + attr.pending_delete = 0; + if (write_attr(DC_FILE, DC_GENERAL_ATTR, &attr, sizeof(attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + } return 0; } -uint8_t ctap_make_auth_data(uint8_t *rpIdHash, uint8_t *buf, uint8_t flags, uint8_t extensionSize, - const uint8_t *extension, size_t *len, int32_t alg_type) { +uint8_t ctap_make_auth_data(uint8_t *rp_id_hash, uint8_t *buf, uint8_t flags, const uint8_t *extension, + uint8_t extension_size, size_t *len, int32_t alg_type, bool dc, uint8_t cred_protect) { // See https://www.w3.org/TR/webauthn/#sec-authenticator-data // auth data is a byte string // -------------------------------------------------------------------------------- - // Name | Length | Description - // -----------|----------|--------------------------------------------------------- - // rpIdHash | 32 | SHA256 of rpId, we generate it outside of this function - // flags | 1 | 0: UP, 2: UV, 6: AT, 7: ED - // signCount | 4 | 32-bit endian number - // attCred | var | Exist iff in authenticatorMakeCredential request - // | | 16-byte aaguid - // | | 2-byte key handle length - // | | key handle - // | | public key (in COSE_key format) - // extension | var | IGNORE FOR NOW + // Name | Length | Description + // ------------|----------|--------------------------------------------------------- + // rp_id_hash | 32 | SHA256 of rp_id, we generate it outside this function + // flags | 1 | 0: UP, 2: UV, 6: AT, 7: ED + // sign_count | 4 | 32-bit endian number + // attCred | var | Exist iff in authenticatorMakeCredential request + // | | 16-byte aaguid + // | | 2-byte key handle length + // | | key handle + // | | public key (in COSE_key format) + // extension | var | Build outside // -------------------------------------------------------------------------------- size_t outLen = 37; // without attCred - CTAP_authData *ad = (CTAP_authData *)buf; + CTAP_auth_data *ad = (CTAP_auth_data *) buf; if (*len < outLen) return CTAP2_ERR_LIMIT_EXCEEDED; - memcpy(ad->rpIdHash, rpIdHash, sizeof(ad->rpIdHash)); + memcpy(ad->rp_id_hash, rp_id_hash, sizeof(ad->rp_id_hash)); ad->flags = flags; uint32_t ctr; - int ret = increase_counter(&ctr); - if (ret < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - ad->signCount = htobe32(ctr); + if (increase_counter(&ctr) < 0) { + DBG_MSG("Fail to increase the counter\n"); + return CTAP2_ERR_UNHANDLED_REQUEST; + } + ad->sign_count = htobe32(ctr); if (flags & FLAGS_AT) { - if (*len < outLen + sizeof(ad->at) - 1) return CTAP2_ERR_LIMIT_EXCEEDED; + if (*len < outLen + sizeof(ad->at) - 1) { + DBG_MSG("Attestation is too long\n"); + return CTAP2_ERR_LIMIT_EXCEEDED; + } + + // If no credProtect extension was included in the request the authenticator SHOULD use the default value of 1 for compatibility with CTAP2.0 platforms. + if (cred_protect == CRED_PROTECT_ABSENT) cred_protect = CRED_PROTECT_VERIFICATION_OPTIONAL; memcpy(ad->at.aaguid, aaguid, sizeof(aaguid)); - ad->at.credentialIdLength = htobe16(sizeof(CredentialId)); - memcpy(ad->at.credentialId.rpIdHash, rpIdHash, sizeof(ad->at.credentialId.rpIdHash)); - if (generate_key_handle(&ad->at.credentialId, ad->at.publicKey, alg_type) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + ad->at.credential_id_length = htobe16(sizeof(credential_id)); + memcpy(ad->at.credential_id.rp_id_hash, rp_id_hash, sizeof(ad->at.credential_id.rp_id_hash)); + if (generate_key_handle(&ad->at.credential_id, ad->at.public_key, alg_type, (uint8_t)dc, cred_protect) < 0) { + DBG_MSG("Fail to generate a key handle\n"); + return CTAP2_ERR_UNHANDLED_REQUEST; + } if (alg_type == COSE_ALG_ES256) { - build_cose_key(ad->at.publicKey, 0); - outLen += sizeof(ad->at) - sizeof(ad->at.publicKey) + COSE_KEY_ES256_SIZE; + build_cose_key(ad->at.public_key, 0); + outLen += sizeof(ad->at) - sizeof(ad->at.public_key) + COSE_KEY_ES256_SIZE; } else if (alg_type == COSE_ALG_EDDSA) { - build_ed25519_cose_key(ad->at.publicKey); - outLen += sizeof(ad->at) - sizeof(ad->at.publicKey) + COSE_KEY_EDDSA_SIZE; + build_ed25519_cose_key(ad->at.public_key); + outLen += sizeof(ad->at) - sizeof(ad->at.public_key) + COSE_KEY_EDDSA_SIZE; } else { + DBG_MSG("Unknown algorithm type\n"); return CTAP2_ERR_UNHANDLED_REQUEST; } } if (flags & FLAGS_ED) { - if (*len < outLen + extensionSize) return CTAP2_ERR_LIMIT_EXCEEDED; - memcpy(buf + outLen, extension, extensionSize); - outLen += extensionSize; + if (*len < outLen + extension_size) { + DBG_MSG("Extension is too long\n"); + return CTAP2_ERR_LIMIT_EXCEEDED; + } + memcpy(buf + outLen, extension, extension_size); + outLen += extension_size; } *len = outLen; return 0; } static uint8_t ctap_make_credential(CborEncoder *encoder, uint8_t *params, size_t len) { - // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-makeCred-authnr-alg + uint8_t data_buf[sizeof(CTAP_auth_data)]; CborParser parser; - CTAP_makeCredential mc; - // CBOR of {"hmac-secret": true} - const uint8_t hmacExt[] = {0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5}; + CTAP_make_credential mc; int ret = parse_make_credential(&parser, &mc, params, len); CHECK_PARSER_RET(ret); - uint8_t data_buf[sizeof(CTAP_authData)]; - if (mc.excludeListSize > 0) { - for (size_t i = 0; i < mc.excludeListSize; ++i) { + ret = ctap_consistency_check(); + CHECK_PARSER_RET(ret); + + // 1. If authenticator supports clientPin features and the platform sends a zero length pin_uv_auth_param + if ((mc.parsed_params & PARAM_PIN_UV_AUTH_PARAM) && mc.pin_uv_auth_param_len == 0) { + // a. Request evidence of user interaction in an authenticator-specific way (e.g., flash the LED light). + // b. If the user declines permission, or the operation times out, then end the operation by returning + // CTAP2_ERR_OPERATION_DENIED. + WAIT(CTAP2_ERR_OPERATION_DENIED); + // c. If evidence of user interaction is provided in this step then return either CTAP2_ERR_PIN_NOT_SET + // if PIN is not set or CTAP2_ERR_PIN_INVALID if PIN has been set. + if (has_pin()) + return CTAP2_ERR_PIN_INVALID; + else + return CTAP2_ERR_PIN_NOT_SET; + } + + // 2. If the pin_uv_auth_param parameter is present + // a. If the pinUvAuthProtocol parameter’s value is not supported, return CTAP1_ERR_INVALID_PARAMETER error. + // > This has been processed when parsing. + // b. If the pinUvAuthProtocol parameter is absent, return CTAP2_ERR_MISSING_PARAMETER error. + if ((mc.parsed_params & PARAM_PIN_UV_AUTH_PARAM) && + !(mc.parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL)) { + DBG_MSG("Missing required pin_uv_auth_protocol\n"); + return CTAP2_ERR_MISSING_PARAMETER; + } + // 3. Validate pubKeyCredParams with the following steps + // > This has been processed when parsing. + + // 4. Create a new authenticatorMakeCredential response structure and initialize both its "uv" bit and "up" bit as false. + bool uv = false; // up is always true, see 14.c + + // 5. If the options parameter is present, process all option keys and values present in the parameter. + // a. If the "uv" option is absent, let the "uv" option be treated as being present with the value false. + if (mc.options.uv == OPTION_ABSENT) mc.options.uv = OPTION_FALSE; + // b. If the pin_uv_auth_param is present, let the "uv" option be treated as being present with the value false. + if (mc.parsed_params & PARAM_PIN_UV_AUTH_PARAM) mc.options.uv = OPTION_FALSE; + // c. If the "uv" option is true then + if (mc.options.uv == OPTION_TRUE) { + // 1) If the authenticator does not support a built-in user verification method end the operation + // by returning CTAP2_ERR_INVALID_OPTION. + DBG_MSG("Rule 5-c-1 not satisfied.\n"); + return CTAP2_ERR_INVALID_OPTION; + // 2) [N/A] If the built-in user verification method has not yet been enabled, end the operation + // by returning CTAP2_ERR_INVALID_OPTION. + } + // d. If the "rk" option is present then: DO NOTHING + // e. Else: (the "rk" option is absent): Let the "rk" option be treated as being present with the value false. + if (mc.options.rk == OPTION_ABSENT) mc.options.rk = OPTION_FALSE; + // f. If the "up" option is present then: + // If the "up" option is false, end the operation by returning CTAP2_ERR_INVALID_OPTION. + if (mc.options.up == OPTION_FALSE) { + DBG_MSG("Rule 5-f not satisfied\n"); + return CTAP2_ERR_INVALID_OPTION; + } + // g. If the "up" option is absent, let the "up" option be treated as being present with the value true + mc.options.up = OPTION_TRUE; + + // 6. [N/A] If the alwaysUv option ID is present and true + + // 7. If the makeCredUvNotRqd option ID is present and set to true in the authenticatorGetInfo response + // If the following statements are all true: + // a) The authenticator is protected by some form of user verification. + // b) [ALWAYS TRUE] The "uv" option is set to false. + // c) The pin_uv_auth_param parameter is not present. + // d) The "rk" option is present and set to true. + if (has_pin() /* a) */ && (mc.parsed_params & PARAM_PIN_UV_AUTH_PARAM) == 0 /* c) */ && + mc.options.rk == OPTION_TRUE) { + // If ClientPin option ID is true and the noMcGaPermissionsWithClientPin option ID is absent or false, + // end the operation by returning CTAP2_ERR_PUAT_REQUIRED. + DBG_MSG("Rule 7 not satisfied\n"); + return CTAP2_ERR_PUAT_REQUIRED; + // [N/A] Otherwise, end the operation by returning CTAP2_ERR_OPERATION_DENIED. + } + + // 8. [N/A] Else (the makeCredUvNotRqd option ID is present with the value false or is absent) + + // 9. [N/A] If the enterpriseAttestation parameter is present + + // 10. If the following statements are all true + // a) "rk" and "uv" [ALWAYS TRUE] options are both set to false or omitted. + // b) [ALWAYS TRUE] the makeCredUvNotRqd option ID in authenticatorGetInfo's response is present with the value true. + // c) the pin_uv_auth_param parameter is not present. + // Then go to Step 12. + if (mc.options.rk == OPTION_FALSE && (mc.parsed_params & PARAM_PIN_UV_AUTH_PARAM) == 0) { + DBG_MSG("Rule 10 satisfied, go to Step 12\n"); + goto step12; + } + + // 11. If the authenticator is protected by some form of user verification, then: + if (has_pin()) { + // 11.1 If pin_uv_auth_param parameter is present (implying the "uv" option is false (see Step 5)): + if (mc.parsed_params & PARAM_PIN_UV_AUTH_PARAM) { + // a) Call verify(pinUvAuthToken, client_data_hash, pin_uv_auth_param). + // If the verification returns error, then end the operation by returning CTAP2_ERR_PIN_AUTH_INVALID error. + if (!consecutive_pin_counter) return CTAP2_ERR_PIN_AUTH_BLOCKED; + if (!cp_verify_pin_token(mc.client_data_hash, sizeof(mc.client_data_hash), mc.pin_uv_auth_param, + mc.pin_uv_auth_protocol)) { + DBG_MSG("Fail to verify pin token\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + // b) Verify that the pinUvAuthToken has the mc permission, if not, then end the operation by returning CTAP2_ERR_PIN_AUTH_INVALID. + if (!cp_has_permission(CP_PERMISSION_MC)) { + DBG_MSG("Fail to verify pin permission\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + // c) If the pinUvAuthToken has a permissions RP ID associated: + // If the permissions RP ID does not match the rp.id in this request, then end the operation by returning CTAP2_ERR_PIN_AUTH_INVALID. + if (!cp_verify_rp_id(mc.rp_id_hash)) { + DBG_MSG("Fail to verify pin rp id\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + // d) Let userVerifiedFlagValue be the result of calling getUserVerifiedFlagValue(). + // e) If userVerifiedFlagValue is false then end the operation by returning CTAP2_ERR_PIN_AUTH_INVALID. + if (!cp_get_user_verified_flag_value()) { + DBG_MSG("userVerifiedFlagValue is false\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + // f) If userVerifiedFlagValue is true then set the "uv" bit to true in the response. + uv = true; + // g) If the pinUvAuthToken does not have a permissions RP ID associated: + // Associate the request’s rp.id parameter value with the pinUvAuthToken as its permissions RP ID. + cp_associate_rp_id(mc.rp_id_hash); + DBG_MSG("PIN verified\n"); + } + // 11.2 [N/A] If the "uv" option is present and set to true + } + + step12: + // 12. If the exclude_list parameter is present and contains a credential ID created by this authenticator, + // that is bound to the specified rp.id: + if (mc.exclude_list_size > 0) { + for (size_t i = 0; i < mc.exclude_list_size; ++i) { ecc_key_t key; - parse_credential_descriptor(&mc.excludeList, data_buf); // save credential id in data_buf - CredentialId *kh = (CredentialId *)data_buf; - // compare rpId first - if (memcmp(kh->rpIdHash, mc.rpIdHash, sizeof(kh->rpIdHash)) != 0) continue; - // then verify key handle and get private key in rpIdHash + parse_credential_descriptor(&mc.exclude_list, data_buf); // save credential id in data_buf + credential_id *kh = (credential_id *) data_buf; + // compare rp_id first + if (memcmp_s(kh->rp_id_hash, mc.rp_id_hash, sizeof(kh->rp_id_hash)) != 0) goto next_exclude_list; + // then verify key handle and get private key in rp_id_hash ret = verify_key_handle(kh, &key); memzero(&key, sizeof(key)); if (ret < 0) return CTAP2_ERR_UNHANDLED_REQUEST; if (ret == 0) { DBG_MSG("Exclude ID found\n"); - WAIT(); - return CTAP2_ERR_CREDENTIAL_EXCLUDED; + // a) If the credential’s credProtect value is not userVerificationRequired + if (kh->nonce[CREDENTIAL_NONCE_CP_POS] != CRED_PROTECT_VERIFICATION_REQUIRED || + // b) Else (implying the credential’s credProtect value is userVerificationRequired) + // AND If the "uv" bit is true in the response: + (kh->nonce[CREDENTIAL_NONCE_CP_POS] == CRED_PROTECT_VERIFICATION_REQUIRED && uv)) { + + // i. Let userPresentFlagValue be false. + bool userPresentFlagValue = false; + // ii. If the pinUvAuthParam parameter is present then let userPresentFlagValue be the result of calling + // getUserPresentFlagValue(). + if (mc.parsed_params & PARAM_PIN_UV_AUTH_PARAM) userPresentFlagValue = cp_get_user_present_flag_value(); + // iii. [N/A] Else, if evidence of user interaction was provided as part of Step 11 let userPresentFlagValue be true. + // iv. If userPresentFlagValue is false, then: + // (1) Wait for user presence. + // (2) Regardless of whether user presence is obtained or the authenticator times out, + // terminate this procedure and return CTAP2_ERR_CREDENTIAL_EXCLUDED. + if (!userPresentFlagValue) WAIT(CTAP2_ERR_CREDENTIAL_EXCLUDED); + // v. Else, (implying userPresentFlagValue is true) terminate this procedure and return CTAP2_ERR_CREDENTIAL_EXCLUDED. + return CTAP2_ERR_CREDENTIAL_EXCLUDED; + + // c) Else (implying user verification was not collected in Step 11), + // remove the credential from the excludeList and continue parsing the rest of the list. + } else { + DBG_MSG("Ignore this Exclude ID\n"); + } } - ret = cbor_value_advance(&mc.excludeList); + next_exclude_list: + ret = cbor_value_advance(&mc.exclude_list); CHECK_CBOR_RET(ret); } } - if (has_pin() && (mc.parsedParams & PARAM_pinAuth) == 0) return CTAP2_ERR_PIN_REQUIRED; - if (mc.parsedParams & PARAM_pinAuth) { - if (mc.pinAuthLength == 0) { - WAIT(); - if (has_pin()) - return CTAP2_ERR_PIN_INVALID; - else - return CTAP2_ERR_PIN_NOT_SET; + // 13. [N/A] If evidence of user interaction was provided as part of Step 11 + + // 14. [ALWAYS TRUE] If the "up" option is set to true + // a) If the pin_uv_auth_param parameter is present then: + if (mc.parsed_params & PARAM_PIN_UV_AUTH_PARAM) { + if (!cp_get_user_present_flag_value()) { + WAIT(CTAP2_ERR_OPERATION_DENIED); } - if ((mc.parsedParams & PARAM_pinProtocol) == 0) return CTAP2_ERR_PIN_AUTH_INVALID; - hmac_sha256(pin_token, PIN_TOKEN_SIZE, mc.clientDataHash, sizeof(mc.clientDataHash), params); - if (memcmp(params, mc.pinAuth, PIN_AUTH_SIZE) != 0) return CTAP2_ERR_PIN_AUTH_INVALID; + } else { + // b) Else (implying the pin_uv_auth_param parameter is not present) + // 1. [ALWAYS TRUE] If the "up" bit is false in the response : + WAIT(CTAP2_ERR_OPERATION_DENIED); } + // c) [N/A] Set the "up" bit to true in the response + // d) Call clearUserPresentFlag(), clearUserVerifiedFlag(), and clearPinUvAuthTokenPermissionsExceptLbw(). + cp_clear_user_present_flag(); + cp_clear_user_verified_flag(); + cp_clear_pin_uv_auth_token_permissions_except_lbw(); + + // 15. If the extensions parameter is present: + uint8_t extension_buffer[MAX_EXTENSION_SIZE_IN_AUTH]; + CborEncoder extension_encoder, map; + cbor_encoder_init(&extension_encoder, extension_buffer, sizeof(extension_buffer), 0); + ret = cbor_encoder_create_map(&extension_encoder, &map, + (mc.ext_hmac_secret ? 1 : 0) + + // largeBlobKey has no outputs here + (mc.ext_cred_protect != CRED_PROTECT_ABSENT ? 1 : 0) + + (mc.ext_has_cred_blob ? 1 : 0)); + CHECK_CBOR_RET(ret); + if (mc.ext_large_blob_key) { + if (mc.options.rk != OPTION_TRUE) { + DBG_MSG("largeBlobKey requires rk\n"); + return CTAP2_ERR_INVALID_OPTION; + } + // Generate key in Step 17 + } + if (mc.ext_has_cred_blob) { + bool accepted = false; + if (mc.ext_cred_blob_len <= MAX_CRED_BLOB_LENGTH && mc.options.rk == OPTION_TRUE) { + accepted = true; + } + ret = cbor_encode_text_stringz(&map, "credBlob"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_boolean(&map, accepted); + CHECK_CBOR_RET(ret); + } + if (mc.ext_cred_protect != CRED_PROTECT_ABSENT) { + ret = cbor_encode_text_stringz(&map, "credProtect"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, mc.ext_cred_protect); + CHECK_CBOR_RET(ret); + } + if (mc.ext_hmac_secret) { + ret = cbor_encode_text_stringz(&map, "hmac-secret"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_boolean(&map, true); + CHECK_CBOR_RET(ret); + } + ret = cbor_encoder_close_container(&extension_encoder, &map); + CHECK_CBOR_RET(ret); + size_t extension_size = cbor_encoder_get_buffer_size(&extension_encoder, extension_buffer); - WAIT(); - - // build response - CborEncoder map; - ret = cbor_encoder_create_map(encoder, &map, 3); + // Now prepare the response + ret = cbor_encoder_create_map(encoder, &map, 3 /*fmt, authData, attStmt*/ + (mc.ext_large_blob_key ? 1 : 0)); CHECK_CBOR_RET(ret); - // fmt - ret = cbor_encode_int(&map, RESP_fmt); + // [member name] fmt + ret = cbor_encode_int(&map, MC_RESP_FMT); CHECK_CBOR_RET(ret); ret = cbor_encode_text_stringz(&map, "packed"); CHECK_CBOR_RET(ret); - // auth data + // 16. Generate a new credential key pair for the algorithm chosen in step 3. + // [member name] authData len = sizeof(data_buf); - uint8_t flags = FLAGS_AT | (mc.extension_hmac_secret ? FLAGS_ED : 0) | (has_pin() > 0 ? FLAGS_UV : 0) | FLAGS_UP; - ret = ctap_make_auth_data(mc.rpIdHash, data_buf, flags, sizeof(hmacExt), hmacExt, &len, mc.alg_type); + uint8_t flags = FLAGS_AT | (extension_size > 0 ? FLAGS_ED : 0) | (uv ? FLAGS_UV : 0) | FLAGS_UP; + ret = ctap_make_auth_data(mc.rp_id_hash, data_buf, flags, extension_buffer, extension_size, &len, + mc.alg_type, mc.options.rk == OPTION_TRUE, mc.ext_cred_protect); if (ret != 0) return ret; - ret = cbor_encode_int(&map, RESP_authData); + ret = cbor_encode_int(&map, MC_RESP_AUTH_DATA); CHECK_CBOR_RET(ret); ret = cbor_encode_byte_string(&map, data_buf, len); CHECK_CBOR_RET(ret); - // process rk - if (mc.rk) { - CTAP_residentKey rk; - int size = get_file_size(RK_FILE); + // 17. If the "rk" option is set to true: + // a) The authenticator MUST create a discoverable credential. + // b) If a credential for the same rp.id and account ID already exists on the authenticator: + // Overwrite that credential. + // c) Store the user parameter along with the newly-created key pair. + // d) If authenticator does not have enough internal storage to persist the new credential, return CTAP2_ERR_KEY_STORE_FULL. + CTAP_discoverable_credential dc = {0}; + if (mc.options.rk == OPTION_TRUE) { + DBG_MSG("Processing discoverable credential\n"); + int size = get_file_size(DC_FILE); if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - size_t nRk = size / sizeof(CTAP_residentKey), i; - for (i = 0; i != nRk; ++i) { - size = read_file(RK_FILE, &rk, i * sizeof(CTAP_residentKey), sizeof(CTAP_residentKey)); - if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - if (memcmp(mc.rpIdHash, rk.credential_id.rpIdHash, SHA256_DIGEST_LENGTH) == 0 && - mc.user.id_size == rk.user.id_size && memcmp(mc.user.id, rk.user.id, mc.user.id_size) == 0) + int n_dc = size / (int) sizeof(CTAP_discoverable_credential), pos, first_deleted = MAX_DC_NUM; + for (pos = 0; pos != n_dc; ++pos) { + if (read_file(DC_FILE, &dc, pos * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential)) < 0) { + ERR_MSG("Unable to read DC_FILE\n"); + return CTAP2_ERR_UNHANDLED_REQUEST; + } + if (dc.deleted) { + if (first_deleted == MAX_DC_NUM) first_deleted = pos; + continue; + } + // b + if (memcmp_s(mc.rp_id_hash, dc.credential_id.rp_id_hash, SHA256_DIGEST_LENGTH) == 0 && + mc.user.id_size == dc.user.id_size && memcmp_s(mc.user.id, dc.user.id, mc.user.id_size) == 0) break; } - if (i >= MAX_RK_NUM) return CTAP2_ERR_KEY_STORE_FULL; - memcpy(&rk.credential_id, data_buf + 55, sizeof(rk.credential_id)); - memcpy(&rk.user, &mc.user, sizeof(UserEntity)); - ret = write_file(RK_FILE, &rk, i * sizeof(CTAP_residentKey), sizeof(CTAP_residentKey), 0); - if (ret < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + // d + if (pos == n_dc && first_deleted != MAX_DC_NUM) { + DBG_MSG("Use slot %d\n", first_deleted); + pos = first_deleted; + } + DBG_MSG("Finally use slot %d\n", pos); + if (pos >= MAX_DC_NUM) { + DBG_MSG("Storage full\n"); + return CTAP2_ERR_KEY_STORE_FULL; + } + memcpy(&dc.credential_id, data_buf + 55, sizeof(dc.credential_id)); + memcpy(&dc.user, &mc.user, sizeof(user_entity)); // c + dc.has_large_blob_key = mc.ext_large_blob_key; + if (dc.has_large_blob_key) random_buffer(dc.large_blob_key, LARGE_BLOB_KEY_SIZE); + dc.cred_blob_len = 0; + if (mc.ext_has_cred_blob && mc.ext_cred_blob_len <= MAX_CRED_BLOB_LENGTH) { + dc.cred_blob_len = mc.ext_cred_blob_len; + memcpy(dc.cred_blob, mc.ext_cred_blob, mc.ext_cred_blob_len); + } + dc.deleted = false; + + CTAP_dc_general_attr attr; + if (read_attr(DC_FILE, DC_GENERAL_ATTR, &attr, sizeof(attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + attr.pending_add = 1; + attr.index = (uint8_t)pos; + if (write_attr(DC_FILE, DC_GENERAL_ATTR, &attr, sizeof(attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (write_file(DC_FILE, &dc, pos * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential), 0) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + + // Process metadata + size = get_file_size(DC_META_FILE); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + int n_rp = size / (int) sizeof(CTAP_rp_meta), meta_pos; + CTAP_rp_meta meta; + first_deleted = MAX_DC_NUM; + for (meta_pos = 0; meta_pos != n_rp; ++meta_pos) { + size = read_file(DC_META_FILE, &meta, meta_pos * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (meta.slots == 0) { // deleted + if (first_deleted == MAX_DC_NUM) first_deleted = meta_pos; + continue; + } + if (memcmp_s(mc.rp_id_hash, meta.rp_id_hash, SHA256_DIGEST_LENGTH) == 0) break; + } + if (meta_pos == n_rp) { + meta.slots = 0; // a new entry's slot should be empty + if (first_deleted != MAX_DC_NUM) { + DBG_MSG("Use deleted slot %d for meta\n", first_deleted); + meta_pos = first_deleted; + } + } + DBG_MSG("Finally use slot %d for meta\n", meta_pos); + memcpy(meta.rp_id_hash, mc.rp_id_hash, SHA256_DIGEST_LENGTH); + memcpy(meta.rp_id, mc.rp_id, MAX_STORED_RPID_LENGTH); + meta.rp_id_len = mc.rp_id_len; + meta.slots |= 1ull << pos; + DBG_MSG("New meta.slots = %llu\n", meta.slots); + if (write_file(DC_META_FILE, &meta, meta_pos * (int) sizeof(CTAP_rp_meta), + sizeof(CTAP_rp_meta), 0) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + attr.pending_add = 0; + ++attr.numbers; + if (write_attr(DC_FILE, DC_GENERAL_ATTR, &attr, sizeof(attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; } - // attestation statement + // 18. Otherwise, if the "rk" option is false: the authenticator MUST create a non-discoverable credential. + // 19. Generate an attestation statement for the newly-created credential using client_data_hash + + // [member name] attStmt // https://www.w3.org/TR/webauthn/#packed-attestation // { // alg: COSE_ALG_ES256, // sig: bytes (ASN.1), // x5c: [ attestnCert: bytes, * (caCert: bytes) ] // } - ret = cbor_encode_int(&map, RESP_attStmt); + ret = cbor_encode_int(&map, MC_RESP_ATT_STMT); CHECK_CBOR_RET(ret); CborEncoder att_map; ret = cbor_encoder_create_map(&map, &att_map, 3); @@ -328,7 +664,7 @@ static uint8_t ctap_make_credential(CborEncoder *encoder, uint8_t *params, size_ CHECK_CBOR_RET(ret); sha256_init(); sha256_update(data_buf, len); - sha256_update(mc.clientDataHash, sizeof(mc.clientDataHash)); + sha256_update(mc.client_data_hash, sizeof(mc.client_data_hash)); sha256_final(data_buf); len = sign_with_device_key(data_buf, PRIVATE_KEY_LENGTH[SECP256R1], data_buf); ret = cbor_encode_byte_string(&att_map, data_buf, len); @@ -342,7 +678,8 @@ static uint8_t ctap_make_credential(CborEncoder *encoder, uint8_t *params, size_ CHECK_CBOR_RET(ret); { // to save RAM, generate an empty cert first, then fill it manually - ret = cbor_encode_byte_string(&x5carr, NULL, 0); + // data_buf is never read here because length=0 + ret = cbor_encode_byte_string(&x5carr, data_buf, 0); CHECK_CBOR_RET(ret); uint8_t *ptr = x5carr.data.ptr - 1; ret = get_cert(ptr + 3); @@ -359,310 +696,656 @@ static uint8_t ctap_make_credential(CborEncoder *encoder, uint8_t *params, size_ ret = cbor_encoder_close_container(&map, &att_map); CHECK_CBOR_RET(ret); + if (mc.ext_large_blob_key) { + ret = cbor_encode_int(&map, MC_RESP_LARGE_BLOB_KEY); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&map, dc.large_blob_key, LARGE_BLOB_KEY_SIZE); + CHECK_CBOR_RET(ret); + } + ret = cbor_encoder_close_container(encoder, &map); CHECK_CBOR_RET(ret); return 0; } -static uint8_t ctap_get_assertion(CborEncoder *encoder, uint8_t *params, size_t len) { - static CTAP_getAssertion ga; +static uint8_t ctap_get_assertion(CborEncoder *encoder, uint8_t *params, size_t len, bool in_get_next_assertion) { + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-getAssert-authnr-alg + static CTAP_get_assertion ga; + static uint8_t credential_list[MAX_DC_NUM], number_of_credentials, credential_counter; + static bool uv, up, user_details; + static uint32_t timer; + + CTAP_discoverable_credential dc = {0}; // We use dc to store the selected credential + uint8_t data_buf[sizeof(CTAP_auth_data) + CLIENT_DATA_HASH_SIZE]; + ecc_key_t key; // TODO: cleanup CborParser parser; int ret; - uint8_t pinAuth[SHA256_DIGEST_LENGTH]; - if (credential_idx == 0) { - ret = parse_get_assertion(&parser, &ga, params, len); + + if (!in_get_next_assertion) { + credential_counter = 0; + ret = ctap_consistency_check(); CHECK_PARSER_RET(ret); + } else { + // GET_NEXT_ASSERTION + // 1. If authenticator does not remember any authenticatorGetAssertion parameters, return CTAP2_ERR_NOT_ALLOWED. + if (last_cmd != CTAP_GET_ASSERTION && last_cmd != CTAP_GET_NEXT_ASSERTION) return CTAP2_ERR_NOT_ALLOWED; + // 2. If the credentialCounter is equal to or greater than numberOfCredentials, return CTAP2_ERR_NOT_ALLOWED. + if (credential_counter >= number_of_credentials) return CTAP2_ERR_NOT_ALLOWED; + // 3. If timer since the last call to authenticatorGetAssertion/authenticatorGetNextAssertion is greater than + // 30 seconds, discard the current authenticatorGetAssertion state and return CTAP2_ERR_NOT_ALLOWED. + // This step is OPTIONAL if transport is done over NFC. + if (device_get_tick() - timer > 30000) return CTAP2_ERR_NOT_ALLOWED; + // 4. Select the credential indexed by credentialCounter. (I.e. credentials[n] assuming a zero-based array.) + // 5. Update the response to include the selected credential’s publicKeyCredentialUserEntity information. + // User identifiable information (name, DisplayName, icon) inside the publicKeyCredentialUserEntity MUST NOT be + // returned if user verification was not done by the authenticator in the original authenticatorGetAssertion call. + // 6. Sign the client_data_hash along with authData with the selected credential. + goto step7; + // 7. Reset the timer. This step is OPTIONAL if transport is done over NFC. + // 8. Increment credentialCounter. + // > Process at the end of this function. } + ret = parse_get_assertion(&parser, &ga, params, len); + CHECK_PARSER_RET(ret); - if (ga.parsedParams & PARAM_pinAuth) { - if (ga.pinAuthLength == 0) { - WAIT(); - if (has_pin()) - return CTAP2_ERR_PIN_INVALID; - else - return CTAP2_ERR_PIN_NOT_SET; + // 1. If authenticator supports clientPin features and the platform sends a zero length pin_uv_auth_param + if ((ga.parsed_params & PARAM_PIN_UV_AUTH_PARAM) && ga.pin_uv_auth_param_len == 0) { + // a. Request evidence of user interaction in an authenticator-specific way (e.g., flash the LED light). + // b. If the user declines permission, or the operation times out, then end the operation by returning + // CTAP2_ERR_OPERATION_DENIED. + WAIT(CTAP2_ERR_OPERATION_DENIED); + // c. If evidence of user interaction is provided in this step then return either CTAP2_ERR_PIN_NOT_SET + // if PIN is not set or CTAP2_ERR_PIN_INVALID if PIN has been set. + if (has_pin()) + return CTAP2_ERR_PIN_INVALID; + else + return CTAP2_ERR_PIN_NOT_SET; + } + + // 2. If the pin_uv_auth_param parameter is present + // a. If the pinUvAuthProtocol parameter’s value is not supported, return CTAP1_ERR_INVALID_PARAMETER error. + // > This has been processed when parsing. + // b. If the pinUvAuthProtocol parameter is absent, return CTAP2_ERR_MISSING_PARAMETER error. + if ((ga.parsed_params & PARAM_PIN_UV_AUTH_PARAM) && + !(ga.parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL)) { + DBG_MSG("Missing required pin_uv_auth_protocol\n"); + return CTAP2_ERR_MISSING_PARAMETER; + } + + // 3. Create a new authenticatorGetAssertion response structure and initialize both its "uv" bit and "up" bit as false. + uv = false; + up = false; + + // 4. If the options parameter is present, process all option keys and values present in the parameter. + // a. If the "uv" option is absent, let the "uv" option be treated as being present with the value false. + if (ga.options.uv == OPTION_ABSENT) ga.options.uv = OPTION_FALSE; + // b. If the pin_uv_auth_param is present, let the "uv" option be treated as being present with the value false. + if (ga.parsed_params & PARAM_PIN_UV_AUTH_PARAM) ga.options.uv = OPTION_FALSE; + // c. If the "uv" option is true then + if (ga.options.uv == OPTION_TRUE) { + // 1) If the authenticator does not support a built-in user verification method end the operation + // by returning CTAP2_ERR_INVALID_OPTION. + DBG_MSG("Rule 4-c-1 not satisfied.\n"); + return CTAP2_ERR_INVALID_OPTION; + // 2) [N/A] If the built-in user verification method has not yet been enabled, end the operation + // by returning CTAP2_ERR_INVALID_OPTION. + } + // d. If the "rk" option is present then: Return CTAP2_ERR_UNSUPPORTED_OPTION. + if (ga.options.rk != OPTION_ABSENT) { + DBG_MSG("Rule 4-d not satisfied.\n"); + return CTAP2_ERR_UNSUPPORTED_OPTION; + } + // e. If the "up" option is not present then: Let the "up" option be treated as being present with the value true. + if (ga.options.up == OPTION_ABSENT) ga.options.up = OPTION_TRUE; + + // 5. [N/A] If the alwaysUv option ID is present and true and the "up" option is present and true + + // 6. If authenticator is protected by some form of user verification, then: + // 6.2 [N/A] If the "uv" option is present and set to true + // 6.1 If pin_uv_auth_param parameter is present + if (has_pin() && (ga.parsed_params & PARAM_PIN_UV_AUTH_PARAM)) { + // a) Call verify(pinUvAuthToken, client_data_hash, pin_uv_auth_param). + // If the verification returns error, return CTAP2_ERR_PIN_AUTH_INVALID error. + // If the verification returns success, set the "uv" bit to true in the response. + if (!consecutive_pin_counter) return CTAP2_ERR_PIN_AUTH_BLOCKED; + if (!cp_verify_pin_token(ga.client_data_hash, sizeof(ga.client_data_hash), ga.pin_uv_auth_param, + ga.pin_uv_auth_protocol)) { + DBG_MSG("Fail to verify pin token\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; } - if ((ga.parsedParams & PARAM_pinProtocol) == 0) return CTAP2_ERR_PIN_AUTH_INVALID; - hmac_sha256(pin_token, PIN_TOKEN_SIZE, ga.clientDataHash, sizeof(ga.clientDataHash), pinAuth); -#ifndef FUZZ - if (memcmp(pinAuth, ga.pinAuth, PIN_AUTH_SIZE) != 0) return CTAP2_ERR_PIN_AUTH_INVALID; -#endif + uv = true; + // b) Let userVerifiedFlagValue be the result of calling getUserVerifiedFlagValue(). + // c) If userVerifiedFlagValue is false then end the operation by returning CTAP2_ERR_PIN_AUTH_INVALID. + if (!cp_get_user_verified_flag_value()) { + DBG_MSG("userVerifiedFlagValue is false\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + // d) Verify that the pinUvAuthToken has the ga permission, if not, return CTAP2_ERR_PIN_AUTH_INVALID. + if (!cp_has_permission(CP_PERMISSION_GA)) { + DBG_MSG("Fail to verify pin permission\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + // e) If the pinUvAuthToken has a permissions RP ID associated: + // If the permissions RP ID does not match the rp_id in this request, return CTAP2_ERR_PIN_AUTH_INVALID. + if (!cp_verify_rp_id(ga.rp_id_hash)) { + DBG_MSG("Fail to verify pin rp id\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + // f) If the pinUvAuthToken does not have a permissions RP ID associated: + // Associate the request’s rp_id parameter value with the pinUvAuthToken as its permissions RP ID. + cp_associate_rp_id(ga.rp_id_hash); } - uint8_t data_buf[sizeof(CTAP_authData) + CLIENT_DATA_HASH_SIZE]; - ecc_key_t key; // TODO: cleanup - CTAP_residentKey rk; - if (ga.allowListSize > 0) { + step7: + // 7. Locate all credentials that are eligible for retrieval under the specified criteria + // a) If the allow_list parameter is present and is non-empty, locate all denoted credentials created by this + // authenticator and bound to the specified rp_id. + // b) If an allow_list is not present, locate all discoverable credentials that are created by this authenticator + // and bound to the specified rp_id. + // c) Create an applicable credentials list populated with the located credentials. + // d) Iterate through the applicable credentials list, and if credential protection for a credential is marked + // as userVerificationRequired, and the "uv" bit is false in the response, remove that credential from the + // applicable credentials list. + // e) Iterate through the applicable credentials list, and if credential protection for a credential is marked + // as userVerificationOptionalWithCredentialIDList and there is no allow_list passed by the client and the "uv" + // bit is false in the response, remove that credential from the applicable credentials list. + // f) If the applicable credentials list is empty, return CTAP2_ERR_NO_CREDENTIALS. + // g) Let numberOfCredentials be the number of applicable credentials found. + // NOTE: only one credential is used as stated in Step 11 & 12; therefore, we select that credential according to + // Step 11 & 12: + // 11. If the allow_list parameter is present: + // Select any credential from the applicable credentials list. + // Delete the numberOfCredentials member. + // 12. If allow_list is not present: + // a) If numberOfCredentials is one: Select that credential. + // b) If numberOfCredentials is more than one: + // 1) Order the credentials in the applicable credentials list by the time when they were created in + // reverse order. (I.e. the first credential is the most recently created.) + // 2)If the authenticator does not have a display: + // i. Remember the authenticatorGetAssertion parameters. + // ii. Create a credential counter (credentialCounter) and set it to 1. This counter signifies the next + // credential to be returned by the authenticator, assuming zero-based indexing. + // iii. Start a timer. This is used during authenticatorGetNextAssertion command. This step is OPTIONAL + // if transport is done over NFC. + // iv. Select the first credential. + // 3) [N/A] If authenticator has a display and at least one of the "uv" and "up" options is true. + // c) Update the response to include the selected credential’s publicKeyCredentialUserEntity information. + // User identifiable information (name, DisplayName, icon) inside the publicKeyCredentialUserEntity + // MUST NOT be returned if user verification is not done by the authenticator. + if (ga.allow_list_size > 0) { // Step 11 size_t i; - for (i = 0; i < ga.allowListSize; ++i) { - parse_credential_descriptor(&ga.allowList, (uint8_t *)&rk.credential_id); - // compare rpId first - if (memcmp(rk.credential_id.rpIdHash, ga.rpIdHash, sizeof(rk.credential_id.rpIdHash)) != 0) goto next; - // then verify key handle and get private key - int err = verify_key_handle(&rk.credential_id, &key); + for (i = 0; i < ga.allow_list_size; ++i) { + parse_credential_descriptor(&ga.allow_list, (uint8_t *) &dc.credential_id); + // compare the rp_id first + if (memcmp_s(dc.credential_id.rp_id_hash, ga.rp_id_hash, sizeof(dc.credential_id.rp_id_hash)) != 0) goto next; + // then verify the key handle and get private key + int err = verify_key_handle(&dc.credential_id, &key); if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - if (err == 0) break; // only process one support credential - next: - ret = cbor_value_advance(&ga.allowList); + if (err == 0) { + // Skip the credential which is protected + if (!check_credential_protect_requirements(&dc.credential_id, true, uv)) goto next; + if (dc.credential_id.nonce[CREDENTIAL_NONCE_DC_POS]) { // Verify if it's a valid dc. + memcpy(data_buf, dc.credential_id.nonce, sizeof(dc.credential_id.nonce)); // use data_buf to store the nonce temporarily + int size = get_file_size(DC_FILE); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + int n_dc = (int) (size / sizeof(CTAP_discoverable_credential)); + bool found = false; + DBG_MSG("%d discoverable credentials\n", n_dc); + for (int j = 0; j < n_dc; ++j) { + if (read_file(DC_FILE, &dc, j * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential)) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + if (dc.deleted) { + DBG_MSG("Skipped DC at %d\n", j); + continue; + } + if (memcmp_s(ga.rp_id_hash, dc.credential_id.rp_id_hash, SHA256_DIGEST_LENGTH) == 0 && + memcmp_s(data_buf, dc.credential_id.nonce, sizeof(dc.credential_id.nonce)) == 0) { + found = true; + break; + } + } + DBG_MSG("matching credential_id%s found\n", (found ? "" : " not")); + if (found) break; + // if (!found) return CTAP2_ERR_NO_CREDENTIALS; + } else { // not DC + break; // Step 11: Select any credential from the applicable credentials list. + } + } + next: + ret = cbor_value_advance(&ga.allow_list); CHECK_CBOR_RET(ret); } - if (i == ga.allowListSize) { - if (ga.up) WAIT(); + // 7-f + if (i == ga.allow_list_size) { + DBG_MSG("no valid credential found in the allow list\n"); return CTAP2_ERR_NO_CREDENTIALS; } - } else { - int size = 0; - if (credential_idx == 0) { - size = get_file_size(RK_FILE); + number_of_credentials = 1; + } else { // Step 12 + int size; + if (credential_counter == 0) { + size = get_file_size(DC_FILE); if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - int nRk = (int)(size / sizeof(CTAP_residentKey)); - credential_numbers = 0; - // GA step 9: If more than one credential was located in step 1 and allowList is present and not empty, select any - // applicable credential and proceed to step 12. Otherwise, order the credentials by the time when they were - // created in reverse order. The first credential is the most recent credential that was created. - for (int i = nRk - 1; i >= 0; --i) { - size = read_file(RK_FILE, &rk, i * sizeof(CTAP_residentKey), sizeof(CTAP_residentKey)); - if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - if (memcmp(ga.rpIdHash, rk.credential_id.rpIdHash, SHA256_DIGEST_LENGTH) == 0) - credential_list[credential_numbers++] = i; + int n_dc = (int) (size / sizeof(CTAP_discoverable_credential)); + number_of_credentials = 0; + for (int i = n_dc - 1; i >= 0; --i) { // 12-b-1 + if (read_file(DC_FILE, &dc, i * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential)) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + if (dc.deleted) { + DBG_MSG("Skipped DC at %d\n", i); + continue; + } + // Skip the credential which is protected + if (!check_credential_protect_requirements(&dc.credential_id, false, uv)) continue; + if (memcmp_s(ga.rp_id_hash, dc.credential_id.rp_id_hash, SHA256_DIGEST_LENGTH) == 0) + credential_list[number_of_credentials++] = i; } - if (credential_numbers == 0) { - if (ga.up) WAIT(); - return CTAP2_ERR_NO_CREDENTIALS; + // 7-f + if (number_of_credentials == 0) return CTAP2_ERR_NO_CREDENTIALS; + } + // fetch dc and get private key + if (read_file(DC_FILE, &dc, credential_list[credential_counter] * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential)) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + if (verify_key_handle(&dc.credential_id, &key) != 0) return CTAP2_ERR_UNHANDLED_REQUEST; + } + + // For single account per RP case, authenticator returns "id" field to the platform which will be returned to the [WebAuthn] layer. + // For multiple accounts per RP case, where the authenticator does not have a display, authenticator returns "id" as well as other fields to the platform. + // User identifiable information (name, DisplayName, icon) MUST NOT be returned if user verification is not done by the authenticator. + user_details = uv && number_of_credentials > 1; + + // 8. [N/A] If evidence of user interaction was provided as part of Step 6.2 + // 9. If the "up" option is set to true or not present: + if (ga.options.up == OPTION_TRUE) { + // a) If the pin_uv_auth_param parameter is present then: + if (ga.parsed_params & PARAM_PIN_UV_AUTH_PARAM) { + if (!cp_get_user_present_flag_value()) { + WAIT(CTAP2_ERR_OPERATION_DENIED); } + } else { + // b) Else (implying the pin_uv_auth_param parameter is not present): + WAIT(CTAP2_ERR_OPERATION_DENIED); } - // fetch rk and get private key - size = - read_file(RK_FILE, &rk, credential_list[credential_idx] * sizeof(CTAP_residentKey), sizeof(CTAP_residentKey)); - if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - int err = verify_key_handle(&rk.credential_id, &key); - if (err != 0) return CTAP2_ERR_UNHANDLED_REQUEST; + // c) Set the "up" bit to true in the response. + up = true; + // d) Call clearUserPresentFlag(), clearUserVerifiedFlag(), and clearPinUvAuthTokenPermissionsExceptLbw(). + cp_clear_user_present_flag(); + cp_clear_user_verified_flag(); + cp_clear_pin_uv_auth_token_permissions_except_lbw(); } - uint8_t extensionBuffer[79], extensionSize = 0; - uint8_t iv[16] = {0}; - block_cipher_config cfg = {.block_size = 16, .mode = CBC, .iv = iv, .encrypt = aes256_enc, .decrypt = aes256_dec}; - if ((ga.parsedParams & PARAM_hmacSecret) && credential_idx == 0) { - ret = get_shared_secret(ga.hmacSecretKeyAgreement); + DBG_MSG("Credential id: "); + PRINT_HEX((const uint8_t *) &dc.credential_id, sizeof(dc.credential_id)); + + // 10. If the extensions parameter is present: + // a) Process any extensions that this authenticator supports, ignoring any that it does not support. + // b) Authenticator extension outputs generated by the authenticator extension processing are returned to the + // authenticator data. The set of keys in the authenticator extension outputs map MUST be equal to, or a subset + // of, the keys of the authenticator extension inputs map. + + uint8_t extension_buffer[134], extension_size = 0; + CborEncoder extension_encoder, map, sub_map; + // build extensions + cbor_encoder_init(&extension_encoder, extension_buffer, sizeof(extension_buffer), 0); + ret = cbor_encoder_create_map(&extension_encoder, &map, + (ga.ext_cred_blob ? 1 : 0) + + // largeBlobKey has no outputs here + ((ga.parsed_params & PARAM_HMAC_SECRET) ? 1 : 0)); + CHECK_CBOR_RET(ret); + + // Process credBlob extension + if (ga.ext_cred_blob) { + ret = cbor_encode_text_stringz(&map, "credBlob"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&map, dc.cred_blob, dc.cred_blob_len); + CHECK_CBOR_RET(ret); + } + + // Process credProtect extension + if (!check_credential_protect_requirements(&dc.credential_id, ga.allow_list_size > 0, uv)) return CTAP2_ERR_NO_CREDENTIALS; + + // Process hmac-secret extension + if (ga.parsed_params & PARAM_HMAC_SECRET) { + uint8_t iv[16] = {0}; + block_cipher_config cfg = {.block_size = 16, .mode = CBC, .iv = iv, .encrypt = aes256_enc, .decrypt = aes256_dec}; + uint8_t *hmac_enc_key = ga.ext_hmac_secret_pin_protocol == 2 ? + ga.ext_hmac_secret_key_agreement + SHARED_SECRET_SIZE : + ga.ext_hmac_secret_key_agreement; + if (credential_counter == 0) { + ret = cp_decapsulate(ga.ext_hmac_secret_key_agreement, ga.ext_hmac_secret_pin_protocol); + CHECK_PARSER_RET(ret); + DBG_MSG("ext_hmac_secret_key_agreement: "); + PRINT_HEX(ga.ext_hmac_secret_key_agreement, ga.ext_hmac_secret_pin_protocol == 2 ? 64 : 32); + uint8_t hmac_buf[SHA256_DIGEST_LENGTH]; + hmac_sha256(ga.ext_hmac_secret_key_agreement, SHARED_SECRET_SIZE, ga.ext_hmac_secret_salt_enc, + ga.ext_hmac_secret_salt_len, + hmac_buf); + if (memcmp_s(hmac_buf, ga.ext_hmac_secret_salt_auth, HMAC_SECRET_SALT_AUTH_SIZE) != 0) + return CTAP2_ERR_EXTENSION_FIRST; + cfg.key = hmac_enc_key; + cfg.in_size = ga.ext_hmac_secret_salt_len; + cfg.in = ga.ext_hmac_secret_salt_enc; + cfg.out = ga.ext_hmac_secret_salt_enc; + block_cipher_dec(&cfg); + } + uint8_t hmac_secret_output[HMAC_SECRET_SALT_SIZE]; + DBG_MSG("hmac-secret-salt: "); + PRINT_HEX(ga.ext_hmac_secret_salt_enc, ga.ext_hmac_secret_salt_len); + ret = make_hmac_secret_output(dc.credential_id.nonce, ga.ext_hmac_secret_salt_enc, ga.ext_hmac_secret_salt_len, + hmac_secret_output, uv); CHECK_PARSER_RET(ret); - uint8_t hmac_buf[SHA256_DIGEST_LENGTH]; - hmac_sha256(ga.hmacSecretKeyAgreement, SHARED_SECRET_SIZE, ga.hmacSecretSaltEnc, ga.hmacSecretSaltLen, hmac_buf); - if (memcmp(hmac_buf, ga.hmacSecretSaltAuth, HMAC_SECRET_SALT_AUTH_SIZE) != 0) return CTAP2_ERR_EXTENSION_FIRST; - cfg.key = ga.hmacSecretKeyAgreement; - cfg.in_size = ga.hmacSecretSaltLen; - cfg.in = ga.hmacSecretSaltEnc; - cfg.out = ga.hmacSecretSaltEnc; - block_cipher_dec(&cfg); - } - - if (ga.uv) return CTAP2_ERR_UNSUPPORTED_OPTION; - if (ga.up) WAIT(); - - if (ga.parsedParams & PARAM_hmacSecret) { - ret = make_hmac_secret_output(rk.credential_id.nonce, ga.hmacSecretSaltEnc, ga.hmacSecretSaltLen, - ga.hmacSecretSaltEnc); - if (ret) return ret; - DBG_MSG("hmac-secret(plain): "); - PRINT_HEX(ga.hmacSecretSaltEnc, ga.hmacSecretSaltLen); - cfg.key = ga.hmacSecretKeyAgreement; - cfg.in_size = ga.hmacSecretSaltLen; - cfg.in = ga.hmacSecretSaltEnc; - cfg.out = ga.hmacSecretSaltEnc; + DBG_MSG("hmac-secret %s UV (plain): ", uv ? "with" : "without"); + PRINT_HEX(hmac_secret_output, ga.ext_hmac_secret_salt_len); + cfg.key = hmac_enc_key; + cfg.in_size = ga.ext_hmac_secret_salt_len; + cfg.in = hmac_secret_output; + cfg.out = hmac_secret_output; block_cipher_enc(&cfg); - memzero(ga.hmacSecretKeyAgreement, sizeof(ga.hmacSecretKeyAgreement)); + if (credential_counter + 1 == number_of_credentials) { // encryption key will not be used any more + memzero(ga.ext_hmac_secret_key_agreement, sizeof(ga.ext_hmac_secret_key_agreement)); + } - CborEncoder extensionEncoder; - CborEncoder map; - // build extensions - cbor_encoder_init(&extensionEncoder, extensionBuffer, sizeof(extensionBuffer), 0); - ret = cbor_encoder_create_map(&extensionEncoder, &map, 1); - CHECK_CBOR_RET(ret); ret = cbor_encode_text_stringz(&map, "hmac-secret"); CHECK_CBOR_RET(ret); - ret = cbor_encode_byte_string(&map, ga.hmacSecretSaltEnc, ga.hmacSecretSaltLen); - CHECK_CBOR_RET(ret); - ret = cbor_encoder_close_container(&extensionEncoder, &map); + ret = cbor_encode_byte_string(&map, hmac_secret_output, ga.ext_hmac_secret_salt_len); CHECK_CBOR_RET(ret); - - extensionSize = cbor_encoder_get_buffer_size(&extensionEncoder, extensionBuffer); - DBG_MSG("extensionSize=%hhu\n", extensionSize); } + ret = cbor_encoder_close_container(&extension_encoder, &map); + CHECK_CBOR_RET(ret); + extension_size = cbor_encoder_get_buffer_size(&extension_encoder, extension_buffer); + if (extension_size == 1) extension_size = 0; // Skip the empty map + DBG_MSG("extension_size=%hhu\n", extension_size); - // build response - CborEncoder map, sub_map; + // 13. Sign the client_data_hash along with authData with the selected credential. uint8_t map_items = 3; - if (ga.allowListSize == 0) ++map_items; - if (credential_idx == 0 && credential_numbers > 1) ++map_items; + if (dc.credential_id.nonce[CREDENTIAL_NONCE_DC_POS]) ++map_items; // user. For discoverable credentials on FIDO devices, at least user "id" is mandatory. + if (ga.allow_list_size == 0 && credential_counter == 0 && number_of_credentials > 1) ++map_items; // numberOfCredentials + if (dc.has_large_blob_key) ++map_items; // largeBlobKey ret = cbor_encoder_create_map(encoder, &map, map_items); CHECK_CBOR_RET(ret); // build credential id - ret = cbor_encode_int(&map, RESP_credential); + ret = cbor_encode_int(&map, GA_RESP_CREDENTIAL); CHECK_CBOR_RET(ret); ret = cbor_encoder_create_map(&map, &sub_map, 2); CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, "id"); - CHECK_CBOR_RET(ret); - ret = cbor_encode_byte_string(&sub_map, (const uint8_t *)&rk.credential_id, sizeof(CredentialId)); - CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, "type"); - CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, "public-key"); - CHECK_CBOR_RET(ret); + { + ret = cbor_encode_text_stringz(&sub_map, "id"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&sub_map, (const uint8_t *) &dc.credential_id, sizeof(credential_id)); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "type"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "public-key"); + CHECK_CBOR_RET(ret); + } ret = cbor_encoder_close_container(&map, &sub_map); CHECK_CBOR_RET(ret); // auth data len = sizeof(data_buf); - uint8_t flags = ((ga.parsedParams & PARAM_hmacSecret) ? FLAGS_ED : 0) | - (has_pin() && (ga.parsedParams & PARAM_pinAuth) > 0 ? FLAGS_UV : 0) | (ga.up ? FLAGS_UP : 0); - ret = ctap_make_auth_data(ga.rpIdHash, data_buf, flags, extensionSize, extensionBuffer, &len, rk.credential_id.alg_type); + uint8_t flags = (extension_size > 0 ? FLAGS_ED : 0) | (uv ? FLAGS_UV : 0) | (up ? FLAGS_UP : 0); + ret = ctap_make_auth_data(ga.rp_id_hash, data_buf, flags, extension_buffer, extension_size, &len, + dc.credential_id.alg_type, dc.credential_id.nonce[CREDENTIAL_NONCE_DC_POS], + dc.credential_id.nonce[CREDENTIAL_NONCE_CP_POS]); if (ret != 0) return ret; - ret = cbor_encode_int(&map, RESP_authData); + ret = cbor_encode_int(&map, MC_RESP_AUTH_DATA); CHECK_CBOR_RET(ret); ret = cbor_encode_byte_string(&map, data_buf, len); CHECK_CBOR_RET(ret); // signature - ret = cbor_encode_int(&map, RESP_signature); + ret = cbor_encode_int(&map, GA_RESP_SIGNATURE); CHECK_CBOR_RET(ret); - memcpy(data_buf + len, ga.clientDataHash, CLIENT_DATA_HASH_SIZE); + memcpy(data_buf + len, ga.client_data_hash, CLIENT_DATA_HASH_SIZE); DBG_MSG("Message: "); PRINT_HEX(data_buf, len + CLIENT_DATA_HASH_SIZE); - len = sign_with_private_key(rk.credential_id.alg_type, &key, data_buf, len + CLIENT_DATA_HASH_SIZE, data_buf); + len = sign_with_private_key(dc.credential_id.alg_type, &key, data_buf, len + CLIENT_DATA_HASH_SIZE, data_buf); DBG_MSG("Signature: "); PRINT_HEX(data_buf, len); ret = cbor_encode_byte_string(&map, data_buf, len); CHECK_CBOR_RET(ret); // user - if (ga.allowListSize == 0) { - // CTAP Spec: User identifiable information (name, DisplayName, icon) MUST not - // be returned if user verification is not done by the authenticator. - bool user_details = (ga.parsedParams & PARAM_pinAuth) && credential_numbers > 1; - ret = cbor_encode_int(&map, RESP_publicKeyCredentialUserEntity); - CHECK_CBOR_RET(ret); - ret = cbor_encoder_create_map(&map, &sub_map, user_details ? 4 : 1); - CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, "id"); + if (dc.credential_id.nonce[CREDENTIAL_NONCE_DC_POS]) { + ret = cbor_encode_int(&map, GA_RESP_PUBLIC_KEY_CREDENTIAL_USER_ENTITY); CHECK_CBOR_RET(ret); - ret = cbor_encode_byte_string(&sub_map, rk.user.id, rk.user.id_size); + ret = cbor_encoder_create_map(&map, &sub_map, user_details ? 3 : 1); CHECK_CBOR_RET(ret); - if (user_details) { - ret = cbor_encode_text_stringz(&sub_map, "icon"); - CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, (char *)rk.user.icon); - CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, "name"); - CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, (char *)rk.user.name); - CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, "displayName"); + { + ret = cbor_encode_text_stringz(&sub_map, "id"); CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&sub_map, (char *)rk.user.displayName); + ret = cbor_encode_byte_string(&sub_map, dc.user.id, dc.user.id_size); CHECK_CBOR_RET(ret); + if (user_details) { + ret = cbor_encode_text_stringz(&sub_map, "name"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, dc.user.name); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "displayName"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, dc.user.display_name); + CHECK_CBOR_RET(ret); + } } ret = cbor_encoder_close_container(&map, &sub_map); CHECK_CBOR_RET(ret); } - if (credential_idx == 0 && credential_numbers > 1) { - ret = cbor_encode_int(&map, RESP_numberOfCredentials); + if (ga.allow_list_size == 0 && credential_counter == 0 && number_of_credentials > 1) { + ret = cbor_encode_int(&map, GA_RESP_NUMBER_OF_CREDENTIALS); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, number_of_credentials); + CHECK_CBOR_RET(ret); + } + + if (dc.has_large_blob_key) { + ret = cbor_encode_int(&map, GA_RESP_LARGE_BLOB_KEY); CHECK_CBOR_RET(ret); - ret = cbor_encode_int(&map, credential_numbers); + ret = cbor_encode_byte_string(&map, dc.large_blob_key, LARGE_BLOB_KEY_SIZE); CHECK_CBOR_RET(ret); } ret = cbor_encoder_close_container(encoder, &map); CHECK_CBOR_RET(ret); - ++credential_idx; + ++credential_counter; + timer = device_get_tick(); return 0; } +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorGetNextAssertion static uint8_t ctap_get_next_assertion(CborEncoder *encoder) { - if (last_cmd != CTAP_GET_ASSERTION && last_cmd != CTAP_GET_NEXT_ASSERTION) return CTAP2_ERR_NOT_ALLOWED; - if (credential_idx >= credential_numbers) return CTAP2_ERR_NOT_ALLOWED; - return ctap_get_assertion(encoder, NULL, 0); + return ctap_get_assertion(encoder, NULL, 0, true); } static uint8_t ctap_get_info(CborEncoder *encoder) { - // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo - // Currently, we respond versions, aaguid, pin protocol. - CborEncoder map; - int ret = cbor_encoder_create_map(encoder, &map, 6); + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorGetInfo + CborEncoder map, sub_map; + int ret = cbor_encoder_create_map(encoder, &map, 13); CHECK_CBOR_RET(ret); // versions - ret = cbor_encode_int(&map, RESP_versions); + ret = cbor_encode_int(&map, GI_RESP_VERSIONS); CHECK_CBOR_RET(ret); CborEncoder array; - ret = cbor_encoder_create_array(&map, &array, 2); + ret = cbor_encoder_create_array(&map, &array, 3); CHECK_CBOR_RET(ret); { + ret = cbor_encode_text_stringz(&array, "U2F_V2"); + CHECK_CBOR_RET(ret); ret = cbor_encode_text_stringz(&array, "FIDO_2_0"); CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&array, "U2F_V2"); + ret = cbor_encode_text_stringz(&array, "FIDO_2_1"); CHECK_CBOR_RET(ret); } ret = cbor_encoder_close_container(&map, &array); CHECK_CBOR_RET(ret); // extensions - ret = cbor_encode_int(&map, RESP_extensions); + ret = cbor_encode_int(&map, GI_RESP_EXTENSIONS); CHECK_CBOR_RET(ret); - ret = cbor_encoder_create_array(&map, &array, 1); + ret = cbor_encoder_create_array(&map, &array, 4); CHECK_CBOR_RET(ret); { + ret = cbor_encode_text_stringz(&array, "credBlob"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&array, "credProtect"); + CHECK_CBOR_RET(ret); ret = cbor_encode_text_stringz(&array, "hmac-secret"); CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&array, "largeBlobKey"); + CHECK_CBOR_RET(ret); } ret = cbor_encoder_close_container(&map, &array); CHECK_CBOR_RET(ret); // aaguid - ret = cbor_encode_int(&map, RESP_aaguid); + ret = cbor_encode_int(&map, GI_RESP_AAGUID); CHECK_CBOR_RET(ret); ret = cbor_encode_byte_string(&map, aaguid, sizeof(aaguid)); CHECK_CBOR_RET(ret); // options - ret = cbor_encode_int(&map, RESP_options); + ret = cbor_encode_int(&map, GI_RESP_OPTIONS); CHECK_CBOR_RET(ret); CborEncoder option_map; - ret = cbor_encoder_create_map(&map, &option_map, 2); + ret = cbor_encoder_create_map(&map, &option_map, 6); + CHECK_CBOR_RET(ret); + { + ret = cbor_encode_text_stringz(&option_map, "rk"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_boolean(&option_map, true); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&option_map, "credMgmt"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_boolean(&option_map, true); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&option_map, "clientPin"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_boolean(&option_map, has_pin()); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&option_map, "largeBlobs"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_boolean(&option_map, true); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&option_map, "pinUvAuthToken"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_boolean(&option_map, true); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&option_map, "makeCredUvNotRqd"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_boolean(&option_map, true); + CHECK_CBOR_RET(ret); + } + ret = cbor_encoder_close_container(&map, &option_map); CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&option_map, "rk"); + + // maxMsgSize + ret = cbor_encode_int(&map, GI_RESP_MAX_MSG_SIZE); CHECK_CBOR_RET(ret); - ret = cbor_encode_boolean(&option_map, true); + ret = cbor_encode_int(&map, MAX_CTAP_BUFSIZE); CHECK_CBOR_RET(ret); - ret = cbor_encode_text_stringz(&option_map, "clientPin"); + + // pinUvAuthProtocols + ret = cbor_encode_int(&map, GI_RESP_PIN_UV_AUTH_PROTOCOLS); CHECK_CBOR_RET(ret); - ret = cbor_encode_boolean(&option_map, has_pin() > 0); + ret = cbor_encoder_create_array(&map, &array, 2); CHECK_CBOR_RET(ret); - ret = cbor_encoder_close_container(&map, &option_map); + { + ret = cbor_encode_int(&array, 1); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&array, 2); + CHECK_CBOR_RET(ret); + } + ret = cbor_encoder_close_container(&map, &array); CHECK_CBOR_RET(ret); - // max message length - ret = cbor_encode_int(&map, RESP_maxMsgSize); + // maxCredentialCountInList + ret = cbor_encode_int(&map, GI_RESP_MAX_CREDENTIAL_COUNT_IN_LIST); CHECK_CBOR_RET(ret); - ret = cbor_encode_int(&map, MAX_CTAP_BUFSIZE); + ret = cbor_encode_int(&map, MAX_CREDENTIAL_COUNT_IN_LIST); CHECK_CBOR_RET(ret); - // pin protocol - ret = cbor_encode_int(&map, RESP_pinProtocols); + // maxCredentialIdLength + ret = cbor_encode_int(&map, GI_RESP_MAX_CREDENTIAL_ID_LENGTH); CHECK_CBOR_RET(ret); - ret = cbor_encoder_create_array(&map, &array, 1); + ret = cbor_encode_int(&map, sizeof(credential_id)); + CHECK_CBOR_RET(ret); + + // transports + ret = cbor_encode_int(&map, GI_RESP_TRANSPORTS); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_create_array(&map, &array, 2); CHECK_CBOR_RET(ret); { - ret = cbor_encode_int(&array, 1); + ret = cbor_encode_text_stringz(&array, "nfc"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&array, "usb"); + CHECK_CBOR_RET(ret); + } + ret = cbor_encoder_close_container(&map, &array); + CHECK_CBOR_RET(ret); + + // algorithms + ret = cbor_encode_int(&map, GI_RESP_ALGORITHMS); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_create_array(&map, &array, 2); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_create_map(&array, &sub_map, 2); + CHECK_CBOR_RET(ret); + { + ret = cbor_encode_text_stringz(&sub_map, "alg"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&sub_map, -7); // ES256 (P-256) + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "type"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "public-key"); CHECK_CBOR_RET(ret); } + ret = cbor_encoder_close_container(&array, &sub_map); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_create_map(&array, &sub_map, 2); + CHECK_CBOR_RET(ret); + { + ret = cbor_encode_text_stringz(&sub_map, "alg"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&sub_map, -8); // EdDSA + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "type"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "public-key"); + CHECK_CBOR_RET(ret); + } + ret = cbor_encoder_close_container(&array, &sub_map); + CHECK_CBOR_RET(ret); ret = cbor_encoder_close_container(&map, &array); CHECK_CBOR_RET(ret); + // maxSerializedLargeBlobArray + ret = cbor_encode_int(&map, GI_RESP_MAX_SERIALIZED_LARGE_BLOB_ARRAY); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, LARGE_BLOB_SIZE_LIMIT); + CHECK_CBOR_RET(ret); + + // firmwareVersion + ret = cbor_encode_int(&map, GI_RESP_FIRMWARE_VERSION); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, FIRMWARE_VERSION); + CHECK_CBOR_RET(ret); + + // maxCredBlobLength + ret = cbor_encode_int(&map, GI_RESP_MAX_CRED_BLOB_LENGTH); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, MAX_CRED_BLOB_LENGTH); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(encoder, &map); CHECK_CBOR_RET(ret); return 0; @@ -670,210 +1353,789 @@ static uint8_t ctap_get_info(CborEncoder *encoder) { static uint8_t ctap_client_pin(CborEncoder *encoder, const uint8_t *params, size_t len) { CborParser parser; - CTAP_clientPin cp; + CTAP_client_pin cp; int ret = parse_client_pin(&parser, &cp, params, len); CHECK_PARSER_RET(ret); CborEncoder map, key_map; - uint8_t iv[16], hmac_buf[80], i; + uint8_t iv[16], buf[PIN_ENC_SIZE_P2 + PIN_HASH_SIZE_P2], i; memzero(iv, sizeof(iv)); - block_cipher_config cfg = {.block_size = 16, .mode = CBC, .iv = iv, .encrypt = aes256_enc, .decrypt = aes256_dec}; uint8_t *ptr; - int err, retries = 0; - switch (cp.subCommand) { - case CP_cmdGetRetries: - ret = cbor_encoder_create_map(encoder, &map, 1); - CHECK_CBOR_RET(ret); - ret = cbor_encode_int(&map, RESP_retries); - CHECK_CBOR_RET(ret); - ret = cbor_encode_int(&map, get_pin_retries()); - CHECK_CBOR_RET(ret); - ret = cbor_encoder_close_container(encoder, &map); - CHECK_CBOR_RET(ret); - break; + int err, retries; + switch (cp.sub_command) { + case CP_CMD_GET_PIN_RETRIES: + ret = cbor_encoder_create_map(encoder, &map, 1); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CP_RESP_PIN_RETRIES); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, get_pin_retries()); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(encoder, &map); + CHECK_CBOR_RET(ret); + break; - case CP_cmdGetKeyAgreement: - ret = cbor_encoder_create_map(encoder, &map, 1); - CHECK_CBOR_RET(ret); - ret = cbor_encode_int(&map, RESP_keyAgreement); - CHECK_CBOR_RET(ret); - // to save RAM, generate an empty key first, then fill it manually - ret = cbor_encoder_create_map(&map, &key_map, 0); - CHECK_CBOR_RET(ret); - ptr = key_map.data.ptr - 1; - memcpy(ptr, key_agreement_key.pub, PUB_KEY_SIZE); - build_cose_key(ptr, 1); - key_map.data.ptr = ptr + MAX_COSE_KEY_SIZE; - ret = cbor_encoder_close_container(&map, &key_map); - CHECK_CBOR_RET(ret); - ret = cbor_encoder_close_container(encoder, &map); - CHECK_CBOR_RET(ret); - break; + case CP_CMD_GET_KEY_AGREEMENT: + ret = cbor_encoder_create_map(encoder, &map, 1); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CP_RESP_KEY_AGREEMENT); + CHECK_CBOR_RET(ret); + // to save RAM, generate an empty key first, then fill it manually + ret = cbor_encoder_create_map(&map, &key_map, 0); + CHECK_CBOR_RET(ret); + ptr = key_map.data.ptr - 1; + cp_get_public_key(ptr); + build_cose_key(ptr, 1); + key_map.data.ptr = ptr + MAX_COSE_KEY_SIZE; + ret = cbor_encoder_close_container(&map, &key_map); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(encoder, &map); + CHECK_CBOR_RET(ret); + break; - case CP_cmdSetPin: - err = has_pin(); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - if (err > 0) return CTAP2_ERR_PIN_AUTH_INVALID; - ret = get_shared_secret(cp.keyAgreement); - CHECK_PARSER_RET(ret); - hmac_sha256(cp.keyAgreement, SHARED_SECRET_SIZE, cp.newPinEnc, sizeof(cp.newPinEnc), hmac_buf); -#ifndef FUZZ - if (memcmp(hmac_buf, cp.pinAuth, PIN_AUTH_SIZE) != 0) return CTAP2_ERR_PIN_AUTH_INVALID; -#endif - cfg.key = cp.keyAgreement; - cfg.in_size = MAX_PIN_SIZE + 1; - cfg.in = cp.newPinEnc; - cfg.out = cp.newPinEnc; - block_cipher_dec(&cfg); - i = 63; - while (i > 0 && cp.newPinEnc[i] == 0) - --i; - if (i < 3 || i >= 63) return CTAP2_ERR_PIN_POLICY_VIOLATION; - err = set_pin(cp.newPinEnc, i + 1); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - break; - - case CP_cmdChangePin: - err = has_pin(); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - if (err == 0) return CTAP2_ERR_PIN_NOT_SET; - err = get_pin_retries(); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + case CP_CMD_SET_PIN: + err = has_pin(); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (err > 0) return CTAP2_ERR_PIN_AUTH_INVALID; + ret = cp_decapsulate(cp.key_agreement, cp.pin_uv_auth_protocol); + CHECK_PARSER_RET(ret); + DBG_MSG("Shared Secret: "); + PRINT_HEX(cp.key_agreement, PUB_KEY_SIZE); + if (!cp_verify(cp.key_agreement, SHARED_SECRET_SIZE, cp.new_pin_enc, + cp.pin_uv_auth_protocol == 1 ? PIN_ENC_SIZE_P1 : PIN_ENC_SIZE_P2, cp.pin_uv_auth_param, + cp.pin_uv_auth_protocol)) { + ERR_MSG("CP verification failed\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + if (cp_decrypt(cp.key_agreement, cp.new_pin_enc, + cp.pin_uv_auth_protocol == 1 ? PIN_ENC_SIZE_P1 : PIN_ENC_SIZE_P2, + cp.new_pin_enc, cp.pin_uv_auth_protocol) != 0) { + ERR_MSG("CP decryption failed\n"); + return CTAP2_ERR_UNHANDLED_REQUEST; + } + DBG_MSG("Decrypted key: "); + PRINT_HEX(cp.new_pin_enc, 64); + i = 63; + while (i > 0 && cp.new_pin_enc[i] == 0) + --i; + if (i < 3 || i >= 63) return CTAP2_ERR_PIN_POLICY_VIOLATION; + err = set_pin(cp.new_pin_enc, i + 1); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + break; + + case CP_CMD_CHANGE_PIN: + err = has_pin(); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (err == 0) return CTAP2_ERR_PIN_NOT_SET; + err = get_pin_retries(); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; #ifndef FUZZ - if (err == 0) return CTAP2_ERR_PIN_BLOCKED; - retries = err - 1; + if (err == 0) return CTAP2_ERR_PIN_BLOCKED; + if (consecutive_pin_counter == 0) return CTAP2_ERR_PIN_AUTH_BLOCKED; + retries = err - 1; #endif - ret = get_shared_secret(cp.keyAgreement); - CHECK_PARSER_RET(ret); - memcpy(hmac_buf, cp.newPinEnc, sizeof(cp.newPinEnc)); - memcpy(hmac_buf + sizeof(cp.newPinEnc), cp.pinHashEnc, sizeof(cp.pinHashEnc)); - hmac_sha256(cp.keyAgreement, SHARED_SECRET_SIZE, hmac_buf, sizeof(cp.newPinEnc) + sizeof(cp.pinHashEnc), hmac_buf); + ret = cp_decapsulate(cp.key_agreement, cp.pin_uv_auth_protocol); + CHECK_PARSER_RET(ret); + if (cp.pin_uv_auth_protocol == 1) { + memcpy(buf, cp.new_pin_enc, PIN_ENC_SIZE_P1); + memcpy(buf + PIN_ENC_SIZE_P1, cp.pin_hash_enc, PIN_HASH_SIZE_P1); + ret = cp_verify(cp.key_agreement, SHARED_SECRET_SIZE, buf, PIN_ENC_SIZE_P1 + PIN_HASH_SIZE_P1, + cp.pin_uv_auth_param, cp.pin_uv_auth_protocol); + } else { + memcpy(buf, cp.new_pin_enc, PIN_ENC_SIZE_P2); + memcpy(buf + PIN_ENC_SIZE_P2, cp.pin_hash_enc, PIN_HASH_SIZE_P2); + ret = cp_verify(cp.key_agreement, SHARED_SECRET_SIZE, buf, PIN_ENC_SIZE_P2 + PIN_HASH_SIZE_P2, + cp.pin_uv_auth_param, cp.pin_uv_auth_protocol); + } + if (ret == false) { + ERR_MSG("CP verification failed\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + err = set_pin_retries(retries); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (cp_decrypt(cp.key_agreement, cp.pin_hash_enc, + cp.pin_uv_auth_protocol == 1 ? PIN_HASH_SIZE_P1 : PIN_HASH_SIZE_P2, + cp.pin_hash_enc, cp.pin_uv_auth_protocol)) { + ERR_MSG("CP decryption failed\n"); + return CTAP2_ERR_UNHANDLED_REQUEST; + } + err = verify_pin_hash(cp.pin_hash_enc); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; #ifndef FUZZ - if (memcmp(hmac_buf, cp.pinAuth, PIN_AUTH_SIZE) != 0) return CTAP2_ERR_PIN_AUTH_INVALID; + if (err > 0) { + cp_regenerate(); + if (retries == 0) return CTAP2_ERR_PIN_BLOCKED; + --consecutive_pin_counter; + if (consecutive_pin_counter == 0) return CTAP2_ERR_PIN_AUTH_BLOCKED; + return CTAP2_ERR_PIN_INVALID; + } #endif - err = set_pin_retries(retries); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - cfg.key = cp.keyAgreement; - cfg.in_size = PIN_HASH_SIZE; - cfg.in = cp.pinHashEnc; - cfg.out = cp.pinHashEnc; - block_cipher_dec(&cfg); - err = verify_pin_hash(cp.pinHashEnc); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + consecutive_pin_counter = 3; + if (cp_decrypt(cp.key_agreement, cp.new_pin_enc, + cp.pin_uv_auth_protocol == 1 ? PIN_ENC_SIZE_P1 : PIN_ENC_SIZE_P2, + cp.new_pin_enc, cp.pin_uv_auth_protocol) != 0) { + ERR_MSG("CP decryption failed\n"); + return CTAP2_ERR_UNHANDLED_REQUEST; + } + i = 63; + while (i > 0 && cp.new_pin_enc[i] == 0) + --i; + if (i < 3 || i >= 63) return CTAP2_ERR_PIN_POLICY_VIOLATION; + err = set_pin(cp.new_pin_enc, i + 1); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + break; + + case CP_CMD_GET_PIN_TOKEN: + case CP_CMD_GET_PIN_UV_AUTH_TOKEN_USING_PIN_WITH_PERMISSIONS: + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#getPinToken + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#getPinUvAuthTokenUsingPinWithPermissions + err = has_pin(); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (err == 0) return CTAP2_ERR_PIN_NOT_SET; + err = get_pin_retries(); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; #ifndef FUZZ - if (err > 0) { - if (retries == 0) return CTAP2_ERR_PIN_BLOCKED; - if (consecutive_pin_counter == 1) return CTAP2_ERR_PIN_AUTH_BLOCKED; - --consecutive_pin_counter; - return CTAP2_ERR_PIN_INVALID; - } + if (err == 0) return CTAP2_ERR_PIN_BLOCKED; + if (consecutive_pin_counter == 0) return CTAP2_ERR_PIN_AUTH_BLOCKED; + retries = err - 1; #endif - consecutive_pin_counter = 3; - cfg.key = cp.keyAgreement; - cfg.in_size = MAX_PIN_SIZE + 1; - cfg.in = cp.newPinEnc; - cfg.out = cp.newPinEnc; - block_cipher_dec(&cfg); - i = 63; - while (i > 0 && cp.newPinEnc[i] == 0) - --i; - if (i < 3 || i >= 63) return CTAP2_ERR_PIN_POLICY_VIOLATION; - err = set_pin(cp.newPinEnc, i + 1); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - break; - - case CP_cmdGetPinToken: - err = has_pin(); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - if (err == 0) return CTAP2_ERR_PIN_NOT_SET; - err = get_pin_retries(); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + ret = cp_decapsulate(cp.key_agreement, cp.pin_uv_auth_protocol); + CHECK_PARSER_RET(ret); + err = set_pin_retries(retries); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (cp_decrypt(cp.key_agreement, cp.pin_hash_enc, + cp.pin_uv_auth_protocol == 1 ? PIN_HASH_SIZE_P1 : PIN_HASH_SIZE_P2, + cp.pin_hash_enc, cp.pin_uv_auth_protocol)) { + ERR_MSG("CP decryption failed\n"); + return CTAP2_ERR_UNHANDLED_REQUEST; + } + err = verify_pin_hash(cp.pin_hash_enc); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; #ifndef FUZZ - if (err == 0) return CTAP2_ERR_PIN_BLOCKED; - retries = err - 1; + if (err > 0) { + if (retries == 0) return CTAP2_ERR_PIN_BLOCKED; + --consecutive_pin_counter; + if (consecutive_pin_counter == 0) return CTAP2_ERR_PIN_AUTH_BLOCKED; + return CTAP2_ERR_PIN_INVALID; + } #endif - ret = get_shared_secret(cp.keyAgreement); - CHECK_PARSER_RET(ret); - err = set_pin_retries(retries); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - cfg.key = cp.keyAgreement; - cfg.in_size = PIN_HASH_SIZE; - cfg.in = cp.pinHashEnc; - cfg.out = cp.pinHashEnc; - block_cipher_dec(&cfg); - err = verify_pin_hash(cp.pinHashEnc); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; -#ifndef FUZZ - if (err > 0) { - if (retries == 0) return CTAP2_ERR_PIN_BLOCKED; - if (consecutive_pin_counter == 1) return CTAP2_ERR_PIN_AUTH_BLOCKED; - --consecutive_pin_counter; - return CTAP2_ERR_PIN_INVALID; + consecutive_pin_counter = 3; + err = set_pin_retries(8); + if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + cp_reset_pin_uv_auth_token(); + cp_begin_using_uv_auth_token(false); + if (cp.sub_command == CP_CMD_GET_PIN_TOKEN) { + cp_set_permission(CP_PERMISSION_MC | CP_PERMISSION_GA); + } else { + cp_set_permission(cp.permissions); + if (cp.parsed_params & PARAM_RP) cp_associate_rp_id(cp.rp_id_hash); + } + cp_encrypt_pin_token(cp.key_agreement, buf, cp.pin_uv_auth_protocol); + ret = cbor_encoder_create_map(encoder, &map, 1); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CP_RESP_PIN_UV_AUTH_TOKEN); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&map, buf, cp.pin_uv_auth_protocol == 1 ? PIN_TOKEN_SIZE : PIN_TOKEN_SIZE + 16); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(encoder, &map); + CHECK_CBOR_RET(ret); + break; + } + + return 0; +} + +static int get_next_slot(uint64_t *slots, uint8_t *numbers) { + int idx = -1; + uint64_t val = *slots; + *numbers = 0; + for (int i = 0; i < 64; ++i) { + if (val & 1) { + ++*numbers; + if (idx == -1) idx = i; } -#endif - consecutive_pin_counter = 3; - err = set_pin_retries(8); - if (err < 0) return CTAP2_ERR_UNHANDLED_REQUEST; - cfg.in_size = PIN_TOKEN_SIZE; - cfg.in = pin_token; - cfg.out = hmac_buf; - block_cipher_enc(&cfg); + val >>= 1; + } + if (idx != -1) *slots &= ~(1ull << idx); + return idx; +} + +static uint8_t ctap_credential_management(CborEncoder *encoder, const uint8_t *params, size_t len) { + static uint8_t last_cm_cmd; + + CborParser parser; + CTAP_credential_management cm; + int ret = parse_credential_management(&parser, &cm, params, len); + CHECK_PARSER_RET(ret); + ret = ctap_consistency_check(); + CHECK_PARSER_RET(ret); + + static int idx, n_rp; // for rp enumeration + static uint64_t slots; // for credential enumeration + int size, counter; + CborEncoder map, sub_map; + uint8_t numbers = 0; + CTAP_rp_meta meta; + CTAP_discoverable_credential dc; + bool include_numbers; + + if (cm.sub_command == CM_CMD_GET_CREDS_METADATA || + cm.sub_command == CM_CMD_ENUMERATE_RPS_BEGIN || + cm.sub_command == CM_CMD_ENUMERATE_CREDENTIALS_BEGIN || + cm.sub_command == CM_CMD_DELETE_CREDENTIAL || + cm.sub_command == CM_CMD_UPDATE_USER_INFORMATION) { + last_cm_cmd = cm.sub_command; + uint8_t *buf = (uint8_t *) &dc; // buffer reuse + _Static_assert(sizeof(CTAP_dc_general_attr) < sizeof(dc), "CTAP_dc_general_attr buffer overflow"); + if (read_attr(DC_FILE, DC_GENERAL_ATTR, buf, sizeof(CTAP_dc_general_attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + numbers = ((CTAP_dc_general_attr*)buf)->numbers; + + buf[0] = cm.sub_command; + if (cm.param_len + 1 > sizeof(dc)) return CTAP1_ERR_INVALID_LENGTH; + if (cm.param_len > 0) memcpy(&buf[1], cm.sub_command_params_ptr, cm.param_len); + if (!consecutive_pin_counter) return CTAP2_ERR_PIN_AUTH_BLOCKED; + if (!cp_verify_pin_token(buf, cm.param_len + 1, cm.pin_uv_auth_param, cm.pin_uv_auth_protocol)) { + DBG_MSG("PIN verification error\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + if (!cp_has_permission(CP_PERMISSION_CM)) return CTAP2_ERR_PIN_AUTH_INVALID; + } + + DBG_MSG("processing cm.sub_command %hhu\n", cm.sub_command); + switch (cm.sub_command) { + case CM_CMD_GET_CREDS_METADATA: + if (cp_has_associated_rp_id()) return CTAP2_ERR_PIN_AUTH_INVALID; + ret = cbor_encoder_create_map(encoder, &map, 2); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_EXISTING_RESIDENT_CREDENTIALS_COUNT); + CHECK_CBOR_RET(ret); + DBG_MSG("Existing credentials: %d\n", numbers); + ret = cbor_encode_int(&map, numbers); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_MAX_POSSIBLE_REMAINING_RESIDENT_CREDENTIALS_COUNT); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, MAX_DC_NUM - numbers); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(encoder, &map); + CHECK_CBOR_RET(ret); + break; + + case CM_CMD_ENUMERATE_RPS_BEGIN: + if (cp_has_associated_rp_id()) return CTAP2_ERR_PIN_AUTH_INVALID; + if (numbers == 0) return CTAP2_ERR_NO_CREDENTIALS; + size = get_file_size(DC_META_FILE), counter = 0; + n_rp = size / (int) sizeof(CTAP_rp_meta); + for (int i = n_rp - 1; i >= 0; --i) { + size = read_file(DC_META_FILE, &meta, i * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (meta.slots > 0) { + idx = i; + ++counter; + } + } + DBG_MSG("%d RPs found\n", counter); + size = read_file(DC_META_FILE, &meta, idx * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + ret = cbor_encoder_create_map(encoder, &map, 3); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_RP); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_create_map(&map, &sub_map, 1); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "id"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_string(&sub_map, (const char *) meta.rp_id, meta.rp_id_len); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(&map, &sub_map); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_RP_ID_HASH); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&map, meta.rp_id_hash, SHA256_DIGEST_LENGTH); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_TOTAL_RPS); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, counter); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(encoder, &map); + CHECK_CBOR_RET(ret); + break; + + case CM_CMD_ENUMERATE_RPS_GET_NEXT_RP: + if (last_cmd != CTAP_CREDENTIAL_MANAGEMENT || + (last_cm_cmd != CM_CMD_ENUMERATE_RPS_BEGIN && last_cm_cmd != CM_CMD_ENUMERATE_RPS_GET_NEXT_RP)) { + last_cm_cmd = 0; + return CTAP2_ERR_NOT_ALLOWED; + } + last_cm_cmd = cm.sub_command; + for (int i = idx + 1; i < n_rp; ++i) { + size = read_file(DC_META_FILE, &meta, i * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (meta.slots > 0) { + DBG_MSG("Fetch RP at %d\n", i); + idx = i; + break; + } + } + ret = cbor_encoder_create_map(encoder, &map, 2); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_RP); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_create_map(&map, &sub_map, 1); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "id"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_string(&sub_map, (const char *) meta.rp_id, meta.rp_id_len); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(&map, &sub_map); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_RP_ID_HASH); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&map, meta.rp_id_hash, SHA256_DIGEST_LENGTH); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(encoder, &map); + CHECK_CBOR_RET(ret); + break; + + case CM_CMD_ENUMERATE_CREDENTIALS_BEGIN: + if (!cp_verify_rp_id(cm.rp_id_hash)) return CTAP2_ERR_PIN_AUTH_INVALID; + if (numbers == 0) return CTAP2_ERR_NO_CREDENTIALS; + include_numbers = true; + size = get_file_size(DC_META_FILE); + n_rp = size / (int) sizeof(CTAP_rp_meta); + for (idx = 0; idx < n_rp; ++idx) { + size = read_file(DC_META_FILE, &meta, idx * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (meta.slots == 0) continue; + if (memcmp_s(meta.rp_id_hash, cm.rp_id_hash, SHA256_DIGEST_LENGTH) == 0) break; + } + if (idx == n_rp) { + DBG_MSG("Specified RP not found\n"); + return CTAP2_ERR_NO_CREDENTIALS; + } + DBG_MSG("Use meta at slot %d: ", idx); + PRINT_HEX((const uint8_t *) &meta, sizeof(meta)); + slots = meta.slots; + generate_credential_response: + DBG_MSG("Current slot bitmap: 0x%llx\n", slots); + idx = get_next_slot(&slots, &numbers); + size = read_file(DC_FILE, &dc, idx * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + DBG_MSG("Slot %d printed\n", idx); + ret = cbor_encoder_create_map(encoder, &map, 4 + (uint8_t)include_numbers + (uint8_t)dc.has_large_blob_key); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_USER); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_create_map(&map, &sub_map, 3); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "id"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&sub_map, dc.user.id, dc.user.id_size); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "name"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, dc.user.name); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "displayName"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, dc.user.display_name); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(&map, &sub_map); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_CREDENTIAL_ID); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_create_map(&map, &sub_map, 2); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "id"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&sub_map, (const uint8_t *) &dc.credential_id, sizeof(credential_id)); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "type"); + CHECK_CBOR_RET(ret); + ret = cbor_encode_text_stringz(&sub_map, "public-key"); + CHECK_CBOR_RET(ret); + ret = cbor_encoder_close_container(&map, &sub_map); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, CM_RESP_PUBLIC_KEY); + CHECK_CBOR_RET(ret); + // to save RAM, generate an empty key first, then fill it manually + ret = cbor_encoder_create_map(&map, &sub_map, 0); + CHECK_CBOR_RET(ret); + ecc_key_t key; + ret = verify_key_handle(&dc.credential_id, &key); + if (ret != 0) return CTAP2_ERR_UNHANDLED_REQUEST; + key_type_t key_type = cose_alg_to_key_type(dc.credential_id.alg_type); + if (ecc_complete_key(key_type, &key) < 0) { + ERR_MSG("Failed to complete key\n"); + return -1; + } + uint8_t *ptr = sub_map.data.ptr - 1; + memcpy(ptr, key.pub, PUBLIC_KEY_LENGTH[key_type]); + if (dc.credential_id.alg_type == COSE_ALG_ES256) { + build_cose_key(ptr, 0); + sub_map.data.ptr = ptr + COSE_KEY_ES256_SIZE; + } else if (dc.credential_id.alg_type == COSE_ALG_EDDSA) { + build_ed25519_cose_key(ptr); + sub_map.data.ptr = ptr + COSE_KEY_EDDSA_SIZE; + } + ret = cbor_encoder_close_container(&map, &sub_map); + CHECK_CBOR_RET(ret); + if (include_numbers) { + ret = cbor_encode_int(&map, CM_RESP_TOTAL_CREDENTIALS); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, numbers); + CHECK_CBOR_RET(ret); + } + ret = cbor_encode_int(&map, CM_RESP_CRED_PROTECT); + CHECK_CBOR_RET(ret); + ret = cbor_encode_int(&map, dc.credential_id.nonce[CREDENTIAL_NONCE_CP_POS]); + CHECK_CBOR_RET(ret); + if (dc.has_large_blob_key) { + ret = cbor_encode_int(&map, CM_RESP_LARGE_BLOB_KEY); + CHECK_CBOR_RET(ret); + ret = cbor_encode_byte_string(&map, dc.large_blob_key, LARGE_BLOB_KEY_SIZE); + CHECK_CBOR_RET(ret); + } + ret = cbor_encoder_close_container(encoder, &map); + CHECK_CBOR_RET(ret); + break; + + case CM_CMD_ENUMERATE_CREDENTIALS_GET_NEXT_CREDENTIAL: + if (last_cmd != CTAP_CREDENTIAL_MANAGEMENT || ( + last_cm_cmd != CM_CMD_ENUMERATE_CREDENTIALS_BEGIN && + last_cm_cmd != CM_CMD_ENUMERATE_CREDENTIALS_GET_NEXT_CREDENTIAL)) { + last_cm_cmd = 0; + return CTAP2_ERR_NOT_ALLOWED; + } + last_cm_cmd = cm.sub_command; + include_numbers = false; + goto generate_credential_response; + + case CM_CMD_DELETE_CREDENTIAL: + if (!cp_verify_rp_id(cm.credential_id.rp_id_hash)) return CTAP2_ERR_PIN_AUTH_INVALID; + if (numbers == 0) return CTAP2_ERR_NO_CREDENTIALS; + size = get_file_size(DC_FILE); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + numbers = size / sizeof(CTAP_discoverable_credential); + for (idx = 0; idx < numbers; ++idx) { + size = read_file(DC_FILE, &dc, idx * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (dc.deleted) continue; + if (memcmp_s(&dc.credential_id, &cm.credential_id, sizeof(credential_id)) == 0) { + DBG_MSG("Found, credential_id: "); + PRINT_HEX((const uint8_t *) &dc.credential_id, sizeof(credential_id)); + break; + } + } + if (idx == numbers) return CTAP2_ERR_NO_CREDENTIALS; + + CTAP_dc_general_attr attr; + if (read_attr(DC_FILE, DC_GENERAL_ATTR, &attr, sizeof(attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + attr.index = (uint8_t)idx; + attr.pending_delete = 1; + if (write_attr(DC_FILE, DC_GENERAL_ATTR, &attr, sizeof(attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + + // delete dc first + dc.deleted = true; + if (write_file(DC_FILE, &dc, idx * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential), + 0) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + DBG_MSG("Slot %d deleted\n", idx); + // delete the meta then + size = get_file_size(DC_META_FILE); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + numbers = size / sizeof(CTAP_rp_meta); + for (int i = 0; i < numbers; ++i) { + size = read_file(DC_META_FILE, &meta, i * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (memcmp_s(meta.rp_id_hash, cm.credential_id.rp_id_hash, SHA256_DIGEST_LENGTH) == 0) { + DBG_MSG("Orig slot bitmap: 0x%llx\n", meta.slots); + meta.slots &= ~(1ull << idx); + DBG_MSG("New slot bitmap: 0x%llx\n", meta.slots); + size = write_file(DC_META_FILE, &meta, i * (int) sizeof(CTAP_rp_meta), sizeof(CTAP_rp_meta), 0); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + break; + } + } + attr.numbers--; + attr.pending_delete = 0; + if (write_attr(DC_FILE, DC_GENERAL_ATTR, &attr, sizeof(attr)) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + break; + + case CM_CMD_UPDATE_USER_INFORMATION: + if (!cp_verify_rp_id(cm.credential_id.rp_id_hash)) return CTAP2_ERR_PIN_AUTH_INVALID; + if (numbers == 0) return CTAP2_ERR_NO_CREDENTIALS; + // TODO: refactor this + size = get_file_size(DC_FILE); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + numbers = size / sizeof(CTAP_discoverable_credential); + for (idx = 0; idx < numbers; ++idx) { + size = read_file(DC_FILE, &dc, idx * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential)); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (dc.deleted) continue; + if (memcmp_s(&dc.credential_id, &cm.credential_id, sizeof(credential_id)) == 0) { + DBG_MSG("Found, credential_id: "); + PRINT_HEX((const uint8_t *) &dc.credential_id, sizeof(credential_id)); + break; + } + } + if (idx == numbers) { + DBG_MSG("No matching credential\n"); + return CTAP2_ERR_NO_CREDENTIALS; + } + if (dc.user.id_size != cm.user.id_size || memcmp_s(&dc.user.id, &cm.user.id, dc.user.id_size) != 0) { + DBG_MSG("Incorrect user id\n"); + return CTAP1_ERR_INVALID_PARAMETER; + } + memcpy(&dc.user, &cm.user, sizeof(user_entity)); + if (write_file(DC_FILE, &dc, idx * (int) sizeof(CTAP_discoverable_credential), + sizeof(CTAP_discoverable_credential), + 0) < 0) + return CTAP2_ERR_UNHANDLED_REQUEST; + DBG_MSG("Slot %d updated\n", idx); + break; + } + + + return 0; +} + +static uint8_t ctap_selection(void) { + WAIT(CTAP2_ERR_USER_ACTION_TIMEOUT); + return 0; +} + +static uint8_t ctap_reset_data(void) { + // If the request comes after 10 seconds of powering up, the authenticator returns CTAP2_ERR_NOT_ALLOWED. + if (device_get_tick() > 10000) { + return CTAP2_ERR_NOT_ALLOWED; + } + return ctap_install(1); +} + +static uint8_t ctap_large_blobs(CborEncoder *encoder, const uint8_t *params, size_t len) { + static uint16_t expectedNextOffset, expectedLength; + + CborParser parser; + CborEncoder map; + CTAP_large_blobs lb; + uint8_t buf[256]; // for pin auth + int ret = parse_large_blobs(&parser, &lb, params, len); + CHECK_PARSER_RET(ret); + + // 1. If offset is not present in the input map, return CTAP1_ERR_INVALID_PARAMETER. + // 2. If neither get nor set are present in the input map, return CTAP1_ERR_INVALID_PARAMETER. + // 3. If both get and set are present in the input map, return CTAP1_ERR_INVALID_PARAMETER. + // > Step 1-3 are checked when parsing. + + // 4. If get is present in the input map: + if (lb.parsed_params & PARAM_GET) { + // a) If length is present, return CTAP1_ERR_INVALID_PARAMETER. + // b) If either of pinUvAuthParam or pinUvAuthProtocol are present, return CTAP1_ERR_INVALID_PARAMETER. + // c) If the value of get is greater than maxFragmentLength, return CTAP1_ERR_INVALID_LENGTH. + // > Step a-c are checked when parsing. + + int size = get_file_size(LB_FILE); + if (size < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + // d) If the value of offset is greater than the length of the stored serialized large-blob array, + // return CTAP1_ERR_INVALID_PARAMETER. + if ((int)lb.offset > size) { + DBG_MSG("4-d not satisfied\n"); + return CTAP1_ERR_INVALID_PARAMETER; + } + // e) Return a CBOR map, as defined below, where the value of config is a substring of the stored serialized + // large-blob array. The substring SHOULD start at the offset given in offset and contain the number of bytes + // specified as get's value. If too few bytes exist at that offset, return the maximum number available. + // Note that if offset is equal to the length of the serialized large-blob array then this will result + // in a zero-length substring. + if (lb.offset + (int)lb.get > size) lb.get = size - lb.offset; + DBG_MSG("read %hu bytes at %hu\n", lb.get, lb.offset); ret = cbor_encoder_create_map(encoder, &map, 1); CHECK_CBOR_RET(ret); - ret = cbor_encode_int(&map, RESP_pinToken); + ret = cbor_encode_int(&map, LB_RESP_CONFIG); CHECK_CBOR_RET(ret); - ret = cbor_encode_byte_string(&map, hmac_buf, PIN_TOKEN_SIZE); + // to save RAM, we encode the buffer manually + uint8_t *ptr = map.data.ptr; + ret = cbor_encode_uint(&map, lb.get); CHECK_CBOR_RET(ret); + *ptr |= 0x40; // CBOR Major type 2 + if (read_file(LB_FILE, map.data.ptr, lb.offset, lb.get) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + map.data.ptr += lb.get; ret = cbor_encoder_close_container(encoder, &map); CHECK_CBOR_RET(ret); - break; + } else { + // 5. Else (implying that set is present in the input map): + // a) If the length of the value of set is greater than maxFragmentLength, return CTAP1_ERR_INVALID_LENGTH. + // > Checked when paring. + // b) If the value of offset is zero: + if (lb.offset == 0) { + // i. If length is not present, return CTAP1_ERR_INVALID_PARAMETER. + // ii. If the value of length is greater than 1024 bytes and exceeds the capacity of the device, + // return CTAP2_ERR_LARGE_BLOB_STORAGE_FULL. (Authenticators MUST be capable of storing at least 1024 bytes.) + // iii. If the value of length is less than 17, return CTAP1_ERR_INVALID_PARAMETER. + // > Step i - iii are checked when parsing. + + // iv. Set expectedLength to the value of length. + expectedLength = lb.length; + // v. Set expectedNextOffset to zero. + expectedNextOffset = 0; + } + // c) Else (i.e. the value of offset is not zero): + // If length is present, return CTAP1_ERR_INVALID_PARAMETER. + // > Checked when paring. + // d) If the value of offset is not equal to expectedNextOffset, return CTAP1_ERR_INVALID_SEQ. + if (lb.offset != expectedNextOffset) { + DBG_MSG("5-d not satisfied\n"); + return CTAP1_ERR_INVALID_SEQ; + } + // e) If the authenticator is protected by some form of user verification + // or the alwaysUv option ID is present and true: + if (has_pin()) { + // i. If pinUvAuthParam is absent from the input map, then end the operation by + // returning CTAP2_ERR_PUAT_REQUIRED. + if (!(lb.parsed_params & PARAM_PIN_UV_AUTH_PARAM)) { + DBG_MSG("5-e-i not satisfied\n"); + return CTAP2_ERR_PUAT_REQUIRED; + } + // ii. If pinUvAuthProtocol is absent from the input map, then end the operation by + // returning CTAP2_ERR_MISSING_PARAMETER. + if (!(lb.parsed_params & PARAM_PIN_UV_AUTH_PROTOCOL)) { + DBG_MSG("5-e-ii not satisfied\n"); + return CTAP2_ERR_MISSING_PARAMETER; + } + // iii. If pinUvAuthProtocol is not supported, return CTAP1_ERR_INVALID_PARAMETER. + // > Checked when paring. + // iv. The authenticator calls verify(pinUvAuthToken, 32×0xff || h’0c00' || uint32LittleEndian(offset) || + // SHA-256(contents of set byte string, i.e. not including an outer CBOR tag with major type two), + // pinUvAuthParam). + // If the verification fails, return CTAP2_ERR_PIN_AUTH_INVALID. + memset(buf, 0xFF, 32); + buf[32] = 0x0C; + buf[33] = 0x00; + buf[34] = lb.offset & 0xFF; + buf[35] = lb.offset >> 8; + buf[36] = 0x00; + buf[37] = 0x00; + sha256_raw(lb.set, lb.set_len, buf + 38); + if (!consecutive_pin_counter) return CTAP2_ERR_PIN_AUTH_BLOCKED; + if (!cp_verify_pin_token(buf, 70, lb.pin_uv_auth_param, lb.pin_uv_auth_protocol)) { + DBG_MSG("Fail to verify pin token\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + // v. Check if the pinUvAuthToken has the lbw permission, if not, return CTAP2_ERR_PIN_AUTH_INVALID. + if (!cp_has_permission(CP_PERMISSION_LBW)) { + DBG_MSG("Fail to verify pin permission\n"); + return CTAP2_ERR_PIN_AUTH_INVALID; + } + } + // f) If the sum of offset and the length of the value of set is greater than the value of expectedLength, + // return CTAP1_ERR_INVALID_PARAMETER. + if (lb.offset + lb.set_len > (size_t)expectedLength) { + DBG_MSG("5-g not satisfied, %hu + %zu > %hu\n", lb.offset, lb.set_len, expectedLength); + return CTAP1_ERR_INVALID_PARAMETER; + } + // g) If the value of offset is zero, prepare a buffer to receive a new serialized large-blob array. + // h) Append the value of set to the buffer containing the pending serialized large-blob array. + if (write_file(LB_FILE_TMP, lb.set, lb.offset, lb.set_len, lb.offset == 0) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + // i) Update expectedNextOffset to be the new length of the pending serialized large-blob array. + expectedNextOffset += lb.set_len; + // j) If the length of the pending serialized large-blob array is equal to expectedLength: + if (expectedNextOffset == expectedLength) { + // i. Verify that the final 16 bytes in the buffer are the truncated SHA-256 hash of the preceding bytes. + // If the hash does not match, return CTAP2_ERR_INTEGRITY_FAILURE. + int offset = 0; + expectedLength -= 16; + sha256_init(); + while (offset < expectedLength) { + int to_read = sizeof(buf); + if (to_read > expectedLength - offset) to_read = expectedLength - offset; + if (read_file(LB_FILE_TMP, buf, offset, to_read) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + sha256_update(buf, to_read); + offset += to_read; + } + sha256_final(buf); + if (read_file(LB_FILE_TMP, buf + 16, offset, 16) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + if (memcmp_s(buf, buf + 16, 16)) return CTAP2_ERR_INTEGRITY_FAILURE; + // ii. Commit the contents of the buffer as the new serialized large-blob array for this authenticator. + if (fs_rename(LB_FILE_TMP, LB_FILE) < 0) return CTAP2_ERR_UNHANDLED_REQUEST; + // iii. Return CTAP2_OK and an empty response. + } + // k) Else: + // i. More data is needed to complete the pending serialized large-blob array. + // ii. Return CTAP2_OK and an empty response. Await further writes. + // > DO NOTHING } - return 0; } int ctap_process_cbor(uint8_t *req, size_t req_len, uint8_t *resp, size_t *resp_len) { if (req_len-- == 0) return -1; + + cp_pin_uv_auth_token_usage_timer_observer(); + CborEncoder encoder; cbor_encoder_init(&encoder, resp + 1, *resp_len - 1, 0); uint8_t cmd = *req++; switch (cmd) { - case CTAP_MAKE_CREDENTIAL: - DBG_MSG("-----------------MC-------------------\n"); - *resp = ctap_make_credential(&encoder, req, req_len); - SET_RESP(); - break; - case CTAP_GET_ASSERTION: - DBG_MSG("-----------------GA-------------------\n"); - credential_idx = 0; - *resp = ctap_get_assertion(&encoder, req, req_len); - SET_RESP(); - break; - case CTAP_GET_NEXT_ASSERTION: - DBG_MSG("----------------NEXT------------------\n"); - *resp = ctap_get_next_assertion(&encoder); - SET_RESP(); - break; - case CTAP_GET_INFO: - DBG_MSG("-----------------GI-------------------\n"); - *resp = ctap_get_info(&encoder); - SET_RESP(); - break; - case CTAP_CLIENT_PIN: - DBG_MSG("-----------------CP-------------------\n"); - *resp = ctap_client_pin(&encoder, req, req_len); - SET_RESP(); - break; - case CTAP_RESET: - DBG_MSG("----------------RESET-----------------\n"); - *resp = ctap_install(1); - *resp_len = 1; - break; - default: - *resp = CTAP2_ERR_UNHANDLED_REQUEST; - *resp_len = 1; - break; + case CTAP_MAKE_CREDENTIAL: + DBG_MSG("-----------------MC-------------------\n"); + *resp = ctap_make_credential(&encoder, req, req_len); + SET_RESP(); + break; + case CTAP_GET_ASSERTION: + DBG_MSG("-----------------GA-------------------\n"); + *resp = ctap_get_assertion(&encoder, req, req_len, false); + SET_RESP(); + break; + case CTAP_GET_NEXT_ASSERTION: + DBG_MSG("----------------NEXT------------------\n"); + *resp = ctap_get_next_assertion(&encoder); + SET_RESP(); + break; + case CTAP_GET_INFO: + DBG_MSG("-----------------GI-------------------\n"); + *resp = ctap_get_info(&encoder); + SET_RESP(); + break; + case CTAP_CLIENT_PIN: + DBG_MSG("-----------------CP-------------------\n"); + *resp = ctap_client_pin(&encoder, req, req_len); + SET_RESP(); + break; + case CTAP_RESET: + DBG_MSG("----------------RESET-----------------\n"); + *resp = ctap_reset_data(); + *resp_len = 1; + break; + case CTAP_CRED_MANAGE_LEGACY: // compatible with old libfido2 + cmd = CTAP_CREDENTIAL_MANAGEMENT; + case CTAP_CREDENTIAL_MANAGEMENT: + DBG_MSG("----------------CM--------------------\n"); + *resp = ctap_credential_management(&encoder, req, req_len); + SET_RESP(); + break; + case CTAP_SELECTION: + DBG_MSG("----------------SELECTION-------------\n"); + *resp = ctap_selection(); + SET_RESP(); + break; + case CTAP_LARGE_BLOBS: + DBG_MSG("----------------LB--------------------\n"); + *resp = ctap_large_blobs(&encoder, req, req_len); + SET_RESP(); + break; + case CTAP_CONFIG: + DBG_MSG("----------------CONFIG----------------\n"); + *resp = CTAP2_ERR_UNHANDLED_REQUEST; + *resp_len = 1; + break; + default: + *resp = CTAP2_ERR_UNHANDLED_REQUEST; + *resp_len = 1; + break; } last_cmd = cmd; return 0; @@ -896,22 +2158,22 @@ int ctap_process_apdu(const CAPDU *capdu, RAPDU *rapdu) { } } else if (CLA == 0x00) { switch (INS) { - case U2F_REGISTER: - ret = u2f_register(capdu, rapdu); - break; - case U2F_AUTHENTICATE: - ret = u2f_authenticate(capdu, rapdu); - break; - case U2F_VERSION: - ret = u2f_version(capdu, rapdu); - break; - case U2F_SELECT: - ret = u2f_select(capdu, rapdu); - break; - case CTAP_INS_MSG: - break; - default: - EXCEPT(SW_INS_NOT_SUPPORTED); + case U2F_REGISTER: + ret = u2f_register(capdu, rapdu); + break; + case U2F_AUTHENTICATE: + ret = u2f_authenticate(capdu, rapdu); + break; + case U2F_VERSION: + ret = u2f_version(capdu, rapdu); + break; + case U2F_SELECT: + ret = u2f_select(capdu, rapdu); + break; + case CTAP_INS_MSG: + break; + default: + EXCEPT(SW_INS_NOT_SUPPORTED); } } else EXCEPT(SW_CLA_NOT_SUPPORTED); @@ -921,3 +2183,9 @@ int ctap_process_apdu(const CAPDU *capdu, RAPDU *rapdu) { else return 0; } + +int ctap_wink(void) { + start_blinking_interval(1, 50); + return 0; +} + diff --git a/applets/ctap/secret.c b/applets/ctap/secret.c index b6551458..ebdfb307 100644 --- a/applets/ctap/secret.c +++ b/applets/ctap/secret.c @@ -1,27 +1,205 @@ // SPDX-License-Identifier: Apache-2.0 +#include "cose-key.h" #include "secret.h" -#include +#include +#include #include #include #include #include #include -#include "cose-key.h" +#include -static int key_type_to_cose_alg(key_type_t type) { - switch (type) { - case SECP256R1: - return COSE_ALG_ES256; - case ED25519: - return COSE_ALG_EDDSA; - case SM2: - return COSE_ALG_SM2; - default: - return -65536; +static uint8_t pin_token[PIN_TOKEN_SIZE]; +static ecc_key_t ka_key; +static uint8_t permissions_rp_id[SHA256_DIGEST_LENGTH + 1]; // the first byte indicates nullable (0: null, 1: not null) +static uint8_t permissions; +static bool in_use; +static bool user_verified; +static bool user_present; +static uint32_t timeout_value; + +// utility functions + +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pinuvauthprotocol-beginusingpinuvauthtoken +void cp_begin_using_uv_auth_token(bool user_is_present) { + user_present = user_is_present; + user_verified = true; + timeout_value = device_get_tick() + 30000; + in_use = true; +} + +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pinuvauthprotocol-pinuvauthtokenusagetimerobserver +void cp_pin_uv_auth_token_usage_timer_observer(void) { + if (!in_use) return; + if (device_get_tick() > timeout_value) { + cp_clear_user_present_flag(); + cp_stop_using_pin_uv_auth_token(); } } -static key_type_t cose_alg_to_key_type(int alg) { +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pinuvauthprotocol-getuserpresentflagvalue +bool cp_get_user_present_flag_value(void) { + if (in_use) return user_present; + return false; +} + +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pinuvauthprotocol-getuserverifiedflagvalue +bool cp_get_user_verified_flag_value(void) { + if (in_use) return user_verified; + return false; +} + +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pinuvauthprotocol-clearuserpresentflag +void cp_clear_user_present_flag(void) { + if (in_use) user_present = false; +} + +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pinuvauthprotocol-clearuserpresentflag +void cp_clear_user_verified_flag(void) { + if (in_use) user_verified = false; +} + +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pinuvauthprotocol-clearpinuvauthtokenpermissionsexceptlbw +void cp_clear_pin_uv_auth_token_permissions_except_lbw(void) { + if (in_use) permissions &= ~CP_PERMISSION_LBW; +} + +void cp_stop_using_pin_uv_auth_token(void) { + permissions_rp_id[0] = 0; + permissions = 0; + in_use = false; + user_verified = false; + user_present = false; +} + +// pin auth protocol + +static void hkdf(uint8_t *salt, size_t salt_len, uint8_t *ikm, size_t ikm_len, uint8_t *out) { + hmac_sha256(salt, salt_len, ikm, ikm_len, salt); + hmac_sha256(salt, SHA256_DIGEST_LENGTH, (const uint8_t *) "CTAP2 HMAC key\x01", 15, out); + hmac_sha256(salt, SHA256_DIGEST_LENGTH, (const uint8_t *) "CTAP2 AES key\x01", 14, out + SHA256_DIGEST_LENGTH); +} + +static void cp2_kdf(uint8_t *z, size_t z_len, uint8_t *out) { + uint8_t salt[32] = {0}; + hkdf(salt, sizeof(salt), z, z_len, out); +} + +void cp_initialize(void) { + cp_regenerate(); + cp_reset_pin_uv_auth_token(); +} + +void cp_regenerate(void) { + ecc_generate(SECP256R1, &ka_key); + DBG_MSG("Regenerate:\nPri: "); + PRINT_HEX(ka_key.pri, PRIVATE_KEY_LENGTH[SECP256R1]); + DBG_MSG("Pub: "); + PRINT_HEX(ka_key.pub, PUBLIC_KEY_LENGTH[SECP256R1]); +} + +void cp_reset_pin_uv_auth_token(void) { + random_buffer(pin_token, sizeof(pin_token)); + cp_stop_using_pin_uv_auth_token(); +} + +void cp_get_public_key(uint8_t *buf) { + memcpy(buf, ka_key.pub, PUBLIC_KEY_LENGTH[SECP256R1]); +} + +int cp_decapsulate(uint8_t *buf, int pin_protocol) { + int ret = ecdh(SECP256R1, ka_key.pri, buf, buf); + if (ret < 0) return 1; + if (pin_protocol == 1) + sha256_raw(buf, PRI_KEY_SIZE, buf); + else + cp2_kdf(buf, PRI_KEY_SIZE, buf); + return 0; +} + +int cp_encrypt(const uint8_t *key, const uint8_t *in, size_t in_size, uint8_t *out, int pin_protocol) { + uint8_t iv[16]; + block_cipher_config cfg = {.block_size = 16, .mode = CBC, .iv = iv, .encrypt = aes256_enc, .decrypt = aes256_dec}; + cfg.in_size = in_size; + cfg.in = in; + if (pin_protocol == 1) { + memzero(iv, sizeof(iv)); + cfg.key = key; + cfg.out = out; + } else { + random_buffer(iv, sizeof(iv)); + cfg.key = key + 32; + cfg.out = out + sizeof(iv); + memcpy(out, iv, sizeof(iv)); + } + return block_cipher_enc(&cfg); +} + +int cp_encrypt_pin_token(const uint8_t *key, uint8_t *out, int pin_protocol) { + return cp_encrypt(key, pin_token, PIN_TOKEN_SIZE, out, pin_protocol); +} + +int cp_decrypt(const uint8_t *key, const uint8_t *in, size_t in_size, uint8_t *out, int pin_protocol) { + uint8_t iv[16]; + block_cipher_config cfg = {.block_size = 16, .mode = CBC, .iv = iv, .encrypt = aes256_enc, .decrypt = aes256_dec}; + if (pin_protocol == 1) { + memzero(iv, sizeof(iv)); + cfg.key = key; + cfg.in_size = in_size; + cfg.in = in; + cfg.out = out; + } else { + if (in_size < sizeof(iv)) return -1; + memcpy(iv, in, sizeof(iv)); + cfg.key = key + 32; + cfg.in_size = in_size - sizeof(iv); + cfg.in = in + sizeof(iv); + cfg.out = out; + } + return block_cipher_dec(&cfg); +} + +bool cp_verify(const uint8_t *key, size_t key_len, const uint8_t *msg, size_t msg_len, const uint8_t *sig, + int pin_protocol) { + uint8_t buf[SHA256_DIGEST_LENGTH]; + if (pin_protocol == 2 && key_len > SHA256_DIGEST_LENGTH) key_len = SHA256_DIGEST_LENGTH; + hmac_sha256(key, key_len, msg, msg_len, buf); + if (pin_protocol == 1) + return memcmp_s(buf, sig, PIN_AUTH_SIZE_P1) == 0; + else + return memcmp_s(buf, sig, SHA256_DIGEST_LENGTH) == 0; +} + +bool cp_verify_pin_token(const uint8_t *msg, size_t msg_len, const uint8_t *sig, int pin_protocol) { + if (!in_use) return false; + timeout_value = device_get_tick() + 30000; + return cp_verify(pin_token, PIN_TOKEN_SIZE, msg, msg_len, sig, pin_protocol); +} + +void cp_set_permission(int new_permissions) { + permissions |= new_permissions; +} + +bool cp_has_permission(int permission) { + return permissions & permission; +} + +bool cp_has_associated_rp_id(void) { + return permissions_rp_id[0] == 1; +} + +bool cp_verify_rp_id(const uint8_t *rp_id_hash) { + if (permissions_rp_id[0] == 0) return true; + return memcmp_s(&permissions_rp_id[1], rp_id_hash, SHA256_DIGEST_LENGTH) == 0; +} + +void cp_associate_rp_id(const uint8_t *rp_id_hash) { + permissions_rp_id[0] = 1; + memcpy(&permissions_rp_id[1], rp_id_hash, SHA256_DIGEST_LENGTH); +} + +key_type_t cose_alg_to_key_type(int alg) { switch (alg) { case COSE_ALG_ES256: return SECP256R1; @@ -61,40 +239,59 @@ int increase_counter(uint32_t *counter) { return 0; } -static void generate_credential_id_nonce_tag(CredentialId *kh, ecc_key_t *key) { +static void generate_credential_id_nonce_tag(credential_id *kh, uint8_t kh_key[KH_KEY_SIZE], ecc_key_t *key) { // works for ECC algorithms with a 256-bit private key - random_buffer(kh->nonce, sizeof(kh->nonce)); - // private key = hmac-sha256(device private key, nonce), stored in key.pri - hmac_sha256(key->pub, KH_KEY_SIZE, kh->nonce, sizeof(kh->nonce), key->pri); + random_buffer(kh->nonce, CREDENTIAL_NONCE_SIZE); + // private key = hmac-sha256(device private key, nonce) + hmac_sha256(kh_key, KH_KEY_SIZE, kh->nonce, sizeof(kh->nonce), key->pri); DBG_MSG("Device key: "); - PRINT_HEX(key->pub, KH_KEY_SIZE); + PRINT_HEX(kh_key, KH_KEY_SIZE); DBG_MSG("Nonce: "); PRINT_HEX(kh->nonce, sizeof(kh->nonce)); DBG_MSG("Private key: "); PRINT_HEX(key->pri, KH_KEY_SIZE); // tag = left(hmac-sha256(private key, rpIdHash or appid), 16), stored in kh.tag via key.pub - hmac_sha256(key->pri, KH_KEY_SIZE, kh->rpIdHash, sizeof(kh->rpIdHash), key->pub); + hmac_sha256(key->pri, KH_KEY_SIZE, kh->rp_id_hash, sizeof(kh->rp_id_hash), key->pub); memcpy(kh->tag, key->pub, sizeof(kh->tag)); } -int generate_key_handle(CredentialId *kh, uint8_t *pubkey, int32_t alg_type) { - ecc_key_t key; +bool check_credential_protect_requirements(credential_id *kh, bool with_cred_list, bool uv) { + DBG_MSG("credProtect: %hhu\n", kh->nonce[CREDENTIAL_NONCE_CP_POS]); + if (kh->nonce[CREDENTIAL_NONCE_CP_POS] == CRED_PROTECT_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST) { + if (!uv && !with_cred_list) { + DBG_MSG("credentialProtectionPolicy (0x02) failed\n"); + return false; + } + } else if (kh->nonce[CREDENTIAL_NONCE_CP_POS] == CRED_PROTECT_VERIFICATION_REQUIRED) { + if (!uv) { + DBG_MSG("credentialProtectionPolicy (0x03) failed\n"); + return false; + } + } + return true; +} - int ret = read_kh_key(key.pub); // use key.pub to store kh key first - if (ret < 0) return ret; +int generate_key_handle(credential_id *kh, uint8_t *pubkey, int32_t alg_type, uint8_t dc, uint8_t cp) { + ecc_key_t key; + uint8_t kh_key[KH_KEY_SIZE]; if (alg_type != COSE_ALG_ES256 && alg_type != COSE_ALG_EDDSA && alg_type != COSE_ALG_SM2) { DBG_MSG("Unsupported algo key_type\n"); - memzero(&key, sizeof(key)); return -1; } kh->alg_type = alg_type; key_type_t key_type = cose_alg_to_key_type(alg_type); + kh->nonce[CREDENTIAL_NONCE_DC_POS] = dc; + kh->nonce[CREDENTIAL_NONCE_CP_POS] = cp; + + int ret = read_kh_key(kh_key); + if (ret < 0) return ret; do { - generate_credential_id_nonce_tag(kh, &key); + generate_credential_id_nonce_tag(kh, kh_key, &key); } while (ecc_complete_key(key_type, &key) < 0); + memzero(kh_key, KH_KEY_SIZE); memcpy(pubkey, key.pub, PUBLIC_KEY_LENGTH[key_type]); DBG_MSG("Public: "); @@ -104,9 +301,10 @@ int generate_key_handle(CredentialId *kh, uint8_t *pubkey, int32_t alg_type) { return 0; } -int verify_key_handle(const CredentialId *kh, ecc_key_t *key) { +int verify_key_handle(const credential_id *kh, ecc_key_t *key) { int ret = read_kh_key(key->pub); // use key.pub to store kh key first if (ret < 0) return ret; + // get private key hmac_sha256(key->pub, KH_KEY_SIZE, kh->nonce, sizeof(kh->nonce), key->pri); DBG_MSG("Device key: "); @@ -116,8 +314,8 @@ int verify_key_handle(const CredentialId *kh, ecc_key_t *key) { DBG_MSG("Private key: "); PRINT_HEX(key->pri, KH_KEY_SIZE); // get tag, store in key->pub, which should be verified first outside this function - hmac_sha256(key->pri, KH_KEY_SIZE, kh->rpIdHash, sizeof(kh->rpIdHash), key->pub); - if (memcmp(key->pub, kh->tag, sizeof(kh->tag)) != 0) { + hmac_sha256(key->pri, KH_KEY_SIZE, kh->rp_id_hash, sizeof(kh->rp_id_hash), key->pub); + if (memcmp_s(key->pub, kh->tag, sizeof(kh->tag)) != 0) { DBG_MSG("Incorrect key handle\n"); memzero(key, sizeof(ecc_key_t)); return 1; @@ -167,9 +365,9 @@ int sign_with_private_key(int32_t alg_type, ecc_key_t *key, const uint8_t *input int get_cert(uint8_t *buf) { return read_file(CTAP_CERT_FILE, buf, 0, MAX_CERT_SIZE); } -int has_pin(void) { +bool has_pin(void) { uint8_t tmp; - return read_attr(CTAP_CERT_FILE, PIN_ATTR, &tmp, 1); + return read_attr(CTAP_CERT_FILE, PIN_ATTR, &tmp, 1) != 0; } int set_pin(uint8_t *buf, uint8_t length) { @@ -178,7 +376,7 @@ int set_pin(uint8_t *buf, uint8_t length) { err = write_attr(CTAP_CERT_FILE, PIN_ATTR, NULL, 0); } else { sha256_raw(buf, length, buf); - err = write_attr(CTAP_CERT_FILE, PIN_ATTR, buf, PIN_HASH_SIZE); + err = write_attr(CTAP_CERT_FILE, PIN_ATTR, buf, PIN_HASH_SIZE_P1); // We only compare the first 16 bytes } if (err < 0) return err; uint8_t ctr = 8; @@ -186,10 +384,10 @@ int set_pin(uint8_t *buf, uint8_t length) { } int verify_pin_hash(uint8_t *buf) { - uint8_t storedPinHash[PIN_HASH_SIZE]; - int err = read_attr(CTAP_CERT_FILE, PIN_ATTR, storedPinHash, PIN_HASH_SIZE); + uint8_t storedPinHash[PIN_HASH_SIZE_P1]; // We only compare the first 16 bytes + int err = read_attr(CTAP_CERT_FILE, PIN_ATTR, storedPinHash, PIN_HASH_SIZE_P1); if (err < 0) return err; - if (memcmp(storedPinHash, buf, PIN_HASH_SIZE) == 0) return 0; + if (memcmp_s(storedPinHash, buf, PIN_HASH_SIZE_P1) == 0) return 0; return 1; } @@ -202,12 +400,17 @@ int get_pin_retries(void) { int set_pin_retries(uint8_t ctr) { return write_attr(CTAP_CERT_FILE, PIN_CTR_ATTR, &ctr, 1); } -int make_hmac_secret_output(uint8_t *nonce, uint8_t *salt, uint8_t len, uint8_t *output) { +int make_hmac_secret_output(uint8_t *nonce, uint8_t *salt, uint8_t len, uint8_t *output, bool uv) { uint8_t hmac_buf[SHA256_DIGEST_LENGTH]; - // use hmac-sha256(HE_KEY, CredentialId::nonce) as CredRandom + // use hmac-sha256(HE_KEY, credential_id::nonce) as CredRandom int err = read_he_key(hmac_buf); if (err < 0) return err; + if (uv) { + for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) + hmac_buf[i] = ~hmac_buf[i]; + } + hmac_sha256(hmac_buf, HE_KEY_SIZE, nonce, CREDENTIAL_NONCE_SIZE, hmac_buf); hmac_sha256(hmac_buf, HE_KEY_SIZE, salt, 32, output); if (len == 64) hmac_sha256(hmac_buf, HE_KEY_SIZE, salt + 32, 32, output + 32); diff --git a/applets/ctap/secret.h b/applets/ctap/secret.h index ddfe1071..b7387f2e 100644 --- a/applets/ctap/secret.h +++ b/applets/ctap/secret.h @@ -5,17 +5,47 @@ #include "ctap-internal.h" #include +// utility functions +void cp_begin_using_uv_auth_token(bool user_is_present); +void cp_pin_uv_auth_token_usage_timer_observer(void); +bool cp_get_user_present_flag_value(void); +bool cp_get_user_verified_flag_value(void); +void cp_clear_user_present_flag(void); +void cp_clear_user_verified_flag(void); +void cp_clear_pin_uv_auth_token_permissions_except_lbw(void); +void cp_stop_using_pin_uv_auth_token(void); + +// pin auth protocol +void cp_initialize(void); +void cp_regenerate(void); +void cp_reset_pin_uv_auth_token(void); +void cp_get_public_key(uint8_t *buf); +int cp_decapsulate(uint8_t *buf, int pin_protocol); +int cp_encrypt(const uint8_t *key, const uint8_t *in, size_t in_size, uint8_t *out, int pin_protocol); +int cp_encrypt_pin_token(const uint8_t *key, uint8_t *out, int pin_protocol); +int cp_decrypt(const uint8_t *key, const uint8_t *in, size_t in_size, uint8_t *out, int pin_protocol); +bool cp_verify(const uint8_t *key, size_t key_len, const uint8_t *msg, size_t msg_len, const uint8_t *sig, int pin_protocol); +bool cp_verify_pin_token(const uint8_t *msg, size_t msg_len, const uint8_t *sig, int pin_protocol); + +void cp_set_permission(int new_permissions); +bool cp_has_permission(int permission); +bool cp_has_associated_rp_id(void); +bool cp_verify_rp_id(const uint8_t *rp_id_hash); +void cp_associate_rp_id(const uint8_t *rp_id_hash); +key_type_t cose_alg_to_key_type(int alg); + int increase_counter(uint32_t *counter); -int generate_key_handle(CredentialId *kh, uint8_t *pubkey, int32_t alg_type); +int generate_key_handle(credential_id *kh, uint8_t *pubkey, int32_t alg_type, uint8_t dc, uint8_t cp); size_t sign_with_device_key(const uint8_t *input, size_t input_len, uint8_t *sig); int sign_with_private_key(int32_t alg_type, ecc_key_t *key, const uint8_t *input, size_t len, uint8_t *sig); -int verify_key_handle(const CredentialId *kh, ecc_key_t *key); +int verify_key_handle(const credential_id *kh, ecc_key_t *key); +bool check_credential_protect_requirements(credential_id *kh, bool with_cred_list, bool uv); int get_cert(uint8_t *buf); -int has_pin(void); +bool has_pin(void); int set_pin(uint8_t *buf, uint8_t length); int verify_pin_hash(uint8_t *buf); int get_pin_retries(void); int set_pin_retries(uint8_t ctr); -int make_hmac_secret_output(uint8_t *nonce, uint8_t *salt, uint8_t len, uint8_t *output); +int make_hmac_secret_output(uint8_t *nonce, uint8_t *salt, uint8_t len, uint8_t *output, bool uv); #endif // CANOKEY_CORE_FIDO2_SECRET_H_ diff --git a/applets/ctap/u2f.c b/applets/ctap/u2f.c index 21398491..755b375b 100644 --- a/applets/ctap/u2f.c +++ b/applets/ctap/u2f.c @@ -22,19 +22,19 @@ int u2f_register(const CAPDU *capdu, RAPDU *rapdu) { stop_blinking(); } - U2F_REGISTER_REQ *req = (U2F_REGISTER_REQ *)DATA; - U2F_REGISTER_RESP *resp = (U2F_REGISTER_RESP *)RDATA; - CredentialId kh; + U2F_REGISTER_REQ *req = (U2F_REGISTER_REQ *) DATA; + U2F_REGISTER_RESP *resp = (U2F_REGISTER_RESP *) RDATA; + credential_id kh; uint8_t digest[SHA256_DIGEST_LENGTH]; uint8_t pubkey[PUB_KEY_SIZE]; - memcpy(kh.rpIdHash, req->appId, U2F_APPID_SIZE); - int err = generate_key_handle(&kh, pubkey, COSE_ALG_ES256); + memcpy(kh.rp_id_hash, req->appId, U2F_APPID_SIZE); + int err = generate_key_handle(&kh, pubkey, COSE_ALG_ES256, 0, CRED_PROTECT_VERIFICATION_OPTIONAL); if (err < 0) return err; // there are overlaps between req and resp sha256_init(); - sha256_update((uint8_t[]){0x00}, 1); + sha256_update((uint8_t[]) {0x00}, 1); sha256_update(req->appId, U2F_APPID_SIZE); sha256_update(req->chal, U2F_CHAL_SIZE); @@ -45,34 +45,34 @@ int u2f_register(const CAPDU *capdu, RAPDU *rapdu) { resp->pubKey.pointFormat = U2F_POINT_UNCOMPRESSED; memcpy(resp->pubKey.x, pubkey, PUB_KEY_SIZE); // accessing out of bounds is intentional. // KEY HANDLE LENGTH (1) - resp->keyHandleLen = sizeof(CredentialId); + resp->keyHandleLen = sizeof(credential_id); // KEY HANDLE (128) - memcpy(resp->keyHandleCertSig, &kh, sizeof(CredentialId)); + memcpy(resp->keyHandleCertSig, &kh, sizeof(credential_id)); // CERTIFICATE (var) - int cert_len = read_file(CTAP_CERT_FILE, resp->keyHandleCertSig + sizeof(CredentialId), 0, U2F_MAX_ATT_CERT_SIZE); + int cert_len = read_file(CTAP_CERT_FILE, resp->keyHandleCertSig + sizeof(credential_id), 0, U2F_MAX_ATT_CERT_SIZE); if (cert_len < 0) return cert_len; // SIG (var) - sha256_update((const uint8_t *)&kh, sizeof(CredentialId)); - sha256_update((const uint8_t *)&resp->pubKey, U2F_EC_PUB_KEY_SIZE + 1); + sha256_update((const uint8_t *) &kh, sizeof(credential_id)); + sha256_update((const uint8_t *) &resp->pubKey, U2F_EC_PUB_KEY_SIZE + 1); sha256_final(digest); size_t signature_len = sign_with_device_key(digest, PRIVATE_KEY_LENGTH[SECP256R1], - resp->keyHandleCertSig + sizeof(CredentialId) + cert_len); - LL = 67 + sizeof(CredentialId) + cert_len + signature_len; + resp->keyHandleCertSig + sizeof(credential_id) + cert_len); + LL = 67 + sizeof(credential_id) + cert_len + signature_len; return 0; } int u2f_authenticate(const CAPDU *capdu, RAPDU *rapdu) { - U2F_AUTHENTICATE_REQ *req = (U2F_AUTHENTICATE_REQ *)DATA; - U2F_AUTHENTICATE_RESP *resp = (U2F_AUTHENTICATE_RESP *)RDATA; - CTAP_authData auth_data; + U2F_AUTHENTICATE_REQ *req = (U2F_AUTHENTICATE_REQ *) DATA; + U2F_AUTHENTICATE_RESP *resp = (U2F_AUTHENTICATE_RESP *) RDATA; + CTAP_auth_data auth_data; size_t len; ecc_key_t key; // TODO: cleanup if (LC != sizeof(U2F_AUTHENTICATE_REQ)) EXCEPT(SW_WRONG_DATA); // required by FIDO Conformance Tool - if (req->keyHandleLen != sizeof(CredentialId)) EXCEPT(SW_WRONG_LENGTH); - if (memcmp(req->appId, ((CredentialId *)req->keyHandle)->rpIdHash, U2F_APPID_SIZE) != 0) EXCEPT(SW_WRONG_DATA); - uint8_t err = verify_key_handle((CredentialId *)req->keyHandle, &key); + if (req->keyHandleLen != sizeof(credential_id)) EXCEPT(SW_WRONG_LENGTH); + if (memcmp_s(req->appId, ((credential_id *)req->keyHandle)->rp_id_hash, U2F_APPID_SIZE) != 0) EXCEPT(SW_WRONG_DATA); + uint8_t err = verify_key_handle((credential_id *)req->keyHandle, &key); if (err) EXCEPT(SW_WRONG_DATA); if (P1 == U2F_AUTH_CHECK_ONLY) EXCEPT(SW_CONDITIONS_NOT_SATISFIED); @@ -85,14 +85,14 @@ int u2f_authenticate(const CAPDU *capdu, RAPDU *rapdu) { len = sizeof(auth_data); uint8_t flags = FLAGS_UP; - err = ctap_make_auth_data(req->appId, (uint8_t *)&auth_data, flags, 0, NULL, &len, COSE_ALG_ES256); + err = ctap_make_auth_data(req->appId, (uint8_t *) &auth_data, flags, NULL, 0, &len, COSE_ALG_ES256, false, 0); if (err) EXCEPT(SW_CONDITIONS_NOT_SATISFIED); sha256_init(); - sha256_update((const uint8_t *)&auth_data, U2F_APPID_SIZE + 1 + sizeof(auth_data.signCount)); + sha256_update((const uint8_t *) &auth_data, U2F_APPID_SIZE + 1 + sizeof(auth_data.sign_count)); sha256_update(req->chal, U2F_CHAL_SIZE); sha256_final(req->appId); - memcpy(resp, &auth_data.flags, 1 + sizeof(auth_data.signCount)); + memcpy(resp, &auth_data.flags, 1 + sizeof(auth_data.sign_count)); ecc_sign(SECP256R1, &key, req->appId, PRIVATE_KEY_LENGTH[SECP256R1], resp->sig); memzero(&key, sizeof(key)); size_t signature_len = ecdsa_sig2ansi(U2F_EC_KEY_SIZE, resp->sig, resp->sig); @@ -109,7 +109,7 @@ int u2f_version(const CAPDU *capdu, RAPDU *rapdu) { } int u2f_select(const CAPDU *capdu, RAPDU *rapdu) { - (void)capdu; + (void) capdu; LL = 6; memcpy(RDATA, "U2F_V2", 6); return 0; diff --git a/applets/ctap/u2f.h b/applets/ctap/u2f.h index 0dd6fe26..af6f911d 100644 --- a/applets/ctap/u2f.h +++ b/applets/ctap/u2f.h @@ -10,7 +10,7 @@ // General constants #define U2F_EC_KEY_SIZE 32 // EC key size in bytes #define U2F_EC_PUB_KEY_SIZE 64 // EC public key size in bytes -#define U2F_KH_SIZE sizeof(CredentialId) // Key handle size +#define U2F_KH_SIZE sizeof(credential_id) // Key handle size #define U2F_MAX_ATT_CERT_SIZE 1152 // Max size of attestation certificate #define U2F_MAX_EC_SIG_SIZE 72 // Max size of DER coded EC signature #define U2F_CTR_SIZE 4 // Size of counter field diff --git a/applets/oath/oath.c b/applets/oath/oath.c index bd8361a1..27b6b788 100644 --- a/applets/oath/oath.c +++ b/applets/oath/oath.c @@ -239,7 +239,7 @@ static int oath_set_code(const CAPDU *capdu, RAPDU *rapdu) { // verify the response uint8_t hmac[SHA1_DIGEST_LENGTH]; hmac_sha1(key_ptr + 1, KEY_LEN, chal_ptr, chal_len, hmac); - if (memcmp(hmac, resp_ptr, SHA1_DIGEST_LENGTH) != 0) EXCEPT(SW_DATA_INVALID); + if (memcmp_s(hmac, resp_ptr, SHA1_DIGEST_LENGTH) != 0) EXCEPT(SW_DATA_INVALID); // save the key return write_attr(OATH_FILE, ATTR_KEY, key_ptr + 1, key_len - 1); @@ -270,7 +270,7 @@ static int oath_validate(const CAPDU *capdu, RAPDU *rapdu) { if (ret == 0) EXCEPT(SW_DATA_INVALID); uint8_t hmac[SHA1_DIGEST_LENGTH]; hmac_sha1(key, KEY_LEN, challenge, sizeof(challenge), hmac); - if (memcmp(hmac, resp_ptr, SHA1_DIGEST_LENGTH) != 0) EXCEPT(SW_DATA_INVALID); + if (memcmp_s(hmac, resp_ptr, SHA1_DIGEST_LENGTH) != 0) EXCEPT(SW_DATA_INVALID); is_validated = true; // build the response diff --git a/applets/piv/piv.c b/applets/piv/piv.c index fd811025..7c205cb7 100644 --- a/applets/piv/piv.c +++ b/applets/piv/piv.c @@ -628,7 +628,7 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { if (auth_ctx[OFFSET_AUTH_STATE] != AUTH_STATE_EXTERNAL || P2 != 0x9B || TDEA_BLOCK_SIZE != len[IDX_RESPONSE] || - memcmp(auth_ctx + OFFSET_AUTH_CHALLENGE, DATA + pos[IDX_RESPONSE], TDEA_BLOCK_SIZE) != 0) { + memcmp_s(auth_ctx + OFFSET_AUTH_CHALLENGE, DATA + pos[IDX_RESPONSE], TDEA_BLOCK_SIZE) != 0) { authenticate_reset(); EXCEPT(SW_SECURITY_STATUS_NOT_SATISFIED); } @@ -683,7 +683,7 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { if (auth_ctx[OFFSET_AUTH_STATE] != AUTH_STATE_MUTUAL || P2 != 0x9B || TDEA_BLOCK_SIZE != len[IDX_WITNESS] || - memcmp(auth_ctx + OFFSET_AUTH_CHALLENGE, DATA + pos[IDX_WITNESS], TDEA_BLOCK_SIZE) != 0) { + memcmp_s(auth_ctx + OFFSET_AUTH_CHALLENGE, DATA + pos[IDX_WITNESS], TDEA_BLOCK_SIZE) != 0) { authenticate_reset(); EXCEPT(SW_SECURITY_STATUS_NOT_SATISFIED); } diff --git a/canokey-crypto b/canokey-crypto index 794c1af9..9dd712b7 160000 --- a/canokey-crypto +++ b/canokey-crypto @@ -1 +1 @@ -Subproject commit 794c1af926b65dca84f534d333ae6d8a04622b1e +Subproject commit 9dd712b79da8e28a6ff3b9cb8aeb165223971645 diff --git a/include/ctap.h b/include/ctap.h index 740f5613..fe94f09d 100644 --- a/include/ctap.h +++ b/include/ctap.h @@ -10,5 +10,6 @@ int ctap_install_private_key(const CAPDU *capdu, RAPDU *rapdu); int ctap_install_cert(const CAPDU *capdu, RAPDU *rapdu); int ctap_process_cbor(uint8_t *req, size_t req_len, uint8_t *resp, size_t *resp_len); int ctap_process_apdu(const CAPDU *capdu, RAPDU *rapdu); +int ctap_wink(void); #endif // CANOKEY_CORE_FIDO2_FIDO2_H_ diff --git a/include/device.h b/include/device.h index 666e16d1..ec4bd846 100644 --- a/include/device.h +++ b/include/device.h @@ -61,6 +61,9 @@ void fm_receive(uint8_t *buf, uint8_t len); // only for test int testmode_emulate_user_presence(void); int testmode_get_is_nfc_mode(void); +void testmode_set_initial_ticks(uint32_t ticks); +void testmode_inject_error(uint8_t p1, uint8_t p2, uint16_t len, const uint8_t *data); +bool testmode_err_triggered(const char* filename, bool file_wr); // platform independent functions uint8_t wait_for_user_presence(uint8_t entry); diff --git a/include/fs.h b/include/fs.h index 194831b3..eeb2b3b7 100644 --- a/include/fs.h +++ b/include/fs.h @@ -13,6 +13,7 @@ int truncate_file(const char *path, lfs_size_t len); int read_attr(const char *path, uint8_t attr, void *buf, lfs_size_t len); int write_attr(const char *path, uint8_t attr, const void *buf, lfs_size_t len); int get_file_size(const char *path); +int fs_rename(const char *old, const char *new); /** * Get the total size (in KiB) of the file system. diff --git a/interfaces/USB/class/ctaphid/ctaphid.c b/interfaces/USB/class/ctaphid/ctaphid.c index ba99e9aa..6d20fb16 100644 --- a/interfaces/USB/class/ctaphid/ctaphid.c +++ b/interfaces/USB/class/ctaphid/ctaphid.c @@ -147,6 +147,7 @@ uint8_t CTAPHID_Loop(uint8_t wait_for_user) { } channel.bcnt_total = (uint16_t)MSG_LEN(frame); if (channel.bcnt_total > MAX_CTAP_BUFSIZE) { + DBG_MSG("bcnt_total=%hu exceeds MAX_CTAP_BUFSIZE\n", channel.bcnt_total); CTAPHID_SendErrorResponse(frame.cid, ERR_INVALID_LEN); return LOOP_SUCCESS; } @@ -207,12 +208,17 @@ uint8_t CTAPHID_Loop(uint8_t wait_for_user) { else CTAPHID_SendResponse(channel.cid, channel.cmd, channel.data, channel.bcnt_total); break; + case CTAPHID_WINK: + DBG_MSG("WINK\n"); + if (!wait_for_user) ctap_wink(); + CTAPHID_SendResponse(channel.cid, channel.cmd, channel.data, 0); + break; case CTAPHID_CANCEL: DBG_MSG("CANCEL\n"); ret = LOOP_CANCEL; break; default: - DBG_MSG("Invalid CMD\n"); + DBG_MSG("Invalid CMD 0x%hhx\n", channel.cmd); CTAPHID_SendErrorResponse(channel.cid, ERR_INVALID_CMD); break; } diff --git a/interfaces/USB/class/ctaphid/ctaphid.h b/interfaces/USB/class/ctaphid/ctaphid.h index fb858a10..af61f87b 100644 --- a/interfaces/USB/class/ctaphid/ctaphid.h +++ b/interfaces/USB/class/ctaphid/ctaphid.h @@ -93,7 +93,7 @@ typedef struct { #define LOOP_SUCCESS 0x00 #define LOOP_CANCEL 0x01 -#define MAX_CTAP_BUFSIZE 1280 +#define MAX_CTAP_BUFSIZE 1300 typedef struct { uint32_t cid; diff --git a/littlefs b/littlefs index ead50807..40dba4a5 160000 --- a/littlefs +++ b/littlefs @@ -1 +1 @@ -Subproject commit ead50807f1ca3fdf2da00b77a0ce02651ded2d13 +Subproject commit 40dba4a556e0d81dfbe64301a6aa4e18ceca896c diff --git a/src/apdu.c b/src/apdu.c index 6ffcb30e..cdfa536b 100644 --- a/src/apdu.c +++ b/src/apdu.c @@ -219,11 +219,19 @@ void process_apdu(CAPDU *capdu, RAPDU *rapdu) { #ifdef TEST if (CLA == 0x00 && INS == 0xEE && LC == 0x04 && memcmp(DATA, "\x12\x56\xAB\xF0", 4) == 0) { printf("MAGIC REBOOT command received!\r\n"); + testmode_set_initial_ticks(0); + testmode_set_initial_ticks(device_get_tick()); ctap_install(0); SW = 0x9000; LL = 0; break; } + if (CLA == 0x00 && INS == 0xEF) { + testmode_inject_error(P1, P2, LC, DATA); + SW = 0x9000; + LL = 0; + break; + } #endif ctap_process_apdu(capdu, &rapdu_chaining.rapdu); rapdu->len = LE; diff --git a/src/fs.c b/src/fs.c index 799685b7..0e652cd3 100644 --- a/src/fs.c +++ b/src/fs.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include static lfs_t lfs; @@ -22,13 +23,18 @@ int read_file(const char *path, void *buf, lfs_soff_t off, lfs_size_t len) { err = lfs_file_close(&lfs, &f); if (err < 0) return err; return read_length; -err_close: + err_close: lfs_file_close(&lfs, &f); return err; } int write_file(const char *path, const void *buf, lfs_soff_t off, lfs_size_t len, uint8_t trunc) { lfs_file_t f; +#ifdef TEST + if (testmode_err_triggered(path, true)) { + return LFS_ERR_IO; + } +#endif int flags = LFS_O_WRONLY | LFS_O_CREAT; if (trunc) flags |= LFS_O_TRUNC; int err = lfs_file_open(&lfs, &f, path, flags); @@ -42,7 +48,7 @@ int write_file(const char *path, const void *buf, lfs_soff_t off, lfs_size_t len err = lfs_file_close(&lfs, &f); if (err < 0) return err; return 0; -err_close: + err_close: lfs_file_close(&lfs, &f); return err; } @@ -60,7 +66,7 @@ int append_file(const char *path, const void *buf, lfs_size_t len) { err = lfs_file_close(&lfs, &f); if (err < 0) return err; return 0; -err_close: + err_close: lfs_file_close(&lfs, &f); return err; } @@ -75,7 +81,7 @@ int truncate_file(const char *path, lfs_size_t len) { err = lfs_file_close(&lfs, &f); if (err < 0) return err; return 0; -err_close: + err_close: lfs_file_close(&lfs, &f); return err; } @@ -100,15 +106,17 @@ int get_file_size(const char *path) { err = lfs_file_close(&lfs, &f); if (err < 0) return err; return size; -err_close: + err_close: lfs_file_close(&lfs, &f); return err; } -int get_fs_size(void) { return (int)(lfs.cfg->block_size * lfs.cfg->block_count) / 1024; } +int get_fs_size(void) { return (int) (lfs.cfg->block_size * lfs.cfg->block_count) / 1024; } int get_fs_usage(void) { int blocks = lfs_fs_size(&lfs); if (blocks < 0) return blocks; - return (int)(lfs.cfg->block_size * blocks) / 1024; + return (int) (lfs.cfg->block_size * blocks) / 1024; } + +int fs_rename(const char *old, const char *new) { return lfs_rename(&lfs, old, new); } diff --git a/test-real/test-libfido2.sh b/test-real/test-libfido2.sh new file mode 100755 index 00000000..3bcc581e --- /dev/null +++ b/test-real/test-libfido2.sh @@ -0,0 +1,355 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +export LANGUAGE=en_US +export LANG=en_US.UTF8 +export TEST_TMP_DIR=/tmp/canokey-libfido2 +export USER=`id -nu` +PIN=123456 +NON_TTY="setsid -w" + +oneTimeSetUp() { + # rm -rf "$TEST_TMP_DIR" + mkdir -p "$TEST_TMP_DIR" + killall -u $USER -9 gpg-agent && sleep 2 || true + export RDID=$(fido2-token -L | grep -Po '^(pcsc:|/dev).*(?=: )' | tail -n 1) +} + +ToolHelper() { + # echo "PIN is $PIN" + echo $PIN | $NON_TTY $* "$RDID" 2>"$TEST_TMP_DIR/stderr" + local res=$? + sed -i -E 's/Enter.*PIN for \S+ *//g' "$TEST_TMP_DIR/stderr" + cat "$TEST_TMP_DIR/stderr" 1>&2 + return $res +} + +FIDO2MakeCred() { + local rpid=$1 + local username=$2 + local userid=$(dd status=none if=/dev/urandom bs=64 count=1 | base64 -w 0) + openssl rand -base64 32 >"$TEST_TMP_DIR/cred_param" # client data hash + assertTrue "openssl failed" $? + echo "$rpid" >>"$TEST_TMP_DIR/cred_param" + echo "$username" >>"$TEST_TMP_DIR/cred_param" + echo "$userid" >>"$TEST_TMP_DIR/cred_param" # user id + ToolHelper fido2-cred -M -r -b -i "$TEST_TMP_DIR/cred_param" >"$TEST_TMP_DIR/mc" + assertTrue "fido2-cred -M failed" $? + local largeBlobKey=$(tail -n 1 "$TEST_TMP_DIR/mc") + head -n -1 "$TEST_TMP_DIR/mc" | fido2-cred -V -o "$TEST_TMP_DIR/verified" + local ret=$? + assertTrue "fido2-cred -V failed" $ret + if [[ $ret != 0 ]];then + return 1 + fi + local credid=$(head -n 1 "$TEST_TMP_DIR/verified") + echo $userid $credid $largeBlobKey +} + +FIDO2GetAssert() { + local rpid=$1 + local credid=$2 + local userid=$(dd status=none if=/dev/urandom bs=64 count=1 | base64 -w 0) + openssl rand -base64 32 >"$TEST_TMP_DIR/assert_param" # client data hash + echo "$rpid" >>"$TEST_TMP_DIR/assert_param" + echo "$credid" >>"$TEST_TMP_DIR/assert_param" + ToolHelper fido2-assert -G -b -i "$TEST_TMP_DIR/assert_param" >"$TEST_TMP_DIR/assert" + assertTrue "fido2-assert -G failed" $? + local largeBlobKey=$(tail -n 1 "$TEST_TMP_DIR/assert") + echo $largeBlobKey +} + +FIDO2ListRP() { + local nrRk=$(ToolHelper fido2-token -I -c | grep 'existing rk') + echo "$nrRk" # existing rk(s): 64 + if [[ $nrRk != *" 0" ]]; then + # Idx Credential RpID + # 00: NBuC7jAK2Ty/3ileQvETIZ8BUQ+93GoraEm3Su3KvC0= RPID_aaaaaaaaaaaaabbbbbbbbbbbb01 + ToolHelper fido2-token -L -r + assertTrue "FIDO2ListRP failed" $? + fi +} + +FIDO2GetRkByRp() { + # Idx: CredID DispName UserID Algo Prot + # 00: +CSO/Hjxmj8YJ0Iv+TQw018+a+8y+AE36XlODTT6vhUBADQbgu4wCtk8v94pXkLxEyGfAVEPvdxqK2hJt0rtyrwtd+Az/H8AABB24DP8fwAA/7CVQft/AAAAAAAAAAAAACB1+f///w== DispName E+It8WJq4TJbPzfjSqeJDPpP+XkKVMBzIAk0sKAVu8IaZDhG2vOEH4rqw0eP6yWg es256 unknown + ToolHelper fido2-token -L -k "$1" + assertTrue "FIDO2GetRkByRp failed" $? +} + +FIDO2ListRK() { + local rps=$(FIDO2ListRP) + while IFS= read -r rp_line + do + local fields=($rp_line) + if [[ ${fields[0]} == [0-9][0-9]: ]]; then + rpid=${fields[2]} + FIDO2GetRkByRp $rpid + fi + done <<< "$rps" +} + +FIDO2SetName() { + local credid="$1" + local userid="$2" + local name="$3" + local display_name="$4" + echo "Set Cred[$credid] User[$userid] to $name $display_name" + ToolHelper fido2-token -S -c -i "$credid" -k "$userid" -n "$name" -p "$display_name" + assertTrue "FIDO2SetName failed" $? +} + +FIDO2SetBlob() { + local rpid="$1" + local credid="$2" + local blobPath="$TEST_TMP_DIR/blob" + echo "Set LB of RP[$rpid] Cred[$credid] to $blobPath" + echo " with Key:" $(cat "$TEST_TMP_DIR/blobkey") + ToolHelper fido2-token -Sb -k "$TEST_TMP_DIR/blobkey" "$blobPath" + # + # ToolHelper fido2-token -Sb -n "$rpid" -i "$credid" "$blobPath" + local ret=$? + assertTrue "FIDO2SetBlob failed" $ret + return $ret +} + +FIDO2DelBlob() { + local rpid="$1" + local credid="$2" + echo "Del LB of RP[$rpid] Cred[$credid]" + # echo "With Key:" $(cat "$TEST_TMP_DIR/blobkey") + # -k "$TEST_TMP_DIR/blobkey" + ToolHelper fido2-token -Db -n "$rpid" -i "$credid" + local ret=$? + assertTrue "FIDO2DelBlob failed" $ret + return $ret +} + +FIDO2GetBlob() { + local rpid="$1" + local credid="$2" + local blobPath="$TEST_TMP_DIR/blob_read" + echo "Get LB of RP[$rpid] Cred[$credid]" + # echo "With Key:" $(cat "$TEST_TMP_DIR/blobkey") + # ToolHelper fido2-token -Gb -k "$TEST_TMP_DIR/blobkey" "$blobPath" + >"$blobPath" + ToolHelper fido2-token -Gb -n "$rpid" -i "$credid" "$blobPath" + local ret=$? + assertTrue "FIDO2GetBlob failed" $ret + return $ret +} + +FIDO2DelRkByID() { + echo "Deleting cred: $1" + ToolHelper fido2-token -D -i "$1" + assertTrue "FIDO2DelRkByID failed" $? +} + +compareAllRk() { + local allRk="" + local rps=$(FIDO2ListRP) + while IFS= read -r rp_line + do + local fields=($rp_line) + if [[ ${fields[0]} == [0-9][0-9]: ]]; then + local rpid=${fields[2]} + local rks=$(FIDO2GetRkByRp $rpid) + while IFS= read -r rk_line + do + fields=($rk_line) + # RpID UserID CredID + allRk+="$rpid ${fields[3]} ${fields[1]}" + allRk+=$'\n' + done <<< "$rks" + fi + done <<< "$rps" + echo -n "$allRk" | sort > "$TEST_TMP_DIR/sorted_allRk" + awk '{print $1,$2,$4}' "$TEST_TMP_DIR/rks" | sort | diff -w "$TEST_TMP_DIR/sorted_allRk" - +} + +makeRPID() { + # MAX_STORED_RPID_LENGTH=32 + printf "RPID_aaaaaaaaaaaaabbbbbbbbbbbb%02x" $1 +} + +makeBlob() { + # Length = 64 + printf "LargeBlob_aaaaaaaaaaaabbbbbbbbbbb_ccccccccccccccdddddddddddddd%02x" $1 +} + +makeUserName() { + # USER_NAME_LIMIT=65-1 + printf "USERNAME_aaaaaaaaaaaabbbbbbbbbbbb_ccccccccccccccdddddddddddddd%02x" $1 +} + +makeDispName() { + # USER_NAME_LIMIT=65-1 + printf "DisplayName_AAAAAAAAAAAAAAAAA_XXXXXXXXXX_YYYYYYYYYYYYYYYYYYYYY%02x" $1 +} + +makeCredAndStore() { + local fields + # "$1=rpid" "$2=uname" + fields=($(FIDO2MakeCred $1 $2)) + if [[ $? != 0 ]]; then + return 1 + fi + local userid=${fields[0]} + local credid=${fields[1]} + # RpID UserID UserName CredID + echo $1 $userid $2 $credid | tee -a "$TEST_TMP_DIR/rks" +} + +setPINforTest() { + # Set PIN + local origPin=$PIN + PIN=$origPin$'\r'$origPin$'\r' + ToolHelper fido2-token -S + PIN=$origPin +} + +test_Reset() { + fido2-token -R "$RDID" + if [[ $? != 0 ]];then + echo "Cannot reset the key" + fi + setPINforTest +} + +test_List() { + FIDO2ListRP + FIDO2ListRK +} + +test_DelAllRk() { + local rps=$(FIDO2ListRK) + while IFS= read -r rp_line + do + local fields=($rp_line) + if [[ ${fields[0]} == [0-9][0-9]: ]]; then + local credid=${fields[1]} + FIDO2DelRkByID $credid + fi + done <<< "$rps" +} + +test_MC() { + echo $'RelyingPartyID UserID UserName CredID' + >"$TEST_TMP_DIR/rks" + for((i=1;i<=64;i++)); do + local rpid=$(makeRPID $i) + local uname=$(makeUserName $i) + makeCredAndStore "$rpid" "$uname" || return 1 + done + local nline=0 + while IFS= read -r line + do + if [[ $nline == 0 ]]; then + assertEquals 'existing rk(s): 64' "$line" + else + local fields=($line) + rpid=$(makeRPID $nline) + assertEquals $rpid ${fields[2]} + fi + ((nline++)) + done < <(FIDO2ListRP) +} + +test_DispName() { + local randSeq=$(seq 1 64 | shuf) + for i in $randSeq; do + local rpid=$(makeRPID $i) + local fields=($(grep $rpid "$TEST_TMP_DIR/rks")) + if [[ ${#fields[@]} != 4 ]];then + break; + fi + local userid=${fields[1]} + local credid=${fields[3]} + local display_name=$(makeDispName $i) + local user_name="new_username$i" + FIDO2SetName "$credid" "$userid" "$user_name" "$display_name" + fields=($(FIDO2GetRkByRp $rpid)) + assertEquals "$credid" "${fields[1]}" + assertEquals "$display_name" "${fields[2]}" + assertEquals "$userid" "${fields[3]}" + assertEquals es256 "${fields[4]}" + done + +} + +test_LargeBlob() { + ToolHelper fido2-token -L -b + local nrLB=8 + local randSeq=$(seq 1 $nrLB | shuf) + for i in $randSeq; do + local rpid=$(makeRPID $i) + local fields=($(grep $rpid "$TEST_TMP_DIR/rks")) + if [[ ${#fields[@]} != 4 ]];then + break; + fi + local credid=${fields[3]} + fields=($(FIDO2GetAssert "$rpid" "$credid")) + echo ${fields[0]} > "$TEST_TMP_DIR/blobkey" + makeBlob $i > "$TEST_TMP_DIR/blob" + FIDO2SetBlob "$rpid" "$credid" || return 1 + done + randSeq=$(seq 1 $nrLB | shuf) + for i in $randSeq; do + rpid=$(makeRPID $i) + fields=($(grep "$rpid" "$TEST_TMP_DIR/rks")) + if [[ ${#fields[@]} != 4 ]];then + break; + fi + credid=${fields[3]} + FIDO2GetBlob "$rpid" "$credid" || return 1 + echo "$rpid: $(cat $TEST_TMP_DIR/blob_read)" + makeBlob $i | diff "$TEST_TMP_DIR/blob_read" - + if [[ $? != 0 ]]; then + return 1 + fi + FIDO2DelBlob "$rpid" "$credid" + done +} + +test_DelRk() { + local randSeq=$(seq 1 64 | shuf) + local nrDel=0 + for i in $randSeq; do + local rpid=$(makeRPID $i) + local fields=($(grep $rpid "$TEST_TMP_DIR/rks")) + if [[ ${#fields[@]} != 4 ]];then + break; + fi + local credid=${fields[3]} + echo "[$nrDel]" Deleting $credid of $rpid + FIDO2DelRkByID $credid + sed -i "/$rpid/d" "$TEST_TMP_DIR/rks" + ((nrDel++)) + if [[ $nrDel == 1 || $nrDel == 2 || $nrDel == 10 || $nrDel == 64 ]];then + compareAllRk || return 1 + fi + done +} + +skip_test_Debug() { + setPINforTest + echo "For debug only" + local rpid=thisRP + makeCredAndStore $rpid thisUser + local fields=($(grep $rpid "$TEST_TMP_DIR/rks")) + local credid=${fields[3]} + fields=($(FIDO2GetAssert "$rpid" "$credid")) + # echo "$TEST_TMP_DIR/assert" + # cat "$TEST_TMP_DIR/assert" + echo "Blob key is ${fields[0]}" + echo ${fields[0]} > "$TEST_TMP_DIR/blobkey" + makeBlob $i > "$TEST_TMP_DIR/blob" + FIDO2SetBlob "$rpid" "$credid" || return 1 + FIDO2GetBlob "$rpid" "$credid" + FIDO2DelBlob "$rpid" "$credid" + # FIDO2GetRkByRp rp1 + # FIDO2DelRkByID "6AwF68LTVupyLx5ddpFRQiPS9+UmkSktTXYWREijOjIBAGO0QIKafRKTv8hiGj4aZxPQSQbfySYyH7CGSbLfBM8/d+Az/H8AABB24DP8fwAA/7CVQft/AAAAAAAAAAAAACB1+f///w==" RPID_aaaaaaaaaaaaabbbbbbbbbbbb40 + # compareAllRk + # FIDO2GetAssert RPID_aaaaaaaaaaaaabbbbbbbbbbbb01 pE23+fIh21aLmVNPJ+HVRnepBYZq+NzYwcz7jCw/prcBADQbgu4wCtk8v94pXkLxEyGfAVEPvdxqK2hJt0rtyrwt6gNn/38AACDpA2f/fwAA/+CkegR/AAAAAAAAAAAAADDo+f///w== +} + +. ./shunit2/shunit2 diff --git a/test-via-pcsc/build_fido_tests.sh b/test-via-pcsc/build_fido_tests.sh index 4378fa56..fdc4552e 100755 --- a/test-via-pcsc/build_fido_tests.sh +++ b/test-via-pcsc/build_fido_tests.sh @@ -1,13 +1,41 @@ #!/bin/bash set -e +if [ ! -d u2f-ref-code ];then git clone --depth 1 https://github.com/google/u2f-ref-code.git pushd u2f-ref-code/u2f-tests/HID git clone --depth 1 -b lollipop-release https://android.googlesource.com/platform/system/core cd ../NFC; make cd ../HID; make popd +fi -git clone --depth 1 https://github.com/canokeys/fido2-tests.git +git clone --depth 1 -b dev-fido2v1 https://github.com/canokeys/fido2-tests.git pushd fido2-tests pip3 install --user -r requirements.txt +echo "Fixing a bug in python-fido2 0.9.3" +patch -p1 -u -d ~/.local/lib/python3.*/site-packages/fido2 < 14 && memcmp(buf, "\x99\x10\x52\xca\x95\xe5\x69\xde\x69\xe0\x2e\xbf", 12) == 0) { + uint8_t *data = buf + 12; + testmode_inject_error(data[0], data[1], length-14, data+2); + continue; } CTAPHID_OutEvent(buf); }