Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54c0769dbd | ||
|
|
0bbcba2f60 | ||
|
|
723648173d | ||
|
|
e6c128fe0d | ||
|
|
2174b516c3 | ||
|
|
4577e4430c | ||
|
|
9a8f4c0f4d | ||
|
|
e21d985344 | ||
|
|
43cd8869f9 | ||
|
|
a42131876f | ||
|
|
e1f4e3035d | ||
|
|
71ecb23af6 | ||
|
|
8c21a2bbcd | ||
|
|
53cc16ab6d | ||
|
|
f213854f8b | ||
|
|
2c125e76eb | ||
|
|
19d8f16056 | ||
|
|
40065217fd | ||
|
|
32c938674a | ||
|
|
4425722a71 | ||
|
|
69eef7651c | ||
|
|
7f97ea4f24 | ||
|
|
467523769e | ||
|
|
2d295d0d98 | ||
|
|
0758644583 | ||
|
|
c3a5b8e708 | ||
|
|
b134d261ae | ||
|
|
4f93b984cd | ||
|
|
ea0547ef49 | ||
|
|
e5b7dff8cc | ||
|
|
6a077d0d8f | ||
|
|
7c271fc4f3 | ||
|
|
2734259c02 | ||
|
|
ba4faa9840 | ||
|
|
746c324113 | ||
|
|
ac224063fc | ||
|
|
cf4778b9ad | ||
|
|
3f80acc81b | ||
|
|
4cea6ebe87 | ||
|
|
02e5eb8dba | ||
|
|
037019b348 | ||
|
|
ae237db9ca | ||
|
|
c2e16fda41 | ||
|
|
f84d36b1da | ||
|
|
04aaf0f572 | ||
|
|
577edbb62f | ||
|
|
40b5f70761 | ||
|
|
a294840425 | ||
|
|
d786a9c6e5 | ||
|
|
b87eb3f278 | ||
|
|
6f226001df | ||
|
|
6e91694253 | ||
|
|
0c5b308aef | ||
|
|
3fc41a12a7 | ||
|
|
8ad8c82baf | ||
|
|
85818d009c | ||
|
|
bb069c5651 | ||
|
|
e3b036456f | ||
|
|
47ea749454 | ||
|
|
cb4827688b | ||
|
|
d43b6caf16 | ||
|
|
7534d7bb76 | ||
|
|
cc8d9e0741 | ||
|
|
4e94cbe40e | ||
|
|
b1b9dad9f5 | ||
|
|
0c51160d23 | ||
|
|
958a20ce11 | ||
|
|
1e7d711c03 | ||
|
|
cc0a181f75 | ||
|
|
b8568d834a | ||
|
|
174241c0a0 | ||
|
|
fa17d5c906 | ||
|
|
7a4be766bc | ||
|
|
1835afe54a | ||
|
|
ad07052e6a | ||
|
|
da577b8e8d | ||
|
|
0ec563c8de | ||
|
|
d4b7bfd6cc | ||
|
|
995870d77e |
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -13,10 +13,10 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '23 5 * * 4'
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'cpp' ]
|
||||
language: [ 'cpp', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ target_sources(pico_fido PUBLIC
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/fido/cbor_client_pin.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/fido/credential.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/fido/cbor_get_assertion.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/fido/cbor_selection.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/fido/cbor_cred_mgmt.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/fido/cbor_config.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/fido/cbor_vendor.c
|
||||
)
|
||||
set(HSM_DRIVER "hid")
|
||||
include(pico-hsm-sdk/pico_hsm_sdk_import.cmake)
|
||||
|
||||
22
README.md
22
README.md
@@ -12,8 +12,13 @@ Pico FIDO has implemented the following features:
|
||||
- User presence enforcement through physical button
|
||||
- User Verification with PIN
|
||||
- Discoverable credentials
|
||||
- Credential management
|
||||
- ECDSA authentication
|
||||
- App registration and login
|
||||
- Device selection
|
||||
- Support for vendor Config
|
||||
- Backup with 24 words
|
||||
- Secure lock to protect the device from flash dumpings
|
||||
|
||||
All these features are compliant with the specification. Therefore, if you detect some behaviour that is not expected or it does not follow the rules of specs, please open an issue.
|
||||
|
||||
@@ -23,7 +28,7 @@ Pico FIDO is an open platform so be careful. The contents in the flash memory ma
|
||||
If the Pico is stolen the contents of private and secret keys can be read.
|
||||
|
||||
## Download
|
||||
Please, go to the [Release page](https://github.com/polhenarejos/pico-fido/releases "Release page")) and download the UF2 file for your board.
|
||||
Please, go to the [Release page](https://github.com/polhenarejos/pico-fido/releases "Release page") and download the UF2 file for your board.
|
||||
|
||||
Note that UF2 files are shiped with a dummy VID/PID to avoid license issues (FEFF:FCFD). If you are planning to use it with OpenSC or similar, you should modify Info.plist of CCID driver to add these VID/PID or use the VID/PID patcher as follows: `./pico-fido-patch-vidpid.sh VID:PID input_fido_file.uf2 output_fido_file.uf2`
|
||||
|
||||
@@ -69,6 +74,21 @@ While processing, the Pico FIDO is busy and cannot receive additional commands u
|
||||
|
||||
Pico FIDO uses the `HID` driver, present in all OS. It should be detected by all OS and browser/applications, like normal USB FIDO keys.
|
||||
|
||||
## Tests
|
||||
|
||||
Tests can be found at `tests` folder. It is based on [FIDO2 tests](https://github.com/solokeys/fido2-tests "FIDO2 tests") from Solokeys, but adapted to [python-fido2](https://github.com/Yubico/python-fido2 "python-fido2") v1.0 package, which is a major refactor from previous 0.8 version and includes latests improvements from CTAP 2.1.
|
||||
|
||||
All tests can be run by
|
||||
|
||||
```
|
||||
pytest
|
||||
```
|
||||
|
||||
or by selecting a subset with `-k <test>` flag:
|
||||
```
|
||||
pytest -k test_credprotect
|
||||
```
|
||||
|
||||
## Credits
|
||||
Pico FIDO uses the following libraries or portion of code:
|
||||
- MbedTLS for cryptographic operations.
|
||||
|
||||
53
build_pico_fido.sh
Executable file
53
build_pico_fido.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
VERSION_MAJOR="2"
|
||||
VERSION_MINOR="4"
|
||||
|
||||
rm -rf release/*
|
||||
cd build_release
|
||||
|
||||
for board in adafruit_feather_rp2040 \
|
||||
adafruit_itsybitsy_rp2040 \
|
||||
adafruit_kb2040 \
|
||||
adafruit_macropad_rp2040 \
|
||||
adafruit_qtpy_rp2040 \
|
||||
adafruit_trinkey_qt2040 \
|
||||
arduino_nano_rp2040_connect \
|
||||
datanoisetv_rp2040_dsp \
|
||||
eetree_gamekit_rp2040 \
|
||||
garatronic_pybstick26_rp2040 \
|
||||
melopero_shake_rp2040 \
|
||||
pico \
|
||||
pico_w \
|
||||
pimoroni_badger2040 \
|
||||
pimoroni_interstate75 \
|
||||
pimoroni_keybow2040 \
|
||||
pimoroni_motor2040 \
|
||||
pimoroni_pga2040 \
|
||||
pimoroni_picolipo_4mb \
|
||||
pimoroni_picolipo_16mb \
|
||||
pimoroni_picosystem \
|
||||
pimoroni_plasma2040 \
|
||||
pimoroni_servo2040 \
|
||||
pimoroni_tiny2040 \
|
||||
pimoroni_tiny2040_2mb \
|
||||
seeed_xiao_rp2040 \
|
||||
solderparty_rp2040_stamp \
|
||||
solderparty_rp2040_stamp_carrier \
|
||||
solderparty_rp2040_stamp_round_carrier \
|
||||
sparkfun_micromod \
|
||||
sparkfun_promicro \
|
||||
sparkfun_thingplus \
|
||||
vgaboard \
|
||||
waveshare_rp2040_lcd_0.96 \
|
||||
waveshare_rp2040_plus_4mb \
|
||||
waveshare_rp2040_plus_16mb \
|
||||
waveshare_rp2040_zero \
|
||||
wiznet_w5100s_evb_pico
|
||||
do
|
||||
rm -rf *
|
||||
PICO_SDK_PATH=../../pico-sdk cmake .. -DPICO_BOARD=$board
|
||||
make -kj20
|
||||
mv pico_fido.uf2 ../release/pico_fido_$board-$VERSION_MAJOR.$VERSION_MINOR.uf2
|
||||
|
||||
done
|
||||
Submodule pico-hsm-sdk updated: 657913d29a...4a8a6728c7
@@ -27,27 +27,51 @@
|
||||
|
||||
const bool _btrue = true, _bfalse = false;
|
||||
|
||||
int cbor_reset();
|
||||
int cbor_get_info();
|
||||
int cbor_make_credential(const uint8_t *data, size_t len);
|
||||
int cbor_client_pin(const uint8_t *data, size_t len);
|
||||
int cbor_get_assertion(const uint8_t *data, size_t len, bool next);
|
||||
int cbor_get_next_assertion(const uint8_t *data, size_t len);
|
||||
int cbor_selection();
|
||||
int cbor_cred_mgmt(const uint8_t *data, size_t len);
|
||||
int cbor_config(const uint8_t *data, size_t len);
|
||||
int cbor_vendor(const uint8_t *data, size_t len);
|
||||
|
||||
const uint8_t aaguid[16] = {0x89, 0xFB, 0x94, 0xB7, 0x06, 0xC9, 0x36, 0x73, 0x9B, 0x7E, 0x30, 0x52, 0x6D, 0x96, 0x81, 0x45}; // First 16 bytes of SHA256("Pico FIDO2")
|
||||
|
||||
const uint8_t *cbor_data = NULL;
|
||||
size_t cbor_len = 0;
|
||||
static const uint8_t *cbor_data = NULL;
|
||||
static size_t cbor_len = 0;
|
||||
static uint8_t cmd = 0;
|
||||
|
||||
int cbor_parse(const uint8_t *data, size_t len) {
|
||||
int cbor_parse(uint8_t cmd, const uint8_t *data, size_t len) {
|
||||
if (len == 0)
|
||||
return CTAP1_ERR_INVALID_LEN;
|
||||
DEBUG_DATA(data+1,len-1);
|
||||
driver_prepare_response();
|
||||
if (data[0] == CTAP_MAKE_CREDENTIAL)
|
||||
return cbor_make_credential(data + 1, len - 1);
|
||||
if (data[0] == CTAP_GET_INFO)
|
||||
return cbor_get_info();
|
||||
else if (data[0] == CTAP_RESET)
|
||||
return cbor_reset();
|
||||
else if (data[0] == CTAP_CLIENT_PIN)
|
||||
return cbor_client_pin(data + 1, len - 1);
|
||||
else if (data[0] == CTAP_GET_ASSERTION)
|
||||
return cbor_get_assertion(data + 1, len - 1, false);
|
||||
else if (data[0] == CTAP_GET_NEXT_ASSERTION)
|
||||
return cbor_get_next_assertion(data + 1, len - 1);
|
||||
if (cmd == CTAPHID_CBOR) {
|
||||
if (data[0] == CTAP_MAKE_CREDENTIAL)
|
||||
return cbor_make_credential(data + 1, len - 1);
|
||||
if (data[0] == CTAP_GET_INFO)
|
||||
return cbor_get_info();
|
||||
else if (data[0] == CTAP_RESET)
|
||||
return cbor_reset();
|
||||
else if (data[0] == CTAP_CLIENT_PIN)
|
||||
return cbor_client_pin(data + 1, len - 1);
|
||||
else if (data[0] == CTAP_GET_ASSERTION)
|
||||
return cbor_get_assertion(data + 1, len - 1, false);
|
||||
else if (data[0] == CTAP_GET_NEXT_ASSERTION)
|
||||
return cbor_get_next_assertion(data + 1, len - 1);
|
||||
else if (data[0] == CTAP_SELECTION)
|
||||
return cbor_selection();
|
||||
else if (data[0] == CTAP_CREDENTIAL_MGMT)
|
||||
return cbor_cred_mgmt(data + 1, len - 1);
|
||||
else if (data[0] == CTAP_CONFIG)
|
||||
return cbor_config(data + 1, len - 1);
|
||||
}
|
||||
else if (cmd == CTAP_VENDOR_CBOR) {
|
||||
return cbor_vendor(data, len);
|
||||
}
|
||||
return CTAP2_ERR_INVALID_CBOR;
|
||||
}
|
||||
|
||||
@@ -63,7 +87,9 @@ void cbor_thread() {
|
||||
break;
|
||||
}
|
||||
|
||||
apdu.sw = cbor_parse(cbor_data, cbor_len);
|
||||
apdu.sw = cbor_parse(cmd, cbor_data, cbor_len);
|
||||
if (apdu.sw == 0)
|
||||
DEBUG_DATA(res_APDU + 1, res_APDU_size);
|
||||
|
||||
finished_data_size = res_APDU_size+1;
|
||||
uint32_t flag = EV_EXEC_FINISHED;
|
||||
@@ -71,9 +97,10 @@ void cbor_thread() {
|
||||
}
|
||||
}
|
||||
|
||||
int cbor_process(const uint8_t *data, size_t len) {
|
||||
int cbor_process(uint8_t last_cmd, const uint8_t *data, size_t len) {
|
||||
cbor_data = data;
|
||||
cbor_len = len;
|
||||
cmd = last_cmd;
|
||||
res_APDU = ctap_resp->init.data + 1;
|
||||
res_APDU_size = 0;
|
||||
return 1;
|
||||
|
||||
@@ -35,8 +35,8 @@ uint8_t permissions_rp_id = 0, permission_set = 0;
|
||||
uint32_t usage_timer = 0, initial_usage_time_limit = 0;
|
||||
uint32_t max_usage_time_period = 600*1000;
|
||||
bool needs_power_cycle = false;
|
||||
mbedtls_ecdh_context hkey;
|
||||
bool hkey_init = false;
|
||||
static mbedtls_ecdh_context hkey;
|
||||
static bool hkey_init = false;
|
||||
|
||||
int beginUsingPinUvAuthToken(bool userIsPresent) {
|
||||
paut.user_present = userIsPresent;
|
||||
@@ -159,8 +159,8 @@ int decrypt(uint8_t protocol, const uint8_t *key, const uint8_t *in, size_t in_l
|
||||
return aes_decrypt(key, NULL, 32*8, HSM_AES_MODE_CBC, out, in_len);
|
||||
}
|
||||
else if (protocol == 2) {
|
||||
memcpy(out, in, in_len);
|
||||
return aes_encrypt(key+32, out, 32*8, HSM_AES_MODE_CBC, out+IV_SIZE, in_len-IV_SIZE);
|
||||
memcpy(out, in+IV_SIZE, in_len);
|
||||
return aes_decrypt(key+32, in, 32*8, HSM_AES_MODE_CBC, out, in_len-IV_SIZE);
|
||||
}
|
||||
|
||||
return -1;
|
||||
@@ -341,7 +341,7 @@ int cbor_client_pin(const uint8_t *data, size_t len) {
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
if (file_has_data(ef_pin))
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
if (newPinEnc.len != 64)
|
||||
if ((pinUvAuthProtocol == 1 && newPinEnc.len != 64) || (pinUvAuthProtocol == 2 && newPinEnc.len != 64+IV_SIZE))
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
if (mbedtls_mpi_read_binary(&hkey.ctx.mbed_ecdh.Qp.X, kax.data, kax.len) != 0) {
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
@@ -387,7 +387,7 @@ int cbor_client_pin(const uint8_t *data, size_t len) {
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_NOT_SET);
|
||||
if (*file_get_data(ef_pin) == 0)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_BLOCKED);
|
||||
if (newPinEnc.len != 64 || pinHashEnc.len != 16)
|
||||
if ((pinUvAuthProtocol == 1 && (newPinEnc.len != 64 || pinHashEnc.len != 16)) || (pinUvAuthProtocol == 2 && (newPinEnc.len != 64+IV_SIZE || pinHashEnc.len != 16+IV_SIZE)))
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
if (mbedtls_mpi_read_binary(&hkey.ctx.mbed_ecdh.Qp.X, kax.data, kax.len) != 0) {
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
@@ -401,10 +401,10 @@ int cbor_client_pin(const uint8_t *data, size_t len) {
|
||||
mbedtls_platform_zeroize(sharedSecret, sizeof(sharedSecret));
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
uint8_t tmp[64 + 16];
|
||||
memcpy(tmp, newPinEnc.data, 64);
|
||||
memcpy(tmp + 64, pinHashEnc.data, 16);
|
||||
if (verify(pinUvAuthProtocol, sharedSecret, tmp, sizeof(tmp), pinUvAuthParam.data) != 0) {
|
||||
uint8_t tmp[80 + 32];
|
||||
memcpy(tmp, newPinEnc.data, newPinEnc.len);
|
||||
memcpy(tmp + newPinEnc.len, pinHashEnc.data, pinHashEnc.len);
|
||||
if (verify(pinUvAuthProtocol, sharedSecret, tmp, newPinEnc.len+pinHashEnc.len, pinUvAuthParam.data) != 0) {
|
||||
mbedtls_platform_zeroize(sharedSecret, sizeof(sharedSecret));
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
}
|
||||
@@ -423,7 +423,7 @@ int cbor_client_pin(const uint8_t *data, size_t len) {
|
||||
if (retries == 0) {
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_BLOCKED);
|
||||
}
|
||||
if (++new_pin_mismatches == 3) {
|
||||
if (++new_pin_mismatches >= 3) {
|
||||
needs_power_cycle = true;
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_BLOCKED);
|
||||
}
|
||||
@@ -460,6 +460,8 @@ int cbor_client_pin(const uint8_t *data, size_t len) {
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
if ((subcommand == 0x9 && permissions == 0) || (subcommand == 0x5 && (permissions != 0 || rpId.present == true)))
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
if (!file_has_data(ef_pin))
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_NOT_SET);
|
||||
if (*file_get_data(ef_pin) == 0)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_BLOCKED);
|
||||
if (mbedtls_mpi_read_binary(&hkey.ctx.mbed_ecdh.Qp.X, kax.data, kax.len) != 0) {
|
||||
@@ -476,7 +478,7 @@ int cbor_client_pin(const uint8_t *data, size_t len) {
|
||||
}
|
||||
uint8_t retries = *file_get_data(ef_pin) - 1;
|
||||
flash_write_data_to_file(ef_pin, &retries, 1);
|
||||
uint8_t paddedNewPin[64];
|
||||
uint8_t paddedNewPin[64], poff = (pinUvAuthProtocol-1)*IV_SIZE;
|
||||
ret = decrypt(pinUvAuthProtocol, sharedSecret, pinHashEnc.data, pinHashEnc.len, paddedNewPin);
|
||||
if (ret != 0) {
|
||||
mbedtls_platform_zeroize(sharedSecret, sizeof(sharedSecret));
|
||||
@@ -506,11 +508,11 @@ int cbor_client_pin(const uint8_t *data, size_t len) {
|
||||
memcpy(paut.rp_id_hash, rpId.data, 32);
|
||||
else
|
||||
memset(paut.rp_id_hash, 0, sizeof(paut.rp_id_hash));
|
||||
uint8_t pinUvAuthToken_enc[32];
|
||||
uint8_t pinUvAuthToken_enc[32+IV_SIZE];
|
||||
encrypt(pinUvAuthProtocol, sharedSecret, paut.data, 32, pinUvAuthToken_enc);
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, 1));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x02));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, pinUvAuthToken_enc, 32));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, pinUvAuthToken_enc, 32+poff));
|
||||
}
|
||||
else
|
||||
CBOR_ERROR(CTAP2_ERR_UNSUPPORTED_OPTION);
|
||||
|
||||
143
src/fido/cbor_config.c
Normal file
143
src/fido/cbor_config.c
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* This file is part of the Pico FIDO distribution (https://github.com/polhenarejos/pico-fido).
|
||||
* Copyright (c) 2022 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
#include "ctap2_cbor.h"
|
||||
#include "fido.h"
|
||||
#include "ctap.h"
|
||||
#include "bsp/board.h"
|
||||
#include "files.h"
|
||||
#include "apdu.h"
|
||||
#include "credential.h"
|
||||
#include "hsm.h"
|
||||
#include "random.h"
|
||||
#include "mbedtls/ecdh.h"
|
||||
#include "mbedtls/chachapoly.h"
|
||||
#include "mbedtls/hkdf.h"
|
||||
|
||||
extern uint8_t keydev_dec[32];
|
||||
extern bool has_keydev_dec;
|
||||
|
||||
int cbor_config(const uint8_t *data, size_t len) {
|
||||
CborParser parser;
|
||||
CborValue map;
|
||||
CborError error = CborNoError;
|
||||
uint64_t subcommand = 0, pinUvAuthProtocol = 0, vendorCommandId = 0;
|
||||
CborByteString pinUvAuthParam = {0}, vendorAutCt = {0};
|
||||
size_t resp_size = 0;
|
||||
CborEncoder encoder, mapEncoder;
|
||||
|
||||
CBOR_CHECK(cbor_parser_init(data, len, 0, &parser, &map));
|
||||
uint64_t val_c = 1;
|
||||
CBOR_PARSE_MAP_START(map, 1) {
|
||||
uint64_t val_u = 0;
|
||||
CBOR_FIELD_GET_UINT(val_u, 1);
|
||||
if (val_c <= 1 && val_c != val_u)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
if (val_u < val_c)
|
||||
CBOR_ERROR(CTAP2_ERR_INVALID_CBOR);
|
||||
val_c = val_u + 1;
|
||||
if (val_u == 0x01) {
|
||||
CBOR_FIELD_GET_UINT(subcommand, 1);
|
||||
}
|
||||
else if (val_u == 0x02) {
|
||||
uint64_t subpara = 0;
|
||||
CBOR_PARSE_MAP_START(_f1, 2) {
|
||||
if (subcommand == 0xff) {
|
||||
CBOR_FIELD_GET_UINT(subpara, 2);
|
||||
if (subpara == 0x01) {
|
||||
CBOR_FIELD_GET_UINT(vendorCommandId, 2);
|
||||
}
|
||||
else if (subpara == 0x02) {
|
||||
CBOR_FIELD_GET_BYTES(vendorAutCt, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
CBOR_PARSE_MAP_END(_f1, 2);
|
||||
}
|
||||
else if (val_u == 0x03) {
|
||||
CBOR_FIELD_GET_UINT(pinUvAuthProtocol, 1);
|
||||
}
|
||||
else if (val_u == 0x04) { // pubKeyCredParams
|
||||
CBOR_FIELD_GET_BYTES(pinUvAuthParam, 1);
|
||||
}
|
||||
}
|
||||
CBOR_PARSE_MAP_END(map, 1);
|
||||
|
||||
cbor_encoder_init(&encoder, ctap_resp->init.data + 1, CTAP_MAX_PACKET_SIZE, 0);
|
||||
|
||||
if (subcommand == 0xff) {
|
||||
if (vendorCommandId == CTAP_CONFIG_AUT_DISABLE) {
|
||||
if (!file_has_data(ef_keydev_enc))
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
if (has_keydev_dec == false)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
flash_write_data_to_file(ef_keydev, keydev_dec, sizeof(keydev_dec));
|
||||
mbedtls_platform_zeroize(keydev_dec, sizeof(keydev_dec));
|
||||
flash_write_data_to_file(ef_keydev_enc, NULL, 0); // Set ef to 0 bytes
|
||||
low_flash_available();
|
||||
}
|
||||
else if (vendorCommandId == CTAP_CONFIG_AUT_ENABLE) {
|
||||
if (!file_has_data(ef_keydev))
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
if (mse.init == false)
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
|
||||
mbedtls_chachapoly_context chatx;
|
||||
int ret = mse_decrypt_ct(vendorAutCt.data, vendorAutCt.len);
|
||||
if (ret != 0) {
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
uint8_t key_dev_enc[12+32+16];
|
||||
random_gen(NULL, key_dev_enc, 12);
|
||||
mbedtls_chachapoly_init(&chatx);
|
||||
mbedtls_chachapoly_setkey(&chatx, vendorAutCt.data);
|
||||
ret = mbedtls_chachapoly_encrypt_and_tag(&chatx, file_get_size(ef_keydev), key_dev_enc, NULL, 0, file_get_data(ef_keydev), key_dev_enc + 12, key_dev_enc + 12 + file_get_size(ef_keydev));
|
||||
mbedtls_chachapoly_free(&chatx);
|
||||
if (ret != 0){
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
flash_write_data_to_file(ef_keydev_enc, key_dev_enc, sizeof(key_dev_enc));
|
||||
mbedtls_platform_zeroize(key_dev_enc, sizeof(key_dev_enc));
|
||||
flash_write_data_to_file(ef_keydev, key_dev_enc, file_get_size(ef_keydev)); // Overwrite ef with 0
|
||||
flash_write_data_to_file(ef_keydev, NULL, 0); // Set ef to 0 bytes
|
||||
low_flash_available();
|
||||
}
|
||||
else {
|
||||
CBOR_ERROR(CTAP2_ERR_INVALID_SUBCOMMAND);
|
||||
}
|
||||
goto err;
|
||||
}
|
||||
else
|
||||
CBOR_ERROR(CTAP2_ERR_UNSUPPORTED_OPTION);
|
||||
CBOR_CHECK(cbor_encoder_close_container(&encoder, &mapEncoder));
|
||||
resp_size = cbor_encoder_get_buffer_size(&encoder, ctap_resp->init.data + 1);
|
||||
|
||||
err:
|
||||
CBOR_FREE_BYTE_STRING(pinUvAuthParam);
|
||||
CBOR_FREE_BYTE_STRING(vendorAutCt);
|
||||
|
||||
if (error != CborNoError) {
|
||||
if (error == CborErrorImproperValue)
|
||||
return CTAP2_ERR_CBOR_UNEXPECTED_TYPE;
|
||||
return error;
|
||||
}
|
||||
res_APDU_size = resp_size;
|
||||
return 0;
|
||||
}
|
||||
368
src/fido/cbor_cred_mgmt.c
Normal file
368
src/fido/cbor_cred_mgmt.c
Normal file
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* This file is part of the Pico FIDO distribution (https://github.com/polhenarejos/pico-fido).
|
||||
* Copyright (c) 2022 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ctap2_cbor.h"
|
||||
#include "fido.h"
|
||||
#include "ctap.h"
|
||||
#include "bsp/board.h"
|
||||
#include "cbor_make_credential.h"
|
||||
#include "files.h"
|
||||
#include "apdu.h"
|
||||
#include "credential.h"
|
||||
#include "hsm.h"
|
||||
|
||||
uint8_t rp_counter = 1;
|
||||
uint8_t rp_total = 0;
|
||||
uint8_t cred_counter = 1;
|
||||
uint8_t cred_total = 0;
|
||||
CborByteString rpIdHashx = {0};
|
||||
|
||||
int cbor_cred_mgmt(const uint8_t *data, size_t len) {
|
||||
CborParser parser;
|
||||
CborValue map;
|
||||
CborError error = CborNoError;
|
||||
uint64_t subcommand = 0, pinUvAuthProtocol = 0;
|
||||
CborByteString pinUvAuthParam = {0}, rpIdHash = {0};
|
||||
PublicKeyCredentialDescriptor credentialId = {0};
|
||||
PublicKeyCredentialUserEntity user = {0};
|
||||
size_t resp_size = 0;
|
||||
CborEncoder encoder, mapEncoder, mapEncoder2;
|
||||
uint8_t *raw_subpara = NULL;
|
||||
size_t raw_subpara_len = 0;
|
||||
bool asserted = false;
|
||||
|
||||
CBOR_CHECK(cbor_parser_init(data, len, 0, &parser, &map));
|
||||
uint64_t val_c = 1;
|
||||
CBOR_PARSE_MAP_START(map, 1) {
|
||||
uint64_t val_u = 0;
|
||||
CBOR_FIELD_GET_UINT(val_u, 1);
|
||||
if (val_c <= 1 && val_c != val_u)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
if (val_u < val_c)
|
||||
CBOR_ERROR(CTAP2_ERR_INVALID_CBOR);
|
||||
val_c = val_u + 1;
|
||||
if (val_u == 0x01) {
|
||||
CBOR_FIELD_GET_UINT(subcommand, 1);
|
||||
}
|
||||
else if (val_u == 0x02) {
|
||||
uint64_t subpara = 0;
|
||||
raw_subpara = (uint8_t *)cbor_value_get_next_byte(&_f1);
|
||||
CBOR_PARSE_MAP_START(_f1, 2) {
|
||||
CBOR_FIELD_GET_UINT(subpara, 2);
|
||||
if (subpara == 0x01) {
|
||||
CBOR_FIELD_GET_BYTES(rpIdHash, 2);
|
||||
}
|
||||
else if (subpara == 0x02) {
|
||||
|
||||
CBOR_PARSE_MAP_START(_f2, 3) {
|
||||
CBOR_FIELD_GET_KEY_TEXT(3);
|
||||
CBOR_FIELD_KEY_TEXT_VAL_BYTES(3, "id", credentialId.id);
|
||||
CBOR_FIELD_KEY_TEXT_VAL_TEXT(3, "type", credentialId.type);
|
||||
if (strcmp(_fd3, "transports") == 0) {
|
||||
CBOR_PARSE_ARRAY_START(_f3, 4) {
|
||||
CBOR_FIELD_GET_TEXT(credentialId.transports[credentialId.transports_len], 4);
|
||||
credentialId.transports_len++;
|
||||
}
|
||||
CBOR_PARSE_ARRAY_END(_f3, 4);
|
||||
}
|
||||
}
|
||||
CBOR_PARSE_MAP_END(_f2, 3);
|
||||
}
|
||||
else if (subpara == 0x03) {
|
||||
CBOR_PARSE_MAP_START(_f1, 3) {
|
||||
CBOR_FIELD_GET_KEY_TEXT(3);
|
||||
CBOR_FIELD_KEY_TEXT_VAL_BYTES(3, "id", user.id);
|
||||
CBOR_FIELD_KEY_TEXT_VAL_TEXT(3, "name", user.parent.name);
|
||||
CBOR_FIELD_KEY_TEXT_VAL_TEXT(3, "displayName", user.displayName);
|
||||
}
|
||||
CBOR_PARSE_MAP_END(_f1, 3);
|
||||
}
|
||||
}
|
||||
CBOR_PARSE_MAP_END(_f1, 2);
|
||||
raw_subpara_len = cbor_value_get_next_byte(&_f1) - raw_subpara;
|
||||
}
|
||||
else if (val_u == 0x03) {
|
||||
CBOR_FIELD_GET_UINT(pinUvAuthProtocol, 1);
|
||||
}
|
||||
else if (val_u == 0x04) { // pubKeyCredParams
|
||||
CBOR_FIELD_GET_BYTES(pinUvAuthParam, 1);
|
||||
}
|
||||
}
|
||||
CBOR_PARSE_MAP_END(map, 1);
|
||||
|
||||
if (subcommand != 0x03 && subcommand != 0x05) {
|
||||
if (pinUvAuthParam.present == false)
|
||||
CBOR_ERROR(CTAP2_ERR_PUAT_REQUIRED);
|
||||
if (pinUvAuthProtocol != 1 && pinUvAuthProtocol != 2)
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
cbor_encoder_init(&encoder, ctap_resp->init.data + 1, CTAP_MAX_PACKET_SIZE, 0);
|
||||
if(subcommand == 0x01) {
|
||||
if (verify(pinUvAuthProtocol, paut.data, (const uint8_t *)"\x01", 1, pinUvAuthParam.data) != CborNoError)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
uint8_t existing = 0;
|
||||
for (int i = 0; i < MAX_RESIDENT_CREDENTIALS; i++) {
|
||||
if (file_has_data(search_dynamic_file(EF_CRED + i)))
|
||||
existing++;
|
||||
}
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, 2));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x01));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, existing));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x02));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, MAX_RESIDENT_CREDENTIALS-existing));
|
||||
}
|
||||
else if (subcommand == 0x02 || subcommand == 0x03) {
|
||||
file_t *rp_ef = NULL;
|
||||
if (subcommand == 0x02) {
|
||||
if (verify(pinUvAuthProtocol, paut.data, (const uint8_t *)"\x02", 1, pinUvAuthParam.data) != CborNoError)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
rp_counter = 1;
|
||||
rp_total = 0;
|
||||
}
|
||||
else {
|
||||
if (rp_counter > rp_total)
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
}
|
||||
uint8_t skip = 0;
|
||||
for (int i = 0; i < MAX_RESIDENT_CREDENTIALS; i++) {
|
||||
file_t *tef = search_dynamic_file(EF_RP + i);
|
||||
if (file_has_data(tef) && *file_get_data(tef) > 0) {
|
||||
if (++skip == rp_counter) {
|
||||
if (rp_ef == NULL)
|
||||
rp_ef = tef;
|
||||
if (subcommand == 0x03)
|
||||
break;
|
||||
}
|
||||
if (subcommand == 0x02)
|
||||
rp_total++;
|
||||
}
|
||||
}
|
||||
if (rp_ef == NULL) // should not happen
|
||||
CBOR_ERROR(CTAP2_ERR_OPERATION_DENIED);
|
||||
rp_counter++;
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, subcommand == 0x02 ? 3 : 2));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x03));
|
||||
CBOR_CHECK(cbor_encoder_create_map(&mapEncoder, &mapEncoder2, 1));
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "id"));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder2, file_get_data(rp_ef)+33, file_get_size(rp_ef)-33));
|
||||
CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &mapEncoder2));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x04));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, file_get_data(rp_ef)+1, 32));
|
||||
if (subcommand == 0x02) {
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x05));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, rp_total));
|
||||
}
|
||||
}
|
||||
else if (subcommand == 0x04 || subcommand == 0x05) {
|
||||
if (subcommand == 0x04 && rpIdHash.present == false)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
if (subcommand == 0x04) {
|
||||
*(raw_subpara-1) = 0x04;
|
||||
if (verify(pinUvAuthProtocol, paut.data, raw_subpara-1, raw_subpara_len+1, pinUvAuthParam.data) != CborNoError)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
cred_counter = 1;
|
||||
cred_total = 0;
|
||||
}
|
||||
else {
|
||||
if (cred_counter > cred_total) {
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
}
|
||||
rpIdHash = rpIdHashx;
|
||||
}
|
||||
file_t *cred_ef = NULL;
|
||||
uint8_t skip = 0;
|
||||
for (int i = 0; i < MAX_RESIDENT_CREDENTIALS; i++) {
|
||||
file_t *tef = search_dynamic_file(EF_CRED + i);
|
||||
if (file_has_data(tef) && memcmp(file_get_data(tef), rpIdHash.data, 32) == 0) {
|
||||
if (++skip == cred_counter) {
|
||||
if (cred_ef == NULL)
|
||||
cred_ef = tef;
|
||||
if (subcommand == 0x05)
|
||||
break;
|
||||
}
|
||||
if (subcommand == 0x04)
|
||||
cred_total++;
|
||||
}
|
||||
}
|
||||
if (!file_has_data(cred_ef))
|
||||
CBOR_ERROR(CTAP2_ERR_NO_CREDENTIALS);
|
||||
|
||||
Credential cred = {0};
|
||||
if (credential_load(file_get_data(cred_ef)+32, file_get_size(cred_ef)-32, rpIdHash.data, &cred) != 0)
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
|
||||
mbedtls_ecdsa_context key;
|
||||
mbedtls_ecdsa_init(&key);
|
||||
if (fido_load_key(cred.curve, cred.id.data, &key) != 0) {
|
||||
credential_free(&cred);
|
||||
mbedtls_ecdsa_free(&key);
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
cred_counter++;
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, subcommand == 0x04 ? 5 : 4));
|
||||
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x06));
|
||||
uint8_t l = 0;
|
||||
if (cred.userId.present == true)
|
||||
l++;
|
||||
if (cred.userName.present == true)
|
||||
l++;
|
||||
if (cred.userDisplayName.present == true)
|
||||
l++;
|
||||
CBOR_CHECK(cbor_encoder_create_map(&mapEncoder, &mapEncoder2, l));
|
||||
if (cred.userId.present == true) {
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "id"));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder2, cred.userId.data, cred.userId.len));
|
||||
}
|
||||
if (cred.userName.present == true) {
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "name"));
|
||||
CBOR_CHECK(cbor_encode_text_string(&mapEncoder2, cred.userName.data, cred.userName.len));
|
||||
}
|
||||
if (cred.userDisplayName.present == true) {
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "displayName"));
|
||||
CBOR_CHECK(cbor_encode_text_string(&mapEncoder2, cred.userDisplayName.data, cred.userDisplayName.len));
|
||||
}
|
||||
CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &mapEncoder2));
|
||||
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x07));
|
||||
CBOR_CHECK(cbor_encoder_create_map(&mapEncoder, &mapEncoder2, 2));
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "id"));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder2, cred.id.data, cred.id.len));
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "type"));
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "public-key"));
|
||||
CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &mapEncoder2));
|
||||
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x08));
|
||||
CBOR_CHECK(cbor_encoder_create_map(&mapEncoder, &mapEncoder2, 5));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder2, 1));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder2, 2));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder2, 3));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder2, cred.alg));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder2, 1));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder2, cred.curve));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder2, 2));
|
||||
uint8_t pkey[66];
|
||||
mbedtls_mpi_write_binary(&key.Q.X, pkey, mbedtls_mpi_size(&key.Q.X));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder2, pkey, mbedtls_mpi_size(&key.Q.X)));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder2, 3));
|
||||
mbedtls_mpi_write_binary(&key.Q.Y, pkey, mbedtls_mpi_size(&key.Q.Y));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder2, pkey, mbedtls_mpi_size(&key.Q.Y)));
|
||||
CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &mapEncoder2));
|
||||
|
||||
if (subcommand == 0x04) {
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x09));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, cred_total));
|
||||
}
|
||||
if (cred_counter <= cred_total) {
|
||||
asserted = true;
|
||||
rpIdHashx = rpIdHash;
|
||||
}
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x0A));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, cred.extensions.credProtect));
|
||||
credential_free(&cred);
|
||||
mbedtls_ecdsa_free(&key);
|
||||
}
|
||||
else if (subcommand == 0x06) {
|
||||
if (credentialId.id.present == false)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
*(raw_subpara - 1) = 0x06;
|
||||
if (verify(pinUvAuthProtocol, paut.data, raw_subpara-1, raw_subpara_len+1, pinUvAuthParam.data) != CborNoError)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
for (int i = 0; i < MAX_RESIDENT_CREDENTIALS; i++) {
|
||||
file_t *ef = search_dynamic_file(EF_CRED + i);
|
||||
if (file_has_data(ef) && memcmp(file_get_data(ef)+32, credentialId.id.data, MIN(file_get_size(ef)-32, credentialId.id.len)) == 0) {
|
||||
uint8_t *rp_id_hash = file_get_data(ef);
|
||||
if (delete_file(ef) != 0)
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
for (int j = 0; j < MAX_RESIDENT_CREDENTIALS; j++) {
|
||||
file_t *rp_ef = search_dynamic_file(EF_RP + j);
|
||||
if (file_has_data(rp_ef) && memcmp(file_get_data(rp_ef)+1, rp_id_hash, 32) == 0) {
|
||||
uint8_t *rp_data = (uint8_t *)calloc(1, file_get_size(rp_ef));
|
||||
memcpy(rp_data, file_get_data(rp_ef), file_get_size(rp_ef));
|
||||
rp_data[0] -= 1;
|
||||
if (rp_data[0] == 0)
|
||||
delete_file(rp_ef);
|
||||
else
|
||||
flash_write_data_to_file(rp_ef, rp_data, file_get_size(rp_ef));
|
||||
free(rp_data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
low_flash_available();
|
||||
goto err; //no error
|
||||
}
|
||||
}
|
||||
CBOR_ERROR(CTAP2_ERR_NO_CREDENTIALS);
|
||||
}
|
||||
else if (subcommand == 0x07) {
|
||||
if (credentialId.id.present == false || user.id.present == false)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
*(raw_subpara - 1) = 0x07;
|
||||
if (verify(pinUvAuthProtocol, paut.data, raw_subpara-1, raw_subpara_len+1, pinUvAuthParam.data) != CborNoError)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
for (int i = 0; i < MAX_RESIDENT_CREDENTIALS; i++) {
|
||||
file_t *ef = search_dynamic_file(EF_CRED + i);
|
||||
if (file_has_data(ef) && memcmp(file_get_data(ef)+32, credentialId.id.data, MIN(file_get_size(ef)-32, credentialId.id.len)) == 0) {
|
||||
Credential cred = {0};
|
||||
uint8_t *rp_id_hash = file_get_data(ef);
|
||||
if (credential_load(rp_id_hash+32, file_get_size(ef)-32, rp_id_hash, &cred) != 0)
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
if (memcmp(user.id.data, cred.userId.data, MIN(user.id.len, cred.userId.len)) != 0) {
|
||||
credential_free(&cred);
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
uint8_t newcred[MAX_CRED_ID_LENGTH];
|
||||
size_t newcred_len = 0;
|
||||
if (credential_create(&cred.rpId, &cred.userId, &user.parent.name, &user.displayName, &cred.opts, &cred.extensions, cred.use_sign_count, cred.alg, cred.curve, newcred, &newcred_len) != 0) {
|
||||
credential_free(&cred);
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
}
|
||||
credential_free(&cred);
|
||||
if (credential_store(newcred, newcred_len, rp_id_hash) != 0) {
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
}
|
||||
low_flash_available();
|
||||
goto err; //no error
|
||||
}
|
||||
}
|
||||
CBOR_ERROR(CTAP2_ERR_NO_CREDENTIALS);
|
||||
}
|
||||
CBOR_CHECK(cbor_encoder_close_container(&encoder, &mapEncoder));
|
||||
resp_size = cbor_encoder_get_buffer_size(&encoder, ctap_resp->init.data + 1);
|
||||
err:
|
||||
CBOR_FREE_BYTE_STRING(pinUvAuthParam);
|
||||
|
||||
if (asserted == false) {
|
||||
CBOR_FREE_BYTE_STRING(rpIdHash);
|
||||
}
|
||||
CBOR_FREE_BYTE_STRING(user.id);
|
||||
CBOR_FREE_BYTE_STRING(user.displayName);
|
||||
CBOR_FREE_BYTE_STRING(user.parent.name);
|
||||
CBOR_FREE_BYTE_STRING(credentialId.type);
|
||||
for (int n = 0; n < credentialId.transports_len; n++) {
|
||||
CBOR_FREE_BYTE_STRING(credentialId.transports[n]);
|
||||
}
|
||||
if (error != CborNoError) {
|
||||
if (error == CborErrorImproperValue)
|
||||
return CTAP2_ERR_CBOR_UNEXPECTED_TYPE;
|
||||
return error;
|
||||
}
|
||||
res_APDU_size = resp_size;
|
||||
return 0;
|
||||
}
|
||||
@@ -29,6 +29,8 @@
|
||||
#include "credential.h"
|
||||
#include <math.h>
|
||||
|
||||
int cbor_get_assertion(const uint8_t *data, size_t len, bool next);
|
||||
|
||||
bool residentx = false;
|
||||
Credential credsx[MAX_CREDENTIAL_COUNT_IN_LIST] = {0};
|
||||
uint8_t credentialCounter = 1;
|
||||
@@ -48,7 +50,7 @@ int cbor_get_next_assertion(const uint8_t *data, size_t len) {
|
||||
timerx = board_millis();
|
||||
credentialCounter++;
|
||||
err:
|
||||
if (error != CborNoError) {
|
||||
if (error != CborNoError || credentialCounter == numberOfCredentialsx) {
|
||||
for (int i = 0; i < MAX_CREDENTIAL_COUNT_IN_LIST; i++)
|
||||
credential_free(&credsx[i]);
|
||||
if (datax) {
|
||||
@@ -196,6 +198,9 @@ int cbor_get_assertion(const uint8_t *data, size_t len, bool next) {
|
||||
}
|
||||
CBOR_PARSE_MAP_END(map, 1);
|
||||
|
||||
if (rpId.present == false || clientDataHash.present == false)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
|
||||
uint8_t flags = 0;
|
||||
uint8_t rp_id_hash[32];
|
||||
mbedtls_sha256((uint8_t *)rpId.data, rpId.len, rp_id_hash, 0);
|
||||
@@ -246,12 +251,11 @@ int cbor_get_assertion(const uint8_t *data, size_t len, bool next) {
|
||||
if (extensions.present == true && extensions.hmac_secret == ptrue) {
|
||||
if (kax.present == false || kay.present == false || crv == 0 || alg == 0 || salt_enc.present == false || salt_auth.present == false)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
if (salt_enc.len != 32 && salt_enc.len != 64)
|
||||
if (salt_enc.len != 32+(hmacSecretPinUvAuthProtocol-1)*IV_SIZE && salt_enc.len != 64+(hmacSecretPinUvAuthProtocol-1)*IV_SIZE)
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_LEN);
|
||||
}
|
||||
|
||||
if (allowList_len > 0)
|
||||
{
|
||||
if (allowList_len > 0) {
|
||||
for (int e = 0; e < allowList_len; e++) {
|
||||
if (allowList[e].type.present == false || allowList[e].id.present == false)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
@@ -402,7 +406,7 @@ int cbor_get_assertion(const uint8_t *data, size_t len, bool next) {
|
||||
mbedtls_platform_zeroize(sharedSecret, sizeof(sharedSecret));
|
||||
CBOR_ERROR(CTAP2_ERR_EXTENSION_FIRST);
|
||||
}
|
||||
uint8_t salt_dec[64];
|
||||
uint8_t salt_dec[64], poff = (hmacSecretPinUvAuthProtocol-1)*IV_SIZE;
|
||||
ret = decrypt(hmacSecretPinUvAuthProtocol, sharedSecret, salt_enc.data, salt_enc.len, salt_dec);
|
||||
if (ret != 0) {
|
||||
mbedtls_platform_zeroize(sharedSecret, sizeof(sharedSecret));
|
||||
@@ -418,11 +422,11 @@ int cbor_get_assertion(const uint8_t *data, size_t len, bool next) {
|
||||
crd = cred_random + 32;
|
||||
else
|
||||
crd = cred_random;
|
||||
uint8_t out1[64], hmac_res[64];
|
||||
uint8_t out1[64], hmac_res[80];
|
||||
mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), crd, 32, salt_dec, 32, out1);
|
||||
if (salt_enc.len == 64)
|
||||
if (salt_enc.len == 64+poff)
|
||||
mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), crd, 32, salt_dec+32, 32, out1+32);
|
||||
encrypt(hmacSecretPinUvAuthProtocol, sharedSecret, out1, salt_enc.len, hmac_res);
|
||||
encrypt(hmacSecretPinUvAuthProtocol, sharedSecret, out1, salt_enc.len-poff, hmac_res);
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, hmac_res, salt_enc.len));
|
||||
}
|
||||
|
||||
@@ -456,7 +460,7 @@ int cbor_get_assertion(const uint8_t *data, size_t len, bool next) {
|
||||
uint8_t lfields = 3;
|
||||
if (selcred->opts.present == true && selcred->opts.rk == ptrue)
|
||||
lfields++;
|
||||
if (numberOfCredentials > 1 && next == false)
|
||||
if (numberOfCredentials > 1 && next == false && !(flags & FIDO2_AUT_FLAG_UP) && !(flags & FIDO2_AUT_FLAG_UV))
|
||||
lfields++;
|
||||
cbor_encoder_init(&encoder, ctap_resp->init.data + 1, CTAP_MAX_PACKET_SIZE, 0);
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, lfields));
|
||||
@@ -476,23 +480,44 @@ int cbor_get_assertion(const uint8_t *data, size_t len, bool next) {
|
||||
|
||||
if (selcred->opts.present == true && selcred->opts.rk == ptrue) {
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x04));
|
||||
CBOR_CHECK(cbor_encoder_create_map(&mapEncoder, &mapEncoder2, 1));
|
||||
uint8_t lu = 1;
|
||||
if (numberOfCredentials > 1 && allowList_len == 0) {
|
||||
if (selcred->userName.present == true)
|
||||
lu++;
|
||||
if (selcred->userDisplayName.present == true)
|
||||
lu++;
|
||||
}
|
||||
CBOR_CHECK(cbor_encoder_create_map(&mapEncoder, &mapEncoder2, lu));
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "id"));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder2, selcred->userId.data, selcred->userId.len));
|
||||
if (numberOfCredentials > 1 && allowList_len == 0) {
|
||||
if (selcred->userName.present == true) {
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "name"));
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, selcred->userName.data));
|
||||
}
|
||||
if (selcred->userDisplayName.present == true) {
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, "displayName"));
|
||||
CBOR_CHECK(cbor_encode_text_stringz(&mapEncoder2, selcred->userDisplayName.data));
|
||||
}
|
||||
}
|
||||
CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &mapEncoder2));
|
||||
}
|
||||
if (numberOfCredentials > 1 && next == false) {
|
||||
if (numberOfCredentials > 1 && next == false && !(flags & FIDO2_AUT_FLAG_UP) && !(flags & FIDO2_AUT_FLAG_UV)) {
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x05));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, numberOfCredentials));
|
||||
}
|
||||
CBOR_CHECK(cbor_encoder_close_container(&encoder, &mapEncoder));
|
||||
resp_size = cbor_encoder_get_buffer_size(&encoder, ctap_resp->init.data + 1);
|
||||
|
||||
ctr++;
|
||||
flash_write_data_to_file(ef_counter, (uint8_t *)&ctr, sizeof(ctr));
|
||||
low_flash_available();
|
||||
err:
|
||||
CBOR_FREE_BYTE_STRING(clientDataHash);
|
||||
CBOR_FREE_BYTE_STRING(pinUvAuthParam);
|
||||
CBOR_FREE_BYTE_STRING(rpId);
|
||||
if (asserted == false) {
|
||||
CBOR_FREE_BYTE_STRING(clientDataHash);
|
||||
CBOR_FREE_BYTE_STRING(pinUvAuthParam);
|
||||
CBOR_FREE_BYTE_STRING(rpId);
|
||||
for (int i = 0; i < MAX_CREDENTIAL_COUNT_IN_LIST; i++)
|
||||
credential_free(&creds[i]);
|
||||
}
|
||||
|
||||
for (int m = 0; m < allowList_len; m++) {
|
||||
|
||||
@@ -26,7 +26,7 @@ int cbor_get_info() {
|
||||
CborEncoder encoder, mapEncoder, arrayEncoder;
|
||||
CborError error = CborNoError;
|
||||
cbor_encoder_init(&encoder, ctap_resp->init.data + 1, CTAP_MAX_PACKET_SIZE, 0);
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, 9));
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, 10));
|
||||
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x01));
|
||||
CBOR_CHECK(cbor_encoder_create_array(&mapEncoder, &arrayEncoder, 3));
|
||||
@@ -79,6 +79,12 @@ int cbor_get_info() {
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x0E));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, PICO_FIDO_VERSION)); // firmwareVersion
|
||||
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x15));
|
||||
CBOR_CHECK(cbor_encoder_create_array(&mapEncoder, &arrayEncoder, 2));
|
||||
CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_AUT_ENABLE));
|
||||
CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_AUT_DISABLE));
|
||||
CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &arrayEncoder));
|
||||
|
||||
CBOR_CHECK(cbor_encoder_close_container(&encoder, &mapEncoder));
|
||||
err:
|
||||
if (error != CborNoError)
|
||||
|
||||
@@ -44,9 +44,9 @@ int cbor_make_credential(const uint8_t *data, size_t len) {
|
||||
size_t resp_size = 0;
|
||||
CredExtensions extensions = {0};
|
||||
//options.present = true;
|
||||
options.up = ptrue;
|
||||
options.uv = pfalse;
|
||||
options.rk = pfalse;
|
||||
//options.up = ptrue;
|
||||
//options.uv = pfalse;
|
||||
//options.rk = pfalse;
|
||||
|
||||
CBOR_CHECK(cbor_parser_init(data, len, 0, &parser, &map));
|
||||
uint64_t val_c = 1;
|
||||
@@ -75,6 +75,7 @@ int cbor_make_credential(const uint8_t *data, size_t len) {
|
||||
CBOR_FIELD_KEY_TEXT_VAL_BYTES(2, "id", user.id);
|
||||
CBOR_FIELD_KEY_TEXT_VAL_TEXT(2, "name", user.parent.name);
|
||||
CBOR_FIELD_KEY_TEXT_VAL_TEXT(2, "displayName", user.displayName);
|
||||
CBOR_ADVANCE(2);
|
||||
}
|
||||
CBOR_PARSE_MAP_END(_f1, 2);
|
||||
}
|
||||
@@ -230,7 +231,7 @@ int cbor_make_credential(const uint8_t *data, size_t len) {
|
||||
CBOR_ERROR(CTAP2_ERR_CREDENTIAL_EXCLUDED);
|
||||
}
|
||||
|
||||
if (options.up == ptrue) { //14.1
|
||||
if (options.up == ptrue || options.up == NULL) { //14.1
|
||||
if (pinUvAuthParam.present == true) {
|
||||
if (getUserPresentFlagValue() == false) {
|
||||
if (check_user_presence() == false)
|
||||
@@ -289,7 +290,7 @@ int cbor_make_credential(const uint8_t *data, size_t len) {
|
||||
const mbedtls_ecp_curve_info *cinfo = mbedtls_ecp_curve_info_from_grp_id(ekey.grp.id);
|
||||
if (cinfo == NULL)
|
||||
CBOR_ERROR(CTAP1_ERR_OTHER);
|
||||
size_t olen = 0, pkey_len = ceil((float)cinfo->bit_size/8);
|
||||
size_t olen = 0;
|
||||
uint32_t ctr = get_sign_counter();
|
||||
uint8_t cbor_buf[1024];
|
||||
cbor_encoder_init(&encoder, cbor_buf, sizeof(cbor_buf), 0);
|
||||
@@ -301,11 +302,11 @@ int cbor_make_credential(const uint8_t *data, size_t len) {
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder, 1));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, curve));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder, 2));
|
||||
mbedtls_mpi_write_binary(&ekey.Q.X, pkey, pkey_len);
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, pkey, pkey_len));
|
||||
mbedtls_mpi_write_binary(&ekey.Q.X, pkey, mbedtls_mpi_size(&ekey.Q.X));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, pkey, mbedtls_mpi_size(&ekey.Q.X)));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder, 3));
|
||||
mbedtls_mpi_write_binary(&ekey.Q.Y, pkey, pkey_len);
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, pkey, pkey_len));
|
||||
mbedtls_mpi_write_binary(&ekey.Q.Y, pkey, mbedtls_mpi_size(&ekey.Q.Y));
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, pkey, mbedtls_mpi_size(&ekey.Q.Y)));
|
||||
|
||||
CBOR_CHECK(cbor_encoder_close_container(&encoder, &mapEncoder));
|
||||
size_t rs = cbor_encoder_get_buffer_size(&encoder, cbor_buf);
|
||||
|
||||
28
src/fido/cbor_selection.c
Normal file
28
src/fido/cbor_selection.c
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
/*
|
||||
* This file is part of the Pico FIDO distribution (https://github.com/polhenarejos/pico-fido).
|
||||
* Copyright (c) 2022 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ctap2_cbor.h"
|
||||
#include "fido.h"
|
||||
#include "ctap.h"
|
||||
#include "bsp/board.h"
|
||||
|
||||
int cbor_selection() {
|
||||
if (wait_button_pressed() == true)
|
||||
return CTAP2_ERR_USER_ACTION_TIMEOUT;
|
||||
return CTAP2_OK;
|
||||
}
|
||||
253
src/fido/cbor_vendor.c
Normal file
253
src/fido/cbor_vendor.c
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* This file is part of the Pico FIDO distribution (https://github.com/polhenarejos/pico-fido).
|
||||
* Copyright (c) 2022 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
#include "ctap2_cbor.h"
|
||||
#include "fido.h"
|
||||
#include "ctap.h"
|
||||
#include "files.h"
|
||||
#include "apdu.h"
|
||||
#include "hsm.h"
|
||||
#include "random.h"
|
||||
#include "mbedtls/ecdh.h"
|
||||
#include "mbedtls/chachapoly.h"
|
||||
#include "mbedtls/hkdf.h"
|
||||
|
||||
extern uint8_t keydev_dec[32];
|
||||
extern bool has_keydev_dec;
|
||||
|
||||
mse_t mse = {.init = false};
|
||||
|
||||
int mse_decrypt_ct(uint8_t *data, size_t len) {
|
||||
mbedtls_chachapoly_context chatx;
|
||||
mbedtls_chachapoly_init(&chatx);
|
||||
mbedtls_chachapoly_setkey(&chatx, mse.key_enc + 12);
|
||||
int ret = mbedtls_chachapoly_auth_decrypt(&chatx, len - 16, mse.key_enc, mse.Qpt, 65, data + len - 16, data, data);
|
||||
mbedtls_chachapoly_free(&chatx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int cbor_vendor_generic(uint8_t cmd, const uint8_t *data, size_t len) {
|
||||
CborParser parser;
|
||||
CborValue map;
|
||||
CborError error = CborNoError;
|
||||
CborByteString pinUvAuthParam = {0}, vendorParam = {0}, kax = {0}, kay = {0};
|
||||
size_t resp_size = 0;
|
||||
uint64_t vendorCmd = 0, pinUvAuthProtocol = 0;
|
||||
int64_t kty = 0, alg = 0, crv = 0;
|
||||
CborEncoder encoder, mapEncoder, mapEncoder2;
|
||||
|
||||
CBOR_CHECK(cbor_parser_init(data, len, 0, &parser, &map));
|
||||
uint64_t val_c = 1;
|
||||
CBOR_PARSE_MAP_START(map, 1) {
|
||||
uint64_t val_u = 0;
|
||||
CBOR_FIELD_GET_UINT(val_u, 1);
|
||||
if (val_c <= 1 && val_c != val_u)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
if (val_u < val_c)
|
||||
CBOR_ERROR(CTAP2_ERR_INVALID_CBOR);
|
||||
val_c = val_u + 1;
|
||||
if (val_u == 0x01) {
|
||||
CBOR_FIELD_GET_UINT(vendorCmd, 1);
|
||||
}
|
||||
else if (val_u == 0x02) {
|
||||
uint64_t subpara = 0;
|
||||
CBOR_PARSE_MAP_START(_f1, 2) {
|
||||
CBOR_FIELD_GET_UINT(subpara, 2);
|
||||
if (subpara == 0x01) {
|
||||
CBOR_FIELD_GET_BYTES(vendorParam, 2);
|
||||
}
|
||||
else if (subpara == 0x02) {
|
||||
int64_t key = 0;
|
||||
CBOR_PARSE_MAP_START(_f2, 3) {
|
||||
CBOR_FIELD_GET_INT(key, 3);
|
||||
if (key == 1) {
|
||||
CBOR_FIELD_GET_INT(kty, 3);
|
||||
}
|
||||
else if (key == 3) {
|
||||
CBOR_FIELD_GET_INT(alg, 3);
|
||||
}
|
||||
else if (key == -1) {
|
||||
CBOR_FIELD_GET_INT(crv, 3);
|
||||
}
|
||||
else if (key == -2) {
|
||||
CBOR_FIELD_GET_BYTES(kax, 3);
|
||||
}
|
||||
else if (key == -3) {
|
||||
CBOR_FIELD_GET_BYTES(kay, 3);
|
||||
}
|
||||
else
|
||||
CBOR_ADVANCE(3);
|
||||
}
|
||||
CBOR_PARSE_MAP_END(_f2, 3);
|
||||
}
|
||||
else
|
||||
CBOR_ADVANCE(2);
|
||||
}
|
||||
CBOR_PARSE_MAP_END(_f1, 2);
|
||||
}
|
||||
else if (val_u == 0x03) {
|
||||
CBOR_FIELD_GET_UINT(pinUvAuthProtocol, 1);
|
||||
}
|
||||
else if (val_u == 0x04) {
|
||||
CBOR_FIELD_GET_BYTES(pinUvAuthParam, 1);
|
||||
}
|
||||
}
|
||||
CBOR_PARSE_MAP_END(map, 1);
|
||||
|
||||
cbor_encoder_init(&encoder, ctap_resp->init.data + 1, CTAP_MAX_PACKET_SIZE, 0);
|
||||
|
||||
if (cmd == CTAP_VENDOR_BACKUP) {
|
||||
if (vendorCmd == 0x01) {
|
||||
if (has_keydev_dec == false)
|
||||
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, 1));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x01));
|
||||
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, file_get_data(ef_keydev_enc), file_get_size(ef_keydev_enc)));
|
||||
}
|
||||
else if (vendorCmd == 0x02) {
|
||||
if (vendorParam.present == false)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
uint8_t zeros[32];
|
||||
memset(zeros, 0, sizeof(zeros));
|
||||
flash_write_data_to_file(ef_keydev_enc, vendorParam.data, vendorParam.len);
|
||||
flash_write_data_to_file(ef_keydev, zeros, file_get_size(ef_keydev)); // Overwrite ef with 0
|
||||
flash_write_data_to_file(ef_keydev, NULL, 0); // Set ef to 0 bytes
|
||||
low_flash_available();
|
||||
goto err;
|
||||
}
|
||||
else {
|
||||
CBOR_ERROR(CTAP2_ERR_INVALID_SUBCOMMAND);
|
||||
}
|
||||
}
|
||||
else if (cmd == CTAP_VENDOR_MSE) {
|
||||
if (vendorCmd == 0x01) { // KeyAgreement
|
||||
if (kax.present == false || kay.present == false || alg == 0)
|
||||
CBOR_ERROR(CTAP2_ERR_MISSING_PARAMETER);
|
||||
|
||||
mbedtls_ecdh_context hkey;
|
||||
mbedtls_ecdh_init(&hkey);
|
||||
mbedtls_ecdh_setup(&hkey, MBEDTLS_ECP_DP_SECP256R1);
|
||||
int ret = mbedtls_ecdh_gen_public(&hkey.ctx.mbed_ecdh.grp, &hkey.ctx.mbed_ecdh.d, &hkey.ctx.mbed_ecdh.Q, random_gen, NULL);
|
||||
mbedtls_mpi_lset(&hkey.ctx.mbed_ecdh.Qp.Z, 1);
|
||||
if (ret != 0) {
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
if (mbedtls_mpi_read_binary(&hkey.ctx.mbed_ecdh.Qp.X, kax.data, kax.len) != 0) {
|
||||
mbedtls_ecdh_free(&hkey);
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
if (mbedtls_mpi_read_binary(&hkey.ctx.mbed_ecdh.Qp.Y, kay.data, kay.len) != 0) {
|
||||
mbedtls_ecdh_free(&hkey);
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
uint8_t buf[MBEDTLS_ECP_MAX_BYTES];
|
||||
size_t olen = 0;
|
||||
ret = mbedtls_ecp_point_write_binary(&hkey.ctx.mbed_ecdh.grp, &hkey.ctx.mbed_ecdh.Qp, MBEDTLS_ECP_PF_UNCOMPRESSED, &olen, mse.Qpt, sizeof(mse.Qpt));
|
||||
if (ret != 0) {
|
||||
mbedtls_ecdh_free(&hkey);
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
ret = mbedtls_ecdh_calc_secret(&hkey, &olen, buf, MBEDTLS_ECP_MAX_BYTES, random_gen, NULL);
|
||||
if (ret != 0) {
|
||||
mbedtls_ecdh_free(&hkey);
|
||||
mbedtls_platform_zeroize(buf, sizeof(buf));
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
ret = mbedtls_hkdf(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), NULL, 0, buf, olen, mse.Qpt, sizeof(mse.Qpt), mse.key_enc, sizeof(mse.key_enc));
|
||||
mbedtls_platform_zeroize(buf, sizeof(buf));
|
||||
if (ret != 0) {
|
||||
mbedtls_ecdh_free(&hkey);
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
mse.init = true;
|
||||
|
||||
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, 1));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x01));
|
||||
|
||||
CBOR_CHECK(cbor_encoder_create_map(&mapEncoder, &mapEncoder2, 5));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder2, 1));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder2, 2));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder2, 3));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder2, -FIDO2_ALG_ECDH_ES_HKDF_256));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder2, 1));
|
||||
CBOR_CHECK(cbor_encode_uint(&mapEncoder2, FIDO2_CURVE_P256));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder2, 2));
|
||||
uint8_t pkey[32];
|
||||
mbedtls_mpi_write_binary(&hkey.ctx.mbed_ecdh.Q.X, pkey, 32);
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder2, pkey, 32));
|
||||
CBOR_CHECK(cbor_encode_negative_int(&mapEncoder2, 3));
|
||||
mbedtls_mpi_write_binary(&hkey.ctx.mbed_ecdh.Q.Y, pkey, 32);
|
||||
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder2, pkey, 32));
|
||||
CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &mapEncoder2));
|
||||
mbedtls_ecdh_free(&hkey);
|
||||
}
|
||||
}
|
||||
else if (cmd == CTAP_VENDOR_UNLOCK) {
|
||||
if (mse.init == false)
|
||||
CBOR_ERROR(CTAP2_ERR_NOT_ALLOWED);
|
||||
|
||||
mbedtls_chachapoly_context chatx;
|
||||
int ret = mse_decrypt_ct(vendorParam.data, vendorParam.len);
|
||||
if (ret != 0) {
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
if (!file_has_data(ef_keydev_enc))
|
||||
CBOR_ERROR(CTAP2_ERR_INTEGRITY_FAILURE);
|
||||
|
||||
uint8_t *keyenc = file_get_data(ef_keydev_enc);
|
||||
size_t keyenc_len = file_get_size(ef_keydev_enc);
|
||||
mbedtls_chachapoly_init(&chatx);
|
||||
mbedtls_chachapoly_setkey(&chatx, vendorParam.data);
|
||||
ret = mbedtls_chachapoly_auth_decrypt(&chatx, sizeof(keydev_dec), keyenc, NULL, 0, keyenc + keyenc_len - 16, keyenc + 12, keydev_dec);
|
||||
mbedtls_chachapoly_free(&chatx);
|
||||
if (ret != 0){
|
||||
CBOR_ERROR(CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
has_keydev_dec = true;
|
||||
goto err;
|
||||
}
|
||||
else
|
||||
CBOR_ERROR(CTAP2_ERR_UNSUPPORTED_OPTION);
|
||||
CBOR_CHECK(cbor_encoder_close_container(&encoder, &mapEncoder));
|
||||
resp_size = cbor_encoder_get_buffer_size(&encoder, ctap_resp->init.data + 1);
|
||||
|
||||
err:
|
||||
CBOR_FREE_BYTE_STRING(pinUvAuthParam);
|
||||
CBOR_FREE_BYTE_STRING(vendorParam);
|
||||
|
||||
if (error != CborNoError) {
|
||||
if (error == CborErrorImproperValue)
|
||||
return CTAP2_ERR_CBOR_UNEXPECTED_TYPE;
|
||||
return error;
|
||||
}
|
||||
res_APDU_size = resp_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cbor_vendor(const uint8_t *data, size_t len) {
|
||||
if (len == 0)
|
||||
return CTAP1_ERR_INVALID_LEN;
|
||||
if (data[0] >= CTAP_VENDOR_BACKUP)
|
||||
return cbor_vendor_generic(data[0], data + 1, len - 1);
|
||||
return CTAP2_ERR_INVALID_CBOR;
|
||||
}
|
||||
@@ -42,21 +42,22 @@ int cmd_authenticate() {
|
||||
uint8_t *tmp_kh = (uint8_t *)calloc(1, req->keyHandleLen);
|
||||
memcpy(tmp_kh, req->keyHandle, req->keyHandleLen);
|
||||
if (credential_verify(tmp_kh, req->keyHandleLen, req->appId) == 0) {
|
||||
DEBUG_DATA(req->keyHandle, req->keyHandleLen);
|
||||
ret = fido_load_key(FIDO2_CURVE_P256, req->keyHandle, &key);
|
||||
}
|
||||
else {
|
||||
ret = derive_key(req->appId, false, req->keyHandle, MBEDTLS_ECP_DP_SECP256R1, &key);
|
||||
if (verify_key(req->appId, req->keyHandle, &key) != 0) {
|
||||
mbedtls_ecdsa_free(&key);
|
||||
return SW_INCORRECT_PARAMS();
|
||||
}
|
||||
}
|
||||
free(tmp_kh);
|
||||
if (ret != CCID_OK) {
|
||||
mbedtls_ecdsa_free(&key);
|
||||
return SW_EXEC_ERROR();
|
||||
}
|
||||
if (verify_key(req->appId, req->keyHandle, &key) != 0) {
|
||||
return SW_INCORRECT_PARAMS();
|
||||
}
|
||||
if (P1(apdu) == CTAP_AUTH_CHECK_ONLY) {
|
||||
mbedtls_ecdsa_free(&key);
|
||||
return SW_CONDITIONS_NOT_SATISFIED();
|
||||
}
|
||||
resp->flags = 0;
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
#include "random.h"
|
||||
#include "files.h"
|
||||
|
||||
const uint8_t *bogus_firefox = (const uint8_t *)"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
const uint8_t *bogus_chrome = (const uint8_t *)"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
|
||||
extern int ctap_error(uint8_t error);
|
||||
int cmd_register() {
|
||||
CTAP_REGISTER_REQ *req = (CTAP_REGISTER_REQ *)apdu.data;
|
||||
CTAP_REGISTER_RESP *resp = (CTAP_REGISTER_RESP *)res_APDU;
|
||||
@@ -34,6 +38,8 @@ int cmd_register() {
|
||||
return SW_WRONG_LENGTH();
|
||||
if (wait_button_pressed() == true)
|
||||
return SW_CONDITIONS_NOT_SATISFIED();
|
||||
if (memcmp(req->appId, bogus_firefox, CTAP_APPID_SIZE) == 0 || memcmp(req->appId, bogus_chrome, CTAP_APPID_SIZE) == 0)
|
||||
return ctap_error(CTAP1_ERR_CHANNEL_BUSY);
|
||||
mbedtls_ecdsa_context key;
|
||||
mbedtls_ecdsa_init(&key);
|
||||
int ret = derive_key(req->appId, true, resp->keyHandleCertSig, MBEDTLS_ECP_DP_SECP256R1, &key);
|
||||
|
||||
@@ -38,7 +38,9 @@ int credential_verify(uint8_t *cred_id, size_t cred_id_len, const uint8_t *rp_id
|
||||
mbedtls_chachapoly_context chatx;
|
||||
mbedtls_chachapoly_init(&chatx);
|
||||
mbedtls_chachapoly_setkey(&chatx, key);
|
||||
return mbedtls_chachapoly_auth_decrypt(&chatx, cred_id_len - (4 + 12 + 16), iv, rp_id_hash, 32, tag, cipher, cipher);
|
||||
int ret = mbedtls_chachapoly_auth_decrypt(&chatx, cred_id_len - (4 + 12 + 16), iv, rp_id_hash, 32, tag, cipher, cipher);
|
||||
mbedtls_chachapoly_free(&chatx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int credential_create(CborCharString *rpId, CborByteString *userId, CborCharString *userName, CborCharString *userDisplayName, CredOptions *opts, CredExtensions *extensions, bool use_sign_count, int alg, int curve, uint8_t *cred_id, size_t *cred_id_len) {
|
||||
@@ -90,7 +92,7 @@ int credential_create(CborCharString *rpId, CborByteString *userId, CborCharStri
|
||||
memset(key, 0, sizeof(key));
|
||||
credential_derive_chacha_key(key);
|
||||
uint8_t iv[12];
|
||||
random_gen(NULL, iv, sizeof(12));
|
||||
random_gen(NULL, iv, sizeof(iv));
|
||||
mbedtls_chachapoly_context chatx;
|
||||
mbedtls_chachapoly_init(&chatx);
|
||||
mbedtls_chachapoly_setkey(&chatx, key);
|
||||
@@ -99,7 +101,7 @@ int credential_create(CborCharString *rpId, CborByteString *userId, CborCharStri
|
||||
if (ret != 0) {
|
||||
CBOR_ERROR(CTAP1_ERR_OTHER);
|
||||
}
|
||||
memcpy(cred_id, "\xf1\xd0\x02\x00", 4);
|
||||
memcpy(cred_id, CRED_PROTO, 4);
|
||||
memcpy(cred_id + 4, iv, 12);
|
||||
|
||||
err:
|
||||
@@ -137,6 +139,12 @@ int credential_load(const uint8_t *cred_id, size_t cred_id_len, const uint8_t *r
|
||||
else if (val_u == 0x03) {
|
||||
CBOR_FIELD_GET_BYTES(cred->userId, 1);
|
||||
}
|
||||
else if (val_u == 0x04) {
|
||||
CBOR_FIELD_GET_TEXT(cred->userName, 1);
|
||||
}
|
||||
else if (val_u == 0x05) {
|
||||
CBOR_FIELD_GET_TEXT(cred->userDisplayName, 1);
|
||||
}
|
||||
else if (val_u == 0x06) {
|
||||
CBOR_FIELD_GET_UINT(cred->creation, 1);
|
||||
}
|
||||
@@ -205,6 +213,7 @@ int credential_store(const uint8_t *cred_id, size_t cred_id_len, const uint8_t *
|
||||
int sloti = -1;
|
||||
Credential cred = {0};
|
||||
int ret = 0;
|
||||
bool new_record = true;
|
||||
ret = credential_load(cred_id, cred_id_len, rp_id_hash, &cred);
|
||||
if (ret != 0) {
|
||||
credential_free(&cred);
|
||||
@@ -228,18 +237,55 @@ int credential_store(const uint8_t *cred_id, size_t cred_id_len, const uint8_t *
|
||||
if (memcmp(rcred.userId.data, cred.userId.data, MIN(rcred.userId.len, cred.userId.len)) == 0) {
|
||||
sloti = i;
|
||||
credential_free(&rcred);
|
||||
new_record = false;
|
||||
break;
|
||||
}
|
||||
credential_free(&rcred);
|
||||
}
|
||||
if (sloti == -1)
|
||||
return -1;
|
||||
credential_free(&cred);
|
||||
uint8_t *data = (uint8_t *)calloc(1, cred_id_len+32);
|
||||
memcpy(data, rp_id_hash, 32);
|
||||
memcpy(data + 32, cred_id, cred_id_len);
|
||||
file_t *ef = file_new(EF_CRED+sloti);
|
||||
flash_write_data_to_file(ef, data, cred_id_len + 32);
|
||||
free(data);
|
||||
|
||||
if (new_record == true) { //increase rps
|
||||
sloti = -1;
|
||||
for (int i = 0; i < MAX_RESIDENT_CREDENTIALS; i++) {
|
||||
ef = search_dynamic_file(EF_RP + i);
|
||||
if (!file_has_data(ef)) {
|
||||
if (sloti == -1)
|
||||
sloti = i;
|
||||
continue;
|
||||
}
|
||||
if (memcmp(file_get_data(ef)+1, rp_id_hash, 32) == 0) {
|
||||
sloti = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sloti == -1)
|
||||
return -1;
|
||||
ef = search_dynamic_file(EF_RP + sloti);
|
||||
if (file_has_data(ef)) {
|
||||
data = (uint8_t *)calloc(1, file_get_size(ef));
|
||||
memcpy(data, file_get_data(ef), file_get_size(ef));
|
||||
data[0] += 1;
|
||||
flash_write_data_to_file(ef, data, file_get_size(ef));
|
||||
free(data);
|
||||
}
|
||||
else {
|
||||
ef = file_new(EF_RP+sloti);
|
||||
data = (uint8_t *)calloc(1, 1 + 32 + cred.rpId.len);
|
||||
data[0] = 1;
|
||||
memcpy(data+1, rp_id_hash, 32);
|
||||
memcpy(data + 1 + 32, cred.rpId.data, cred.rpId.len);
|
||||
flash_write_data_to_file(ef, data, 1 + 32 + cred.rpId.len);
|
||||
free(data);
|
||||
}
|
||||
}
|
||||
credential_free(&cred);
|
||||
low_flash_available();
|
||||
return 0;
|
||||
}
|
||||
@@ -252,21 +298,21 @@ int credential_derive_hmac_key(const uint8_t *cred_id, size_t cred_id_len, uint8
|
||||
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA512);
|
||||
|
||||
mbedtls_md_hmac(md_info, outk, 32, (uint8_t *)"SLIP-0022", 9, outk);
|
||||
mbedtls_md_hmac(md_info, outk, 32, (uint8_t *)"\xf1\xd0\x02\x00", 4, outk);
|
||||
mbedtls_md_hmac(md_info, outk, 32, (uint8_t *)CRED_PROTO, 4, outk);
|
||||
mbedtls_md_hmac(md_info, outk, 32, (uint8_t *)"hmac-secret", 11, outk);
|
||||
mbedtls_md_hmac(md_info, outk, 32, cred_id, cred_id_len, outk);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int credential_derive_chacha_key(uint8_t *outk) {
|
||||
memset(outk, 0, 64);
|
||||
memset(outk, 0, 32);
|
||||
int r = 0;
|
||||
if ((r = load_keydev(outk)) != 0)
|
||||
return r;
|
||||
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA512);
|
||||
|
||||
mbedtls_md_hmac(md_info, outk, 32, (uint8_t *)"SLIP-0022", 9, outk);
|
||||
mbedtls_md_hmac(md_info, outk, 32, (uint8_t *)"\xf1\xd0\x02\x00", 4, outk);
|
||||
mbedtls_md_hmac(md_info, outk, 32, (uint8_t *)CRED_PROTO, 4, outk);
|
||||
mbedtls_md_hmac(md_info, outk, 32, (uint8_t *)"Encryption key", 14, outk);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ typedef struct Credential
|
||||
#define CRED_PROT_UV_OPTIONAL_WITH_LIST 0x02
|
||||
#define CRED_PROT_UV_REQUIRED 0x03
|
||||
|
||||
#define CRED_PROTO "\xf1\xd0\x02\x01"
|
||||
|
||||
extern int credential_verify(uint8_t *cred_id, size_t cred_id_len, const uint8_t *rp_id_hash);
|
||||
extern int credential_create(CborCharString *rpId, CborByteString *userId, CborCharString *userName, CborCharString *userDisplayName, CredOptions *opts, CredExtensions *extensions, bool use_sign_count, int alg, int curve, uint8_t *cred_id, size_t *cred_id_len);
|
||||
extern void credential_free(Credential *cred);
|
||||
|
||||
@@ -104,6 +104,36 @@ typedef struct {
|
||||
uint8_t sig[CTAP_MAX_EC_SIG_SIZE]; // Signature
|
||||
} CTAP_AUTHENTICATE_RESP;
|
||||
|
||||
// CTAP CBOR commands
|
||||
|
||||
#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_MGMT 0x0A
|
||||
#define CTAP_SELECTION 0x0B
|
||||
#define CTAP_CONFIG 0x0D
|
||||
|
||||
#define CTAP_CONFIG_AUT_ENABLE 0x03e43f56b34285e2
|
||||
#define CTAP_CONFIG_AUT_DISABLE 0x1831a40f04a25ed9
|
||||
|
||||
#define CTAP_VENDOR_CBOR (CTAPHID_VENDOR_FIRST + 1)
|
||||
|
||||
#define CTAP_VENDOR_BACKUP 0x01
|
||||
#define CTAP_VENDOR_MSE 0x02
|
||||
#define CTAP_VENDOR_UNLOCK 0x03
|
||||
|
||||
typedef struct mse {
|
||||
uint8_t Qpt[65];
|
||||
uint8_t key_enc[12 + 32];
|
||||
bool init;
|
||||
} mse_t;
|
||||
extern mse_t mse;
|
||||
|
||||
extern int mse_decrypt_ct(uint8_t *, size_t);
|
||||
|
||||
// Command status responses
|
||||
|
||||
#define CTAP_SW_NO_ERROR 0x9000 // SW_NO_ERROR
|
||||
@@ -112,6 +142,7 @@ typedef struct {
|
||||
#define CTAP_SW_COMMAND_NOT_ALLOWED 0x6986 // SW_COMMAND_NOT_ALLOWED
|
||||
#define CTAP_SW_INS_NOT_SUPPORTED 0x6D00 // SW_INS_NOT_SUPPORTED
|
||||
|
||||
#define CTAP2_OK 0x00
|
||||
#define CTAP2_ERR_CBOR_UNEXPECTED_TYPE 0x11
|
||||
#define CTAP2_ERR_INVALID_CBOR 0x12
|
||||
#define CTAP2_ERR_MISSING_PARAMETER 0x14
|
||||
|
||||
@@ -25,13 +25,7 @@
|
||||
|
||||
extern uint8_t *driver_prepare_response();
|
||||
extern void driver_exec_finished(size_t size_next);
|
||||
extern int cbor_process(const uint8_t *data, size_t len);
|
||||
extern int cbor_reset();
|
||||
extern int cbor_get_info();
|
||||
extern int cbor_make_credential(const uint8_t *data, size_t len);
|
||||
extern int cbor_client_pin(const uint8_t *data, size_t len);
|
||||
extern int cbor_get_assertion(const uint8_t *data, size_t len, bool next);
|
||||
extern int cbor_get_next_assertion(const uint8_t *data, size_t len);
|
||||
extern int cbor_process(uint8_t, const uint8_t *data, size_t len);
|
||||
extern const uint8_t aaguid[16];
|
||||
|
||||
extern const bool _btrue, _bfalse;
|
||||
|
||||
@@ -39,6 +39,9 @@ int fido_unload();
|
||||
|
||||
pinUvAuthToken_t paut = {0};
|
||||
|
||||
uint8_t keydev_dec[32];
|
||||
bool has_keydev_dec = false;
|
||||
|
||||
const uint8_t fido_aid[] = {
|
||||
8,
|
||||
0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01
|
||||
@@ -117,9 +120,12 @@ int x509_create_cert(mbedtls_ecdsa_context *ecdsa, uint8_t *buffer, size_t buffe
|
||||
}
|
||||
|
||||
int load_keydev(uint8_t *key) {
|
||||
if (!ef_keydev || file_get_size(ef_keydev) == 0)
|
||||
if (has_keydev_dec == false && !file_has_data(ef_keydev))
|
||||
return CCID_ERR_MEMORY_FATAL;
|
||||
memcpy(key, file_get_data(ef_keydev), file_get_size(ef_keydev));
|
||||
if (has_keydev_dec == true)
|
||||
memcpy(key, keydev_dec, sizeof(keydev_dec));
|
||||
else
|
||||
memcpy(key, file_get_data(ef_keydev), file_get_size(ef_keydev));
|
||||
//return mkek_decrypt(key, file_get_size(ef_keydev));
|
||||
return CCID_OK;
|
||||
}
|
||||
@@ -201,8 +207,9 @@ int derive_key(const uint8_t *app_id, bool new_key, uint8_t *key_handle, int cur
|
||||
|
||||
int scan_files(bool core1) {
|
||||
ef_keydev = search_by_fid(EF_KEY_DEV, NULL, SPECIFY_EF);
|
||||
ef_keydev_enc = search_by_fid(EF_KEY_DEV_ENC, NULL, SPECIFY_EF);
|
||||
if (ef_keydev) {
|
||||
if (!file_has_data(ef_keydev)) {
|
||||
if (!file_has_data(ef_keydev) && !file_has_data(ef_keydev_enc)) {
|
||||
printf("KEY DEVICE is empty. Generating SECP256R1 curve...");
|
||||
mbedtls_ecdsa_context ecdsa;
|
||||
mbedtls_ecdsa_init(&ecdsa);
|
||||
@@ -236,7 +243,7 @@ int scan_files(bool core1) {
|
||||
int ret = mbedtls_ecp_read_key(MBEDTLS_ECP_DP_SECP256R1, &key, file_get_data(ef_keydev), file_get_size(ef_keydev));
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
ret = mbedtls_ecp_mul(&key.grp, &key.Q, &key.d, &key.grp.G, random_gen, NULL);
|
||||
ret = mbedtls_ecp_mul(&key.grp, &key.Q, &key.d, &key.grp.G, core1 ? random_gen : random_gen_core0, NULL);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
ret = x509_create_cert(&key, cert, sizeof(cert), core1);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
file_t file_entries[] = {
|
||||
{.fid = 0x3f00, .parent = 0xff, .name = NULL, .type = FILE_TYPE_DF, .data = NULL, .ef_structure = 0, .acl = {0}}, // MF
|
||||
{.fid = EF_KEY_DEV, .parent = 0, .name = NULL, .type = FILE_TYPE_INTERNAL_EF | FILE_DATA_FLASH, .data = NULL, .ef_structure = FILE_EF_TRANSPARENT, .acl = {0xff}}, // Device Key
|
||||
{.fid = EF_KEY_DEV_ENC, .parent = 0, .name = NULL, .type = FILE_TYPE_INTERNAL_EF | FILE_DATA_FLASH, .data = NULL, .ef_structure = FILE_EF_TRANSPARENT, .acl = {0xff}}, // Device Key Enc
|
||||
{.fid = EF_EE_DEV, .parent = 0, .name = NULL, .type = FILE_TYPE_INTERNAL_EF | FILE_DATA_FLASH, .data = NULL, .ef_structure = FILE_EF_TRANSPARENT, .acl = {0xff}}, // End Entity Certificate Device
|
||||
{.fid = EF_COUNTER, .parent = 0, .name = NULL, .type = FILE_TYPE_INTERNAL_EF | FILE_DATA_FLASH, .data = NULL, .ef_structure = FILE_EF_TRANSPARENT, .acl = {0xff}}, // Global counter
|
||||
{.fid = EF_PIN, .parent = 0, .name = NULL, .type = FILE_TYPE_INTERNAL_EF | FILE_DATA_FLASH, .data = NULL, .ef_structure = FILE_EF_TRANSPARENT, .acl = {0xff}}, // PIN
|
||||
@@ -35,3 +36,4 @@ file_t *ef_certdev = NULL;
|
||||
file_t *ef_counter = NULL;
|
||||
file_t *ef_pin = NULL;
|
||||
file_t *ef_authtoken = NULL;
|
||||
file_t *ef_keydev_enc = NULL;
|
||||
|
||||
@@ -21,16 +21,19 @@
|
||||
#include "file.h"
|
||||
|
||||
#define EF_KEY_DEV 0xCC00
|
||||
#define EF_KEY_DEV_ENC 0xCC01
|
||||
#define EF_EE_DEV 0xCE00
|
||||
#define EF_COUNTER 0xC000
|
||||
#define EF_PIN 0x1080
|
||||
#define EF_AUTHTOKEN 0x1090
|
||||
#define EF_CRED 0xCF00 // Creds at 0xCF00 - 0xCFFF
|
||||
#define EF_RP 0xD000 // RPs at 0xD000 - 0xD0FF
|
||||
|
||||
extern file_t *ef_keydev;
|
||||
extern file_t *ef_certdev;
|
||||
extern file_t *ef_counter;
|
||||
extern file_t *ef_pin;
|
||||
extern file_t *ef_authtoken;
|
||||
extern file_t *ef_keydev_enc;
|
||||
|
||||
#endif //_FILES_H_
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#ifndef __VERSION_H_
|
||||
#define __VERSION_H_
|
||||
|
||||
#define PICO_FIDO_VERSION 0x0200
|
||||
#define PICO_FIDO_VERSION 0x0204
|
||||
|
||||
#define PICO_FIDO_VERSION_MAJOR ((PICO_FIDO_VERSION >> 8) & 0xff)
|
||||
#define PICO_FIDO_VERSION_MINOR (PICO_FIDO_VERSION & 0xff)
|
||||
|
||||
395
tests/conftest.py
Normal file
395
tests/conftest.py
Normal file
@@ -0,0 +1,395 @@
|
||||
from http import client
|
||||
from fido2.hid import CtapHidDevice
|
||||
from fido2.client import Fido2Client, WindowsClient, UserInteraction, ClientError, _Ctap1ClientBackend
|
||||
from fido2.attestation import FidoU2FAttestation
|
||||
from fido2.ctap2.pin import ClientPin
|
||||
from fido2.server import Fido2Server
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.webauthn import CollectedClientData, AttestedCredentialData
|
||||
from utils import *
|
||||
from fido2.cose import ES256
|
||||
import sys
|
||||
import pytest
|
||||
import os
|
||||
import struct
|
||||
|
||||
DEFAULT_PIN='12345678'
|
||||
|
||||
|
||||
class Packet(object):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def ToWireFormat(
|
||||
self,
|
||||
):
|
||||
return self.data
|
||||
|
||||
@staticmethod
|
||||
def FromWireFormat(pkt_size, data):
|
||||
return Packet(data)
|
||||
|
||||
class CliInteraction(UserInteraction):
|
||||
def prompt_up(self):
|
||||
print("\nTouch your authenticator device now...\n")
|
||||
|
||||
def request_pin(self, permissions, rd_id):
|
||||
return DEFAULT_PIN
|
||||
|
||||
def request_uv(self, permissions, rd_id):
|
||||
print("User Verification required.")
|
||||
return True
|
||||
|
||||
class DeviceSelectCredential:
|
||||
def __init__(self, number):
|
||||
pass
|
||||
|
||||
def __call__(self, status):
|
||||
pass
|
||||
|
||||
class Device():
|
||||
def __init__(self, origin="https://example.com", user_interaction=CliInteraction(),uv="discouraged",rp={"id": "example.com", "name": "Example RP"}, attestation="direct"):
|
||||
self.__user = None
|
||||
self.__set_client(origin=origin, user_interaction=user_interaction, uv=uv)
|
||||
self.__set_server(rp=rp, attestation=attestation)
|
||||
|
||||
|
||||
def __set_client(self, origin, user_interaction, uv):
|
||||
self.__uv = uv
|
||||
self.__dev = None
|
||||
self.__origin = origin
|
||||
self.__user_interaction = user_interaction
|
||||
|
||||
# Locate a device
|
||||
self.__dev = next(CtapHidDevice.list_devices(), None)
|
||||
self.dev = self.__dev
|
||||
if self.__dev is not None:
|
||||
print("Use USB HID channel.")
|
||||
else:
|
||||
try:
|
||||
from fido2.pcsc import CtapPcscDevice
|
||||
|
||||
self.__dev = next(CtapPcscDevice.list_devices(), None)
|
||||
print("Use NFC channel.")
|
||||
except Exception as e:
|
||||
print("NFC channel search error:", e)
|
||||
|
||||
if not self.__dev:
|
||||
print("No FIDO device found")
|
||||
sys.exit(1)
|
||||
|
||||
# Set up a FIDO 2 client using the origin https://example.com
|
||||
self.__client = Fido2Client(self.__dev, self.__origin, user_interaction=self.__user_interaction)
|
||||
|
||||
# Prefer UV if supported and configured
|
||||
if self.__client.info.options.get("uv") or self.__client.info.options.get("pinUvAuthToken"):
|
||||
self.__uv = "preferred"
|
||||
print("Authenticator supports User Verification")
|
||||
|
||||
self.__client1 = Fido2Client(self.__dev, self.__origin, user_interaction=self.__user_interaction)
|
||||
self.__client1._backend = _Ctap1ClientBackend(self.__dev, user_interaction=self.__user_interaction)
|
||||
self.ctap1 = self.__client1._backend.ctap1
|
||||
|
||||
def __set_server(self, rp, attestation):
|
||||
self.__rp = rp
|
||||
self.__attestation = attestation
|
||||
self.__server = Fido2Server(self.__rp, attestation=self.__attestation)
|
||||
|
||||
def client(self):
|
||||
return self.__client
|
||||
|
||||
def user(self, user=None):
|
||||
if (self.__user is None):
|
||||
self.__user = {"id": b"user_id", "name": "A. User"}
|
||||
if (user is not None):
|
||||
self.__user = user
|
||||
return self.__user
|
||||
|
||||
def rp(self, rp=None):
|
||||
if (self.__rp is None):
|
||||
self.__rp = {"id": "example.com", "name": "Example RP"}
|
||||
if (rp is not None):
|
||||
self.__rp = rp
|
||||
return self.__rp
|
||||
|
||||
def send_data(self, cmd, data, timeout = 1.0, on_keepalive = None):
|
||||
if not isinstance(data, bytes):
|
||||
data = struct.pack("%dB" % len(data), *[ord(x) for x in data])
|
||||
with Timeout(timeout) as event:
|
||||
event.is_set()
|
||||
return self.dev.call(cmd, data, event, on_keepalive = on_keepalive)
|
||||
|
||||
def cid(self):
|
||||
return self.dev._channel_id
|
||||
|
||||
def set_cid(self, cid):
|
||||
self.dev._channel_id = int.from_bytes(cid, 'big')
|
||||
|
||||
def recv_raw(self):
|
||||
with Timeout(1.0):
|
||||
r = self.dev._connection.read_packet()
|
||||
return r[4], r[7:]
|
||||
|
||||
def send_raw(self, data, cid=None):
|
||||
if cid is None:
|
||||
cid = self.dev._channel_id.to_bytes(4, 'big')
|
||||
elif not isinstance(cid, bytes):
|
||||
cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid])
|
||||
if not isinstance(data, bytes):
|
||||
data = struct.pack("%dB" % len(data), *[ord(x) for x in data])
|
||||
data = cid + data
|
||||
l = len(data)
|
||||
if l != 64:
|
||||
pad = "\x00" * (64 - l)
|
||||
pad = struct.pack("%dB" % len(pad), *[ord(x) for x in pad])
|
||||
data = data + pad
|
||||
data = bytes(data)
|
||||
assert len(data) == 64
|
||||
self.dev._connection.write_packet(data)
|
||||
|
||||
def reset(self):
|
||||
print("Resetting Authenticator...")
|
||||
try:
|
||||
self.__client._backend.ctap2.reset(on_keepalive=DeviceSelectCredential(1))
|
||||
except CtapError:
|
||||
# Some authenticators need a power cycle
|
||||
print("Need to power cycle authentictor to reset..")
|
||||
self.reboot()
|
||||
self.__client._backend.ctap2.reset(on_keepalive=DeviceSelectCredential(1))
|
||||
|
||||
def reboot(self):
|
||||
print("Please reboot authenticator and hit enter")
|
||||
input()
|
||||
self.__set_client(self.__origin, self.__user_interaction, self.__uv)
|
||||
self.__set_server(rp=self.__rp, attestation=self.__attestation)
|
||||
|
||||
def MC(self, client_data_hash=Ellipsis, rp=Ellipsis, user=Ellipsis, key_params=Ellipsis, exclude_list=None, extensions=None, options=None, pin_uv_param=None, pin_uv_protocol=None, enterprise_attestation=None):
|
||||
client_data_hash = client_data_hash if client_data_hash is not Ellipsis else os.urandom(32)
|
||||
rp = rp if rp is not Ellipsis else self.__rp
|
||||
user = user if user is not Ellipsis else self.user()
|
||||
key_params = key_params if key_params is not Ellipsis else self.__server.allowed_algorithms
|
||||
att_obj = self.__client._backend.ctap2.make_credential(
|
||||
client_data_hash=client_data_hash,
|
||||
rp=rp,
|
||||
user=user,
|
||||
key_params=key_params,
|
||||
exclude_list=exclude_list,
|
||||
extensions=extensions,
|
||||
options=options,
|
||||
pin_uv_param=pin_uv_param,
|
||||
pin_uv_protocol=pin_uv_protocol,
|
||||
enterprise_attestation=enterprise_attestation
|
||||
)
|
||||
return {'res':att_obj,'req':{'client_data_hash':client_data_hash,
|
||||
'rp':rp,
|
||||
'user':user,
|
||||
'key_params':key_params}}
|
||||
|
||||
def doMC(self, client_data=Ellipsis, rp=Ellipsis, user=Ellipsis, key_params=Ellipsis, exclude_list=None, extensions=None, rk=None, user_verification=None, enterprise_attestation=None, event=None, ctap1=False):
|
||||
client_data = client_data if client_data is not Ellipsis else CollectedClientData.create(
|
||||
type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32)
|
||||
)
|
||||
rp = rp if rp is not Ellipsis else self.__rp
|
||||
user = user if user is not Ellipsis else self.user()
|
||||
key_params = key_params if key_params is not Ellipsis else self.__server.allowed_algorithms
|
||||
if (ctap1 is True):
|
||||
client = self.__client1
|
||||
else:
|
||||
client = self.__client
|
||||
result = client._backend.do_make_credential(
|
||||
client_data=client_data,
|
||||
rp=rp,
|
||||
user=user,
|
||||
key_params=key_params,
|
||||
exclude_list=exclude_list,
|
||||
extensions=extensions,
|
||||
rk=rk,
|
||||
user_verification=user_verification,
|
||||
enterprise_attestation=enterprise_attestation,
|
||||
event=event
|
||||
)
|
||||
return {'res':result,'req':{'client_data':client_data,
|
||||
'rp':rp,
|
||||
'user':user,
|
||||
'key_params':key_params}}
|
||||
|
||||
def try_make_credential(self, options=None):
|
||||
if (options is None):
|
||||
options, _ = self.__server.register_begin(
|
||||
self.user(), user_verification=self.__uv, authenticator_attachment="cross-platform"
|
||||
)
|
||||
try:
|
||||
result = self.__client.make_credential(options["publicKey"])
|
||||
except ClientError as e:
|
||||
if (e.code == ClientError.ERR.CONFIGURATION_UNSUPPORTED):
|
||||
client_pin = ClientPin(self.__client._backend.ctap2)
|
||||
client_pin.set_pin(DEFAULT_PIN)
|
||||
result = self.__client.make_credential(options["publicKey"])
|
||||
return result
|
||||
|
||||
def register(self, uv=None):
|
||||
# Prepare parameters for makeCredential
|
||||
create_options, state = self.__server.register_begin(
|
||||
self.user(), user_verification=uv or self.__uv, authenticator_attachment="cross-platform"
|
||||
)
|
||||
# Create a credential
|
||||
result = self.try_make_credential(create_options)
|
||||
|
||||
# Complete registration
|
||||
auth_data = self.__server.register_complete(
|
||||
state, result.client_data, result.attestation_object
|
||||
)
|
||||
credentials = [auth_data.credential_data]
|
||||
|
||||
print("New credential created!")
|
||||
|
||||
print("CLIENT DATA:", result.client_data)
|
||||
print("ATTESTATION OBJECT:", result.attestation_object)
|
||||
print()
|
||||
print("CREDENTIAL DATA:", auth_data.credential_data)
|
||||
|
||||
return (result, auth_data)
|
||||
|
||||
def authenticate(self, credentials):
|
||||
# Prepare parameters for getAssertion
|
||||
request_options, state = self.__server.authenticate_begin(credentials, user_verification=self.__uv)
|
||||
|
||||
# Authenticate the credential
|
||||
result = self.__client.get_assertion(request_options["publicKey"])
|
||||
|
||||
# Only one cred in allowCredentials, only one response.
|
||||
result = result.get_response(0)
|
||||
|
||||
# Complete authenticator
|
||||
self.__server.authenticate_complete(
|
||||
state,
|
||||
credentials,
|
||||
result.credential_id,
|
||||
result.client_data,
|
||||
result.authenticator_data,
|
||||
result.signature,
|
||||
)
|
||||
|
||||
print("Credential authenticated!")
|
||||
|
||||
print("CLIENT DATA:", result.client_data)
|
||||
print()
|
||||
print("AUTH DATA:", result.authenticator_data)
|
||||
|
||||
def GA(self, rp_id=Ellipsis, client_data_hash=Ellipsis, allow_list=None, extensions=None, options=None, pin_uv_param=None, pin_uv_protocol=None):
|
||||
rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id']
|
||||
client_data_hash = client_data_hash if client_data_hash is not Ellipsis else os.urandom(32)
|
||||
att_obj = self.__client._backend.ctap2.get_assertion(
|
||||
rp_id=rp_id,
|
||||
client_data_hash=client_data_hash,
|
||||
allow_list=allow_list,
|
||||
extensions=extensions,
|
||||
options=options,
|
||||
pin_uv_param=pin_uv_param,
|
||||
pin_uv_protocol=pin_uv_protocol
|
||||
)
|
||||
return {'res':att_obj,'req':{'rp_id':rp_id,
|
||||
'client_data_hash':client_data_hash}}
|
||||
|
||||
def GNA(self):
|
||||
return self.__client._backend.ctap2.get_next_assertion()
|
||||
|
||||
def doGA(self, client_data=Ellipsis, rp_id=Ellipsis, allow_list=None, extensions=None, user_verification=None, event=None, ctap1=False, check_only=False):
|
||||
client_data = client_data if client_data is not Ellipsis else CollectedClientData.create(
|
||||
type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32)
|
||||
)
|
||||
rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id']
|
||||
if (ctap1 is True):
|
||||
client = self.__client1
|
||||
else:
|
||||
client = self.__client
|
||||
try:
|
||||
result = client._backend.do_get_assertion(
|
||||
client_data=client_data,
|
||||
rp_id=rp_id,
|
||||
allow_list=allow_list,
|
||||
extensions=extensions,
|
||||
user_verification=user_verification,
|
||||
event=event
|
||||
)
|
||||
except ClientError as e:
|
||||
if (e.code == ClientError.ERR.CONFIGURATION_UNSUPPORTED):
|
||||
client_pin = ClientPin(self.__client._backend.ctap2)
|
||||
client_pin.set_pin(DEFAULT_PIN)
|
||||
result = client._backend.do_get_assertion(
|
||||
client_data=client_data,
|
||||
rp_id=rp_id,
|
||||
allow_list=allow_list,
|
||||
extensions=extensions,
|
||||
user_verification=user_verification,
|
||||
event=event
|
||||
)
|
||||
else:
|
||||
raise
|
||||
return {'res':result,'req':{'client_data':client_data,
|
||||
'rp_id':rp_id}}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def device():
|
||||
dev = Device()
|
||||
return dev
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def info(device):
|
||||
return device.client()._backend.info
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def MCRes(device, *args):
|
||||
return device.doMC(*args)
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def resetdevice(device):
|
||||
device.reset()
|
||||
return device
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def GARes(device, MCRes, *args):
|
||||
res = device.doGA(allow_list=[
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
], *args)
|
||||
|
||||
assertions = res['res'].get_assertions()
|
||||
for a in assertions:
|
||||
verify(MCRes['res'].attestation_object, a, res['req']['client_data'].hash)
|
||||
return res
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def MCRes_DC(device, *args):
|
||||
return device.doMC(rk=True, *args)
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def GARes_DC(device, MCRes_DC, *args):
|
||||
res = device.GA(allow_list=[
|
||||
{"id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
], *args)
|
||||
verify(MCRes_DC['res'].attestation_object, res['res'], res['req']['client_data_hash'])
|
||||
|
||||
return res
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def RegRes(resetdevice, *args):
|
||||
res = resetdevice.doMC(ctap1=True, *args)
|
||||
att = FidoU2FAttestation()
|
||||
att.verify(res['res'].attestation_object.att_stmt, res['res'].attestation_object.auth_data, res['req']['client_data'].hash)
|
||||
return res
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def AuthRes(device, RegRes, *args):
|
||||
res = device.doGA(ctap1=True, allow_list=[
|
||||
{"id": RegRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
], *args)
|
||||
aut_data = res['res'].get_response(0)
|
||||
m = aut_data.authenticator_data.rp_id_hash + aut_data.authenticator_data.flags.to_bytes(1, 'big') + aut_data.authenticator_data.counter.to_bytes(4, 'big') + aut_data.client_data.hash
|
||||
ES256(RegRes['res'].attestation_object.auth_data.credential_data.public_key).verify(m, aut_data.signature)
|
||||
return aut_data
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def client_pin(resetdevice):
|
||||
return ClientPin(resetdevice.client()._backend.ctap2)
|
||||
194
tests/pico-fido/test_authenticate.py
Normal file
194
tests/pico-fido/test_authenticate.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from fido2.utils import sha256
|
||||
from fido2.client import CtapError
|
||||
import pytest
|
||||
|
||||
def test_authenticate(device):
|
||||
device.reset()
|
||||
REGRes,AUTData = device.register()
|
||||
|
||||
credentials = [AUTData.credential_data]
|
||||
AUTRes = device.authenticate(credentials)
|
||||
|
||||
def test_assertion_auth_data(GARes):
|
||||
assert len(GARes['res'].get_response(0).authenticator_data) == 37
|
||||
|
||||
def test_Check_that_AT_flag_is_not_set(GARes):
|
||||
assert (GARes['res'].get_response(0).authenticator_data.flags & 0xF8) == 0
|
||||
|
||||
def test_that_user_credential_and_numberOfCredentials_are_not_present(device, MCRes):
|
||||
res = device.GA(allow_list=[
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
])
|
||||
assert res['res'].user == None
|
||||
assert res['res'].number_of_credentials == None
|
||||
|
||||
def test_empty_allowList(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(allow_list=[])
|
||||
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
|
||||
|
||||
def test_get_assertion_allow_list_filtering_and_buffering(device):
|
||||
""" Check that authenticator filters and stores items in allow list correctly """
|
||||
allow_list = []
|
||||
|
||||
rp1 = {"id": "rp1.com", "name": "rp1.com"}
|
||||
rp2 = {"id": "rp2.com", "name": "rp2.com"}
|
||||
|
||||
rp1_registrations = []
|
||||
rp2_registrations = []
|
||||
rp1_assertions = []
|
||||
rp2_assertions = []
|
||||
|
||||
l1 = 4
|
||||
for i in range(0, l1):
|
||||
res = device.doMC(rp=rp1)['res'].attestation_object
|
||||
rp1_registrations.append(res)
|
||||
allow_list.append({
|
||||
"id": res.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
})
|
||||
|
||||
l2 = 6
|
||||
for i in range(0, l2):
|
||||
res = device.doMC(rp=rp2)['res'].attestation_object
|
||||
rp2_registrations.append(res)
|
||||
allow_list.append({
|
||||
"id": res.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
})
|
||||
|
||||
# CTAP 2.1: If allowlist is passed, only one (any) applicable
|
||||
# credential signs, and numberOfCredentials = None is returned.
|
||||
# <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#:~:text=If%20the%20allowList%20parameter%20is%20present%3A,Go%20to%20Step%2013>
|
||||
#
|
||||
# CTAP 2.0: Expects the authenticator to return the total number
|
||||
# even when allowlist is passed (and hence keep the credential IDs
|
||||
# cached.
|
||||
|
||||
# Should authenticate to all credentials matching rp1
|
||||
rp1_assertions = device.doGA(rp_id=rp1['id'], allow_list=allow_list)['res'].get_assertions()
|
||||
|
||||
# Should authenticate to all credentials matching rp2
|
||||
rp2_assertions = device.doGA(rp_id=rp2['id'], allow_list=allow_list)['res'].get_assertions()
|
||||
|
||||
counts = (
|
||||
len(rp1_assertions),
|
||||
len(rp2_assertions)
|
||||
)
|
||||
|
||||
assert counts in [(None, None), (l1, l2)]
|
||||
|
||||
def test_corrupt_credId(device, MCRes):
|
||||
# apply bit flip
|
||||
badid = list(MCRes['res'].attestation_object.auth_data.credential_data.credential_id[:])
|
||||
badid[len(badid) // 2] = badid[len(badid) // 2] ^ 1
|
||||
badid = bytes(badid)
|
||||
|
||||
allow_list = [{"id": badid, "type": "public-key"}]
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(allow_list=allow_list)['res']
|
||||
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
|
||||
|
||||
def test_mismatched_rp(device, GARes):
|
||||
rp_id = device.rp()['id']
|
||||
rp_id += ".com"
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(rp_id=rp_id)
|
||||
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
|
||||
|
||||
def test_missing_rp(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(rp_id=None)
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_bad_rp(device):
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(rp_id={"id": {"type": "wrong"}})
|
||||
|
||||
def test_missing_cdh(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.GA(client_data_hash=None)
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_bad_cdh(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.GA(client_data_hash={"type": "wrong"})
|
||||
|
||||
def test_bad_allow_list(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(allow_list={"type": "wrong"})
|
||||
|
||||
def test_bad_allow_list_item(device, MCRes):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(allow_list=["wrong"] + [
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
]
|
||||
)
|
||||
|
||||
def test_unknown_option(device, MCRes):
|
||||
device.GA(options={"unknown": True}, allow_list=[
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
])
|
||||
|
||||
def test_option_uv(device, info, GARes):
|
||||
if "uv" in info.options:
|
||||
if info.options["uv"]:
|
||||
res = device.doGA(options={"uv": True})['res']
|
||||
assert res.auth_data.flags & (1 << 2)
|
||||
|
||||
def test_option_up(device, info, GARes):
|
||||
if "up" in info.options:
|
||||
if info.options["up"]:
|
||||
res = device.doGA(options={"up": True})['res']
|
||||
assert res.auth_data.flags & (1 << 0)
|
||||
|
||||
def test_allow_list_fake_item(device, MCRes):
|
||||
device.doGA(allow_list=[{"type": "rot13", "id": b"1234"}]
|
||||
+ [
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
],
|
||||
)
|
||||
|
||||
def test_allow_list_missing_field(device, MCRes):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(allow_list=[{"id": b"1234"}] + [
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
]
|
||||
)
|
||||
|
||||
def test_allow_list_field_wrong_type(device, MCRes):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(allow_list=[{"type": b"public-key", "id": b"1234"}]
|
||||
+ [
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
]
|
||||
)
|
||||
|
||||
def test_allow_list_id_wrong_type(device, MCRes):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(allow_list=[{"type": "public-key", "id": 42}]
|
||||
+ [
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
]
|
||||
)
|
||||
|
||||
def test_allow_list_missing_id(device, MCRes):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doGA(allow_list=[{"type": "public-key"}] + [
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
]
|
||||
)
|
||||
|
||||
def test_user_presence_option_false(device, MCRes):
|
||||
res = device.GA(options={"up": False}, allow_list=[
|
||||
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
])
|
||||
|
||||
def test_credential_resets(device, MCRes, GARes):
|
||||
device.reset()
|
||||
with pytest.raises(CtapError) as e:
|
||||
new_auth = device.doGA()
|
||||
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
|
||||
358
tests/pico-fido/test_cred_mgmt.py
Normal file
358
tests/pico-fido/test_cred_mgmt.py
Normal file
@@ -0,0 +1,358 @@
|
||||
import pytest
|
||||
import time
|
||||
import random
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.ctap2 import CredentialManagement
|
||||
from fido2.utils import sha256, hmac_sha256
|
||||
from fido2.ctap2.pin import PinProtocolV2
|
||||
from binascii import hexlify
|
||||
from utils import generate_random_user
|
||||
|
||||
PIN = "12345678"
|
||||
|
||||
|
||||
@pytest.fixture(params=[PIN], scope = 'function')
|
||||
def PinToken(request, device, client_pin):
|
||||
#device.reboot()
|
||||
device.reset()
|
||||
pin = request.param
|
||||
client_pin.set_pin(pin)
|
||||
return client_pin.get_pin_token(pin)
|
||||
|
||||
|
||||
@pytest.fixture(scope = 'function')
|
||||
def MC_RK_Res(device, PinToken):
|
||||
rp = {"id": "ssh:", "name": "Bate Goiko"}
|
||||
device.doMC(rp=rp, rk=True)
|
||||
|
||||
rp = {"id": "xakcop.com", "name": "John Doe"}
|
||||
device.doMC(rp=rp, rk=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope = 'function')
|
||||
def CredMgmt(device, PinToken):
|
||||
pin_protocol = PinProtocolV2()
|
||||
return CredentialManagement(device.client()._backend.ctap2, pin_protocol, PinToken)
|
||||
|
||||
|
||||
def _test_enumeration(CredMgmt, rp_map):
|
||||
"Enumerate credentials using BFS"
|
||||
res = CredMgmt.enumerate_rps()
|
||||
assert len(rp_map.keys()) == len(res)
|
||||
|
||||
for rp in res:
|
||||
creds = CredMgmt.enumerate_creds(sha256(rp[3]["id"]))
|
||||
assert len(creds) == rp_map[rp[3]["id"].decode()]
|
||||
|
||||
|
||||
def _test_enumeration_interleaved(CredMgmt, rp_map):
|
||||
"Enumerate credentials using DFS"
|
||||
first_rp = CredMgmt.enumerate_rps_begin()
|
||||
assert len(rp_map.keys()) == first_rp[CredentialManagement.RESULT.TOTAL_RPS]
|
||||
|
||||
rk_count = 1
|
||||
first_rk = CredMgmt.enumerate_creds_begin(sha256(first_rp[3]["id"]))
|
||||
for i in range(1, first_rk[CredentialManagement.RESULT.TOTAL_CREDENTIALS]):
|
||||
c = CredMgmt.enumerate_creds_next()
|
||||
rk_count += 1
|
||||
|
||||
assert rk_count == rp_map[first_rp[3]["id"].decode()]
|
||||
|
||||
for i in range(1, first_rp[CredentialManagement.RESULT.TOTAL_RPS]):
|
||||
next_rp = CredMgmt.enumerate_rps_next()
|
||||
|
||||
rk_count = 1
|
||||
first_rk = CredMgmt.enumerate_creds_begin(
|
||||
sha256(next_rp[3]["id"])
|
||||
)
|
||||
for i in range(1, first_rk[CredentialManagement.RESULT.TOTAL_CREDENTIALS]):
|
||||
c = CredMgmt.enumerate_creds_next()
|
||||
rk_count += 1
|
||||
|
||||
assert rk_count == rp_map[next_rp[3]["id"].decode()]
|
||||
|
||||
|
||||
def CredMgmtWrongPinAuth(device, pin_token):
|
||||
pin_protocol = PinProtocolV2()
|
||||
wrong_pt = bytearray(pin_token)
|
||||
wrong_pt[0] = (wrong_pt[0] + 1) % 256
|
||||
return CredentialManagement(device.client()._backend.ctap2, pin_protocol, bytes(wrong_pt))
|
||||
|
||||
|
||||
def assert_cred_response_has_all_fields(cred_res):
|
||||
for i in (
|
||||
CredentialManagement.RESULT.USER,
|
||||
CredentialManagement.RESULT.CREDENTIAL_ID,
|
||||
CredentialManagement.RESULT.PUBLIC_KEY,
|
||||
CredentialManagement.RESULT.TOTAL_CREDENTIALS,
|
||||
CredentialManagement.RESULT.CRED_PROTECT,
|
||||
):
|
||||
assert i in cred_res
|
||||
|
||||
|
||||
def test_get_info(info):
|
||||
assert "credMgmt" in info.options
|
||||
assert info.options["credMgmt"] == True
|
||||
assert 0x7 in info
|
||||
assert info[0x7] > 1
|
||||
assert 0x8 in info
|
||||
assert info[0x8] > 1
|
||||
|
||||
def test_get_metadata(CredMgmt, MC_RK_Res):
|
||||
metadata = CredMgmt.get_metadata()
|
||||
assert metadata[CredentialManagement.RESULT.EXISTING_CRED_COUNT] == 2
|
||||
assert metadata[CredentialManagement.RESULT.MAX_REMAINING_COUNT] >= 48
|
||||
|
||||
def test_enumerate_rps(CredMgmt, MC_RK_Res):
|
||||
res = CredMgmt.enumerate_rps()
|
||||
assert len(res) == 2
|
||||
assert res[0][CredentialManagement.RESULT.RP]["id"] == b"ssh:"
|
||||
assert res[0][CredentialManagement.RESULT.RP_ID_HASH] == sha256(b"ssh:")
|
||||
# Solo doesn't store rpId with the exception of "ssh:"
|
||||
assert res[1][CredentialManagement.RESULT.RP]["id"] == b"xakcop.com"
|
||||
assert res[1][CredentialManagement.RESULT.RP_ID_HASH] == sha256(b"xakcop.com")
|
||||
|
||||
def test_enumarate_creds(CredMgmt, MC_RK_Res):
|
||||
res = CredMgmt.enumerate_creds(sha256(b"ssh:"))
|
||||
assert len(res) == 1
|
||||
assert_cred_response_has_all_fields(res[0])
|
||||
res = CredMgmt.enumerate_creds(sha256(b"xakcop.com"))
|
||||
assert len(res) == 1
|
||||
assert_cred_response_has_all_fields(res[0])
|
||||
res = CredMgmt.enumerate_creds(sha256(b"missing.com"))
|
||||
assert not res
|
||||
|
||||
def test_get_metadata_wrong_pinauth(device, MC_RK_Res, PinToken):
|
||||
cmd = lambda credMgmt: credMgmt.get_metadata()
|
||||
_test_wrong_pinauth(device, cmd, PinToken)
|
||||
|
||||
def test_rpbegin_wrong_pinauth(device, MC_RK_Res, PinToken):
|
||||
cmd = lambda credMgmt: credMgmt.enumerate_rps_begin()
|
||||
_test_wrong_pinauth(device, cmd, PinToken)
|
||||
|
||||
def test_rkbegin_wrong_pinauth(device, MC_RK_Res, PinToken):
|
||||
cmd = lambda credMgmt: credMgmt.enumerate_creds_begin(sha256(b"ssh:"))
|
||||
_test_wrong_pinauth(device, cmd, PinToken)
|
||||
|
||||
def test_rpnext_without_rpbegin(device, CredMgmt, MC_RK_Res):
|
||||
CredMgmt.enumerate_creds_begin(sha256(b"ssh:"))
|
||||
with pytest.raises(CtapError) as e:
|
||||
CredMgmt.enumerate_rps_next()
|
||||
assert e.value.code == CtapError.ERR.NOT_ALLOWED
|
||||
|
||||
def test_rknext_without_rkbegin(device, CredMgmt, MC_RK_Res):
|
||||
CredMgmt.enumerate_rps_begin()
|
||||
with pytest.raises(CtapError) as e:
|
||||
CredMgmt.enumerate_creds_next()
|
||||
assert e.value.code == CtapError.ERR.NOT_ALLOWED
|
||||
|
||||
def test_delete(device, PinToken, CredMgmt):
|
||||
|
||||
# create a new RK
|
||||
rp = {"id": "example_3.com", "name": "John Doe 2"}
|
||||
reg = device.doMC(rp=rp, rk=True)['res'].attestation_object
|
||||
|
||||
# make sure it works
|
||||
auth = device.doGA(rp_id=rp['id'])
|
||||
|
||||
# get the ID from enumeration
|
||||
creds = CredMgmt.enumerate_creds(reg.auth_data.rp_id_hash)
|
||||
for cred in creds:
|
||||
if cred[7]["id"] == reg.auth_data.credential_data.credential_id:
|
||||
break
|
||||
|
||||
# delete it
|
||||
cred = {"id": cred[7]["id"], "type": "public-key"}
|
||||
CredMgmt.delete_cred(cred)
|
||||
|
||||
# make sure it doesn't work
|
||||
with pytest.raises(CtapError) as e:
|
||||
auth = device.doGA(rp_id=rp['id'])
|
||||
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
|
||||
|
||||
def test_add_delete(device, PinToken, CredMgmt):
|
||||
""" Delete a credential in the 'middle' and ensure other credentials are not affected. """
|
||||
|
||||
rp = {"id": "example_4.com", "name": "John Doe 3"}
|
||||
regs = []
|
||||
|
||||
# create 3 new RK's
|
||||
for i in range(0, 3):
|
||||
reg = device.doMC(rp=rp, rk=True, user=generate_random_user())['res'].attestation_object
|
||||
regs.append(reg)
|
||||
|
||||
# Check they all enumerate
|
||||
res = CredMgmt.enumerate_creds(regs[1].auth_data.rp_id_hash)
|
||||
assert len(res) == 3
|
||||
|
||||
# delete the middle one
|
||||
creds = CredMgmt.enumerate_creds(reg.auth_data.rp_id_hash)
|
||||
for cred in creds:
|
||||
if cred[7]["id"] == regs[1].auth_data.credential_data.credential_id:
|
||||
break
|
||||
|
||||
assert cred[7]["id"] == regs[1].auth_data.credential_data.credential_id
|
||||
|
||||
cred = {"id": cred[7]["id"], "type": "public-key"}
|
||||
CredMgmt.delete_cred(cred)
|
||||
|
||||
# Check one less enumerates
|
||||
res = CredMgmt.enumerate_creds(regs[0].auth_data.rp_id_hash)
|
||||
assert len(res) == 2
|
||||
|
||||
def test_multiple_creds_per_multiple_rps(
|
||||
device, PinToken, CredMgmt, MC_RK_Res
|
||||
):
|
||||
res = CredMgmt.enumerate_rps()
|
||||
assert len(res) == 2
|
||||
|
||||
new_rps = [
|
||||
{"id": "new_example_1.com", "name": "Example-3-creds"},
|
||||
{"id": "new_example_2.com", "name": "Example-3-creds"},
|
||||
{"id": "new_example_3.com", "name": "Example-3-creds"},
|
||||
]
|
||||
|
||||
# create 3 new credentials per RP
|
||||
for rp in new_rps:
|
||||
for i in range(0, 3):
|
||||
reg = device.doMC(rp=rp, rk=True, user=generate_random_user())
|
||||
|
||||
res = CredMgmt.enumerate_rps()
|
||||
assert len(res) == 5
|
||||
|
||||
for rp in res:
|
||||
if rp[3]["id"][:12] == "new_example_":
|
||||
creds = CredMgmt.enumerate_creds(sha256(rp[3]["id"].encode("utf8")))
|
||||
assert len(creds) == 3
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"enumeration_test", [_test_enumeration, _test_enumeration_interleaved]
|
||||
)
|
||||
def test_multiple_enumeration(
|
||||
device, PinToken, MC_RK_Res, CredMgmt, enumeration_test
|
||||
):
|
||||
""" Test enumerate still works after different commands """
|
||||
|
||||
res = CredMgmt.enumerate_rps()
|
||||
|
||||
expected_enumeration = {"xakcop.com": 1, "ssh:": 1}
|
||||
|
||||
enumeration_test(CredMgmt, expected_enumeration)
|
||||
|
||||
new_rps = [
|
||||
{"id": "example-2.com", "name": "Example-2-creds", "count": 2},
|
||||
{"id": "example-1.com", "name": "Example-1-creds", "count": 1},
|
||||
{"id": "example-5.com", "name": "Example-5-creds", "count": 5},
|
||||
]
|
||||
|
||||
# create 3 new credentials per RP
|
||||
for rp in new_rps:
|
||||
for i in range(0, rp["count"]):
|
||||
reg = device.doMC(rp={"id": rp["id"], "name": rp["name"]}, rk=True, user=generate_random_user())
|
||||
|
||||
# Now expect creds from this RP
|
||||
expected_enumeration[rp["id"]] = rp["count"]
|
||||
|
||||
enumeration_test(CredMgmt, expected_enumeration)
|
||||
enumeration_test(CredMgmt, expected_enumeration)
|
||||
|
||||
metadata = CredMgmt.get_metadata()
|
||||
|
||||
enumeration_test(CredMgmt, expected_enumeration)
|
||||
enumeration_test(CredMgmt, expected_enumeration)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"enumeration_test", [_test_enumeration, _test_enumeration_interleaved]
|
||||
)
|
||||
def test_multiple_enumeration_with_deletions(
|
||||
device, PinToken, MC_RK_Res, CredMgmt, enumeration_test
|
||||
):
|
||||
""" Create each credential in random order. Test enumerate still works after randomly deleting each credential"""
|
||||
|
||||
res = CredMgmt.enumerate_rps()
|
||||
|
||||
expected_enumeration = {"xakcop.com": 1, "ssh:": 1}
|
||||
|
||||
enumeration_test(CredMgmt, expected_enumeration)
|
||||
|
||||
new_rps = [
|
||||
{"id": "example-1.com", "name": "Example-1-creds"},
|
||||
{"id": "example-2.com", "name": "Example-2-creds"},
|
||||
{"id": "example-2.com", "name": "Example-2-creds"},
|
||||
{"id": "example-3.com", "name": "Example-3-creds"},
|
||||
{"id": "example-3.com", "name": "Example-3-creds"},
|
||||
{"id": "example-3.com", "name": "Example-3-creds"},
|
||||
]
|
||||
random.shuffle(new_rps)
|
||||
|
||||
# create new credentials per RP in random order
|
||||
for rp in new_rps:
|
||||
device.doMC(rp=rp, rk=True, user=generate_random_user())
|
||||
|
||||
if rp["id"] not in expected_enumeration:
|
||||
expected_enumeration[rp["id"]] = 1
|
||||
else:
|
||||
expected_enumeration[rp["id"]] += 1
|
||||
|
||||
enumeration_test(CredMgmt, expected_enumeration)
|
||||
|
||||
total_creds = len(new_rps)
|
||||
|
||||
while total_creds != 0:
|
||||
rp = random.choice(list(expected_enumeration.keys()))
|
||||
|
||||
num = expected_enumeration[rp]
|
||||
|
||||
index = 0 if num == 1 else random.randint(0, num - 1)
|
||||
cred = CredMgmt.enumerate_creds(sha256(rp.encode("utf8")))[index]
|
||||
|
||||
# print('Delete %d index (%d total) cred of %s' % (index, expected_enumeration[rp], rp))
|
||||
CredMgmt.delete_cred({"id": cred[7]["id"], "type": "public-key"})
|
||||
|
||||
expected_enumeration[rp] -= 1
|
||||
if expected_enumeration[rp] == 0:
|
||||
del expected_enumeration[rp]
|
||||
|
||||
if len(list(expected_enumeration.keys())) == 0:
|
||||
break
|
||||
|
||||
enumeration_test(CredMgmt, expected_enumeration)
|
||||
|
||||
def _test_wrong_pinauth(device, cmd, PinToken):
|
||||
|
||||
credMgmt = CredMgmtWrongPinAuth(device, PinToken)
|
||||
|
||||
for i in range(2):
|
||||
with pytest.raises(CtapError) as e:
|
||||
cmd(credMgmt)
|
||||
assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
cmd(credMgmt)
|
||||
assert e.value.code == CtapError.ERR.PIN_AUTH_BLOCKED
|
||||
|
||||
#device.reboot()
|
||||
credMgmt = CredMgmtWrongPinAuth(device, PinToken)
|
||||
|
||||
for i in range(2):
|
||||
time.sleep(0.2)
|
||||
with pytest.raises(CtapError) as e:
|
||||
cmd(credMgmt)
|
||||
assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
cmd(credMgmt)
|
||||
assert e.value.code == CtapError.ERR.PIN_AUTH_BLOCKED
|
||||
|
||||
#device.reboot()
|
||||
credMgmt = CredMgmtWrongPinAuth(device, PinToken)
|
||||
|
||||
for i in range(1):
|
||||
time.sleep(0.2)
|
||||
with pytest.raises(CtapError) as e:
|
||||
cmd(credMgmt)
|
||||
assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
cmd(credMgmt)
|
||||
assert e.value.code == CtapError.ERR.PIN_BLOCKED
|
||||
152
tests/pico-fido/test_credprotect.py
Normal file
152
tests/pico-fido/test_credprotect.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import pytest
|
||||
from fido2.ctap2.extensions import CredProtectExtension
|
||||
from fido2.webauthn import UserVerificationRequirement
|
||||
from fido2.ctap import CtapError
|
||||
|
||||
class CredProtect:
|
||||
UserVerificationOptional = 1
|
||||
UserVerificationOptionalWithCredentialId = 2
|
||||
UserVerificationRequired = 3
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def MCCredProtectOptional(resetdevice):
|
||||
res = resetdevice.doMC(rk=True, extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.OPTIONAL})['res'].attestation_object
|
||||
return res
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def MCCredProtectOptionalList(resetdevice):
|
||||
res = resetdevice.doMC(rk=True, extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.OPTIONAL_WITH_LIST})['res'].attestation_object
|
||||
return res
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def MCCredProtectRequired(resetdevice):
|
||||
res = resetdevice.doMC(rk=True, extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.REQUIRED})['res'].attestation_object
|
||||
return res
|
||||
|
||||
|
||||
def test_credprotect_make_credential_1(MCCredProtectOptional):
|
||||
assert MCCredProtectOptional.auth_data.extensions
|
||||
assert "credProtect" in MCCredProtectOptional.auth_data.extensions
|
||||
assert MCCredProtectOptional.auth_data.extensions["credProtect"] == 1
|
||||
|
||||
def test_credprotect_make_credential_2(MCCredProtectOptionalList):
|
||||
assert MCCredProtectOptionalList.auth_data.extensions
|
||||
assert "credProtect" in MCCredProtectOptionalList.auth_data.extensions
|
||||
assert MCCredProtectOptionalList.auth_data.extensions["credProtect"] == 2
|
||||
|
||||
def test_credprotect_make_credential_3(MCCredProtectRequired):
|
||||
assert MCCredProtectRequired.auth_data.extensions
|
||||
assert "credProtect" in MCCredProtectRequired.auth_data.extensions
|
||||
assert MCCredProtectRequired.auth_data.extensions["credProtect"] == 3
|
||||
|
||||
def test_credprotect_optional_excluded(device, MCCredProtectOptional):
|
||||
""" CredProtectOptional Cred should be visible to be excluded with no UV """
|
||||
exclude_list = [
|
||||
{
|
||||
"id": MCCredProtectOptional.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(rk=True, extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.OPTIONAL}, exclude_list=exclude_list)
|
||||
|
||||
assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED
|
||||
|
||||
def test_credprotect_optional_list_excluded(device, MCCredProtectOptionalList):
|
||||
""" CredProtectOptionalList Cred should be visible to be excluded with no UV """
|
||||
exclude_list = [
|
||||
{
|
||||
"id": MCCredProtectOptionalList.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(rk=True, extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.OPTIONAL_WITH_LIST}, exclude_list=exclude_list)
|
||||
|
||||
assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED
|
||||
|
||||
def test_credprotect_required_not_excluded_with_no_uv(device, MCCredProtectRequired):
|
||||
""" CredProtectRequired Cred should NOT be visible to be excluded with no UV """
|
||||
exclude_list = [
|
||||
{
|
||||
"id": MCCredProtectRequired.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
}
|
||||
]
|
||||
|
||||
# works
|
||||
device.doMC(rk=True, extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.REQUIRED}, exclude_list=exclude_list)
|
||||
|
||||
def test_credprotect_optional_works_with_no_allowList_no_uv(device, MCCredProtectOptional):
|
||||
|
||||
# works
|
||||
res = device.doGA()['res'].get_assertions()[0]
|
||||
|
||||
# If there's only one credential, this is None
|
||||
assert res.number_of_credentials == None
|
||||
|
||||
def test_credprotect_optional_and_list_works_no_uv(device, MCCredProtectOptional, MCCredProtectOptionalList, MCCredProtectRequired):
|
||||
allow_list = [
|
||||
{
|
||||
"id": MCCredProtectOptional.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
},
|
||||
{
|
||||
"id": MCCredProtectOptionalList.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
},
|
||||
{
|
||||
"id": MCCredProtectRequired.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
},
|
||||
]
|
||||
# works
|
||||
res1 = device.doGA(allow_list=allow_list)['res'].get_assertions()[0]
|
||||
assert res1.number_of_credentials in (None, 2)
|
||||
|
||||
results = [res1]
|
||||
if res1.number_of_credentials == 2:
|
||||
res2 = device.GNA()['res']
|
||||
results.append(res2)
|
||||
|
||||
# the required credProtect is not returned.
|
||||
for res in results:
|
||||
assert res.credential["id"] != MCCredProtectRequired.auth_data.credential_data.credential_id[:]
|
||||
|
||||
def test_hmac_secret_and_credProtect_make_credential(resetdevice, MCCredProtectOptional
|
||||
):
|
||||
|
||||
res = resetdevice.doMC(extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.OPTIONAL, 'hmacCreateSecret': True})['res'].attestation_object
|
||||
|
||||
for ext in ["credProtect", "hmac-secret"]:
|
||||
assert res.auth_data.extensions
|
||||
assert ext in res.auth_data.extensions
|
||||
assert res.auth_data.extensions[ext] == True
|
||||
|
||||
|
||||
def test_credprotect_all_with_uv(device, MCCredProtectOptional, MCCredProtectOptionalList, MCCredProtectRequired, client_pin):
|
||||
allow_list = [
|
||||
{
|
||||
"id": MCCredProtectOptional.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
},
|
||||
{
|
||||
"id": MCCredProtectOptionalList.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
},
|
||||
{
|
||||
"id": MCCredProtectRequired.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
},
|
||||
]
|
||||
|
||||
pin = "12345678"
|
||||
|
||||
client_pin.set_pin(pin)
|
||||
|
||||
res1 = device.doGA(user_verification=UserVerificationRequirement.REQUIRED, allow_list=allow_list)['res'].get_assertions()[0]
|
||||
|
||||
assert res1.number_of_credentials in (None, 3)
|
||||
|
||||
23
tests/pico-fido/test_ctap1_interop.py
Normal file
23
tests/pico-fido/test_ctap1_interop.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Test U2F register works with FIDO2 auth
|
||||
def test_ctap1_register(RegRes):
|
||||
pass
|
||||
|
||||
def test_ctap1_authenticate(RegRes, AuthRes):
|
||||
pass
|
||||
|
||||
def test_authenticate_ctap1_through_ctap2(device, RegRes):
|
||||
res = device.doGA(ctap1=False, allow_list=[
|
||||
{"id": RegRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
])
|
||||
assert res['res'].get_response(0).credential_id == RegRes['res'].attestation_object.auth_data.credential_data.credential_id
|
||||
|
||||
|
||||
# Test FIDO2 register works with U2F auth
|
||||
def test_ctap1_authenticate_attestation(MCRes, device):
|
||||
key_handle = MCRes['res'].attestation_object.auth_data.credential_data.credential_id
|
||||
if len(key_handle) <= 255:
|
||||
res = device.doGA(ctap1=True, allow_list=[
|
||||
{"id": key_handle, "type": "public-key"}
|
||||
])
|
||||
else:
|
||||
print("ctap2 credId is longer than 255 bytes, cannot use with U2F.")
|
||||
239
tests/pico-fido/test_discoverable.py
Normal file
239
tests/pico-fido/test_discoverable.py
Normal file
@@ -0,0 +1,239 @@
|
||||
from fido2.client import CtapError
|
||||
import pytest
|
||||
import random
|
||||
from utils import *
|
||||
|
||||
@pytest.mark.parametrize("do_reboot", [False, True])
|
||||
def test_user_info_returned_when_using_allowlist(device, MCRes_DC, GARes_DC, do_reboot):
|
||||
assert "id" in GARes_DC['res'].user.keys()
|
||||
|
||||
allow_list = [
|
||||
{
|
||||
"id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
}
|
||||
]
|
||||
|
||||
if do_reboot:
|
||||
device.reboot()
|
||||
|
||||
ga_res = device.GA(allow_list=allow_list)['res']
|
||||
|
||||
assert MCRes_DC["req"]["user"]["id"] == ga_res.user["id"]
|
||||
|
||||
def test_with_allow_list_after_reset(device, MCRes_DC, GARes_DC):
|
||||
assert "id" in GARes_DC['res'].user.keys()
|
||||
|
||||
allow_list = [
|
||||
{
|
||||
"id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
}
|
||||
]
|
||||
|
||||
ga_res = device.GA(allow_list=allow_list)['res']
|
||||
|
||||
assert MCRes_DC["req"]["user"]["id"] == ga_res.user["id"]
|
||||
|
||||
device.reset()
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
ga_res = device.doGA(allow_list=allow_list)
|
||||
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
|
||||
|
||||
|
||||
|
||||
def test_resident_key(MCRes_DC, info):
|
||||
pass
|
||||
|
||||
def test_resident_key_auth(MCRes_DC, GARes_DC):
|
||||
pass
|
||||
|
||||
def test_user_info_returned(device, MCRes_DC, GARes_DC):
|
||||
assert "id" in GARes_DC['res'].user.keys()
|
||||
assert (
|
||||
MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id
|
||||
== GARes_DC['res'].credential["id"]
|
||||
)
|
||||
assert MCRes_DC["req"]["user"]["id"] == GARes_DC['res'].user["id"]
|
||||
if not GARes_DC['res'].number_of_credentials:
|
||||
assert "id" in GARes_DC['res'].user.keys() and len(GARes_DC['res'].user.keys()) == 1
|
||||
else:
|
||||
assert MCRes_DC["req"]["user"] == GARes_DC['res'].user
|
||||
|
||||
|
||||
def test_multiple_rk_nodisplay(device, MCRes_DC):
|
||||
auths = []
|
||||
regs = []
|
||||
# Use unique RP to not collide with other credentials
|
||||
rp = {"id": f"unique-{random.random()}.com", "name": "Example"}
|
||||
for i in range(0, 3):
|
||||
res = device.doMC(rp=rp, rk=True, user=generate_random_user())
|
||||
regs.append(res)
|
||||
# time.sleep(2)
|
||||
|
||||
res = device.doGA(rp_id=rp['id'])['res']
|
||||
auths = res.get_assertions()
|
||||
|
||||
assert len(regs) == 3
|
||||
assert len(regs) == len(auths)
|
||||
|
||||
for x in auths:
|
||||
for y in ("name", "displayName", "id"):
|
||||
if y not in x.user.keys():
|
||||
print("FAIL: %s was not in user: " % y, x.user)
|
||||
|
||||
|
||||
def test_rk_maximum_size_nodisplay(device):
|
||||
"""
|
||||
Check the lengths of the fields according to the FIDO2 spec
|
||||
https://github.com/solokeys/solo/issues/158#issue-426613303
|
||||
https://www.w3.org/TR/webauthn/#dom-publickeycredentialuserentity-displayname
|
||||
"""
|
||||
device.reset()
|
||||
user_max = generate_user_maximum()
|
||||
resMC = device.doMC(user=user_max, rk=True)
|
||||
resGA = device.doGA()['res']
|
||||
auths = resGA.get_assertions()
|
||||
|
||||
user_max_GA = auths[0]
|
||||
print(auths)
|
||||
for y in ("name", "displayName", "id"):
|
||||
if (y in user_max_GA):
|
||||
assert user_max_GA.user[y] == user_max[y]
|
||||
|
||||
|
||||
def test_rk_maximum_list_capacity_per_rp_nodisplay(info, device, MCRes_DC):
|
||||
"""
|
||||
Test maximum returned capacity of the RK for the given RP
|
||||
"""
|
||||
|
||||
# Try to determine from get_info, or default to 19.
|
||||
RK_CAPACITY_PER_RP = info.max_creds_in_list
|
||||
if not RK_CAPACITY_PER_RP:
|
||||
RK_CAPACITY_PER_RP = 19
|
||||
|
||||
users = []
|
||||
|
||||
def get_user():
|
||||
user = generate_user_maximum()
|
||||
users.append(user)
|
||||
return user
|
||||
|
||||
# Use unique RP to not collide with other credentials from other tests.
|
||||
rp = {"id": f"unique-{random.random()}.com", "name": "Example"}
|
||||
|
||||
# req = FidoRequest(MCRes_DC, options=None, user=get_user(), rp = rp)
|
||||
# res = device.sendGA(*req.toGA())
|
||||
current_credentials_count = 0
|
||||
|
||||
auths = []
|
||||
regs = [MCRes_DC]
|
||||
RK_to_generate = RK_CAPACITY_PER_RP - current_credentials_count
|
||||
for i in range(RK_to_generate):
|
||||
res = device.doMC(user=get_user(), rp=rp, rk=True)['res'].attestation_object
|
||||
regs.append(res)
|
||||
|
||||
res = device.GA(rp_id = rp['id'])['res']
|
||||
assert res.number_of_credentials == RK_CAPACITY_PER_RP
|
||||
|
||||
auths.append(res)
|
||||
for i in range(RK_CAPACITY_PER_RP - 1):
|
||||
auths.append(device.GNA())
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.GNA()
|
||||
|
||||
auths = auths[::-1][-RK_to_generate:]
|
||||
regs = regs[-RK_to_generate:]
|
||||
users = users[-RK_to_generate:]
|
||||
|
||||
assert len(auths) == len(users)
|
||||
|
||||
for x, u in zip(auths, users):
|
||||
for y in ("name", "displayName", "id"):
|
||||
assert y in x.user.keys()
|
||||
assert x.user[y] == u[y]
|
||||
|
||||
assert len(auths) == len(regs)
|
||||
|
||||
|
||||
def test_rk_with_allowlist_of_different_rp(resetdevice):
|
||||
"""
|
||||
Test that a rk credential is not found when using an allowList item for a different RP
|
||||
"""
|
||||
|
||||
rk_rp = {"id": "rk-cred.org", "name": "Example"}
|
||||
rk_res = resetdevice.doMC(rp = rk_rp, rk=True)['res'].attestation_object
|
||||
|
||||
server_rp = {"id": "server-cred.com", "name": "Example"}
|
||||
server_res = resetdevice.doMC(rp = server_rp, rk=True)['res'].attestation_object
|
||||
|
||||
allow_list_with_different_rp_cred = [
|
||||
{
|
||||
"id": server_res.auth_data.credential_data.credential_id[:],
|
||||
"type": "public-key",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
res = resetdevice.doGA(rp_id = rk_rp['id'], allow_list = allow_list_with_different_rp_cred)
|
||||
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
|
||||
|
||||
|
||||
def test_same_userId_overwrites_rk(resetdevice):
|
||||
"""
|
||||
A make credential request with a UserId & Rp that is the same as an existing one should overwrite.
|
||||
"""
|
||||
rp = {"id": "overwrite.org", "name": "Example"}
|
||||
user = generate_random_user()
|
||||
|
||||
mc_res1 = resetdevice.doMC(rp = rp, rk=True, user = user)
|
||||
|
||||
# Should overwrite the first credential.
|
||||
mc_res2 = resetdevice.doMC(rp = rp, rk=True, user = user)
|
||||
|
||||
ga_res = resetdevice.GA(rp_id=rp['id'])['res']
|
||||
|
||||
# If there's only one credential, this is None
|
||||
assert ga_res.number_of_credentials == None
|
||||
|
||||
|
||||
def test_larger_icon_than_128(device):
|
||||
"""
|
||||
Test it works if we give an icon value larger than 128 bytes
|
||||
"""
|
||||
rp = {"id": "overwrite.org", "name": "Example"}
|
||||
user = generate_random_user()
|
||||
user['icon'] = 'https://www.w3.org/TR/webauthn/?icon=' + ("A" * 128)
|
||||
|
||||
device.doMC(rp = rp, rk=True, user = user)
|
||||
|
||||
|
||||
def test_returned_credential(device):
|
||||
"""
|
||||
Test that when two rk credentials put in allow_list,
|
||||
only 1 will get returned.
|
||||
"""
|
||||
device.reset()
|
||||
|
||||
regs = []
|
||||
allow_list = []
|
||||
for i in range(0, 2):
|
||||
res = device.doMC(rk=True, user = {
|
||||
"id": b'123456' + bytes([i]), "name": f'Test User {i}', "displayName": f'Test User display {i}'
|
||||
})['res'].attestation_object
|
||||
regs.append(res)
|
||||
allow_list.append({"id": res.auth_data.credential_data.credential_id[:], "type": "public-key"})
|
||||
|
||||
|
||||
ga_res = device.GA(allow_list=allow_list,options={'up':False})['res']
|
||||
|
||||
# No other credentials should be returned
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.GNA()
|
||||
|
||||
# the returned credential should have user id in it
|
||||
print(ga_res)
|
||||
assert 'id' in ga_res.user and len(ga_res.user["id"]) > 0
|
||||
27
tests/pico-fido/test_getinfo.py
Normal file
27
tests/pico-fido/test_getinfo.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import pytest
|
||||
from fido2.client import CtapError
|
||||
|
||||
|
||||
def test_getinfo(device):
|
||||
pass
|
||||
|
||||
|
||||
def test_get_info_version(info):
|
||||
assert "FIDO_2_0" in info.versions
|
||||
|
||||
|
||||
def test_Check_pin_protocols_field(info):
|
||||
if len(info.pin_uv_protocols):
|
||||
assert sum(info.pin_uv_protocols) > 0
|
||||
|
||||
|
||||
def test_Check_options_field(info):
|
||||
for x in info.options:
|
||||
assert info.options[x] in [True, False]
|
||||
|
||||
|
||||
def test_Check_up_option(device, info):
|
||||
if "up" not in info.options or info.options["up"]:
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(options={"up": True})
|
||||
assert e.value.code == CtapError.ERR.INVALID_OPTION
|
||||
253
tests/pico-fido/test_hid.py
Normal file
253
tests/pico-fido/test_hid.py
Normal file
@@ -0,0 +1,253 @@
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from binascii import hexlify, unhexlify
|
||||
|
||||
import pytest
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.hid import CTAPHID
|
||||
from utils import Timeout
|
||||
|
||||
class TestHID(object):
|
||||
def test_long_ping(self, device):
|
||||
amt = 1000
|
||||
pingdata = os.urandom(amt)
|
||||
|
||||
t1 = time.time() * 1000
|
||||
r = device.send_data(CTAPHID.PING, pingdata)
|
||||
t2 = time.time() * 1000
|
||||
delt = t2 - t1
|
||||
|
||||
assert not (delt > 555 * (amt / 1000))
|
||||
|
||||
assert r == pingdata
|
||||
|
||||
def test_init(self, device, check_timeouts=False):
|
||||
if check_timeouts:
|
||||
with pytest.raises(socket.timeout):
|
||||
cmd, resp = self.recv_raw()
|
||||
|
||||
payload = b"\x11\x11\x11\x11\x11\x11\x11\x11"
|
||||
r = device.send_data(CTAPHID.INIT, payload)
|
||||
print(r)
|
||||
assert r[:8] == payload
|
||||
|
||||
def test_ping(self, device):
|
||||
|
||||
pingdata = os.urandom(100)
|
||||
r = device.send_data(CTAPHID.PING, pingdata)
|
||||
assert r == pingdata
|
||||
|
||||
def test_wink(self, device):
|
||||
r = device.send_data(CTAPHID.WINK, "")
|
||||
|
||||
def test_cbor_no_payload(self, device):
|
||||
payload = b"\x11\x11\x11\x11\x11\x11\x11\x11"
|
||||
r = device.send_data(CTAPHID.INIT, payload)
|
||||
capabilities = r[16]
|
||||
|
||||
if (capabilities ^ 0x04) != 0:
|
||||
print("Implements CBOR.")
|
||||
with pytest.raises(CtapError) as e:
|
||||
r = device.send_data(CTAPHID.CBOR, "")
|
||||
assert e.value.code == CtapError.ERR.INVALID_LENGTH
|
||||
else:
|
||||
print("CBOR is not implemented.")
|
||||
|
||||
def test_no_data_in_u2f_msg(self, device):
|
||||
payload = b"\x11\x11\x11\x11\x11\x11\x11\x11"
|
||||
r = device.send_data(CTAPHID.INIT, payload)
|
||||
capabilities = r[16]
|
||||
|
||||
if (capabilities ^ 0x08) == 0:
|
||||
print("U2F implemented.")
|
||||
with pytest.raises(CtapError) as e:
|
||||
r = device.send_data(CTAPHID.MSG, "")
|
||||
print(hexlify(r))
|
||||
assert e.value.code == CtapError.ERR.INVALID_LENGTH
|
||||
else:
|
||||
print("U2F not implemented.")
|
||||
|
||||
def test_invalid_hid_cmd(self, device):
|
||||
r = device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
r = device.send_data(0x66, "")
|
||||
assert e.value.code == CtapError.ERR.INVALID_COMMAND
|
||||
|
||||
def test_oversize_packet(self, device):
|
||||
device.send_raw("\x81\x1d\xba\x00")
|
||||
cmd, resp = device.recv_raw()
|
||||
assert resp[0] == CtapError.ERR.INVALID_LENGTH
|
||||
|
||||
def test_skip_sequence_number(self, device):
|
||||
r = device.send_data(CTAPHID.PING, "\x44" * 200)
|
||||
device.send_raw("\x81\x04\x90")
|
||||
device.send_raw("\x00")
|
||||
device.send_raw("\x01")
|
||||
# skip 2
|
||||
device.send_raw("\x03")
|
||||
cmd, resp = device.recv_raw()
|
||||
assert resp[0] == CtapError.ERR.INVALID_SEQ
|
||||
|
||||
def test_resync_and_ping(self, device):
|
||||
r = device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
pingdata = os.urandom(100)
|
||||
r = device.send_data(CTAPHID.PING, pingdata)
|
||||
if r != pingdata:
|
||||
raise ValueError("Ping data not echo'd")
|
||||
|
||||
def test_ping_abort(self, device):
|
||||
device.send_raw("\x81\x04\x00")
|
||||
device.send_raw("\x00")
|
||||
device.send_raw("\x01")
|
||||
device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
|
||||
def test_ping_abort_from_different_cid(self, device, check_timeouts=False):
|
||||
oldcid = device.dev._channel_id
|
||||
newcid = int.from_bytes(b"\x11\x22\x33\x44", 'big')
|
||||
device.send_raw("\x81\x10\x00")
|
||||
device.send_raw("\x00")
|
||||
device.send_raw("\x01")
|
||||
device.dev._channel_id = newcid
|
||||
device.send_raw(
|
||||
"\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88"
|
||||
) # init from different cid
|
||||
print("wait for init response")
|
||||
cmd, r = device.recv_raw() # init response
|
||||
assert cmd == 0x86
|
||||
device.dev._channel_id = oldcid
|
||||
if check_timeouts:
|
||||
# print('wait for timeout')
|
||||
cmd, r = device.recv_raw() # timeout response
|
||||
assert cmd == 0xBF
|
||||
|
||||
def test_timeout(self, device):
|
||||
device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
t1 = time.time() * 1000
|
||||
device.send_raw("\x81\x04\x00")
|
||||
device.send_raw("\x00")
|
||||
device.send_raw("\x01")
|
||||
cmd, r = device.recv_raw() # timeout response
|
||||
t2 = time.time() * 1000
|
||||
delt = t2 - t1
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.TIMEOUT
|
||||
assert delt < 1000 and delt > 400
|
||||
|
||||
def test_not_cont(self, device, check_timeouts=False):
|
||||
device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
device.send_raw("\x81\x04\x00")
|
||||
device.send_raw("\x00")
|
||||
device.send_raw("\x01")
|
||||
device.send_raw("\x81\x10\x00") # init packet
|
||||
cmd, r = device.recv_raw() # timeout response
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.INVALID_SEQ
|
||||
|
||||
if check_timeouts:
|
||||
device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
device.send_raw("\x01\x10\x00")
|
||||
with pytest.raises(socket.timeout):
|
||||
cmd, r = device.recv_raw() # timeout response
|
||||
|
||||
def test_check_busy(self, device):
|
||||
t1 = time.time() * 1000
|
||||
device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
oldcid = device.cid()
|
||||
newcid = b"\x11\x22\x33\x44"
|
||||
device.send_raw("\x81\x04\x00")
|
||||
device.set_cid(newcid)
|
||||
device.send_raw("\x81\x04\x00")
|
||||
cmd, r = device.recv_raw() # busy response
|
||||
t2 = time.time() * 1000
|
||||
assert t2 - t1 < 100
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.CHANNEL_BUSY
|
||||
|
||||
device.set_cid(oldcid)
|
||||
cmd, r = device.recv_raw() # timeout response
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.TIMEOUT
|
||||
|
||||
def test_check_busy_interleaved(self, device):
|
||||
cid1 = b"\x11\x22\x33\x44"
|
||||
cid2 = b"\x01\x22\x33\x44"
|
||||
device.set_cid(cid2)
|
||||
device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
device.set_cid(cid1)
|
||||
device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
||||
device.send_raw("\x81\x00\x63") # echo 99 bytes first channel
|
||||
|
||||
device.set_cid(cid2) # send ping on 2nd channel
|
||||
device.send_raw("\x81\x00\x39")
|
||||
time.sleep(0.1)
|
||||
device.send_raw("\x00")
|
||||
|
||||
cmd, r = device.recv_raw() # busy response
|
||||
|
||||
device.set_cid(cid1) # finish 1st channel ping
|
||||
device.send_raw("\x00")
|
||||
|
||||
device.set_cid(cid2)
|
||||
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.CHANNEL_BUSY
|
||||
|
||||
device.set_cid(cid1)
|
||||
cmd, r = device.recv_raw() # ping response
|
||||
assert cmd == 0x81
|
||||
assert len(r) == 0x39
|
||||
|
||||
def test_cid_0(self, device):
|
||||
device.set_cid("\x00\x00\x00\x00")
|
||||
device.send_raw(
|
||||
"\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\x00\x00\x00\x00"
|
||||
)
|
||||
cmd, r = device.recv_raw() # timeout
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.INVALID_CHANNEL
|
||||
device.set_cid("\x05\x04\x03\x02")
|
||||
|
||||
def test_cid_ffffffff(self, device):
|
||||
|
||||
device.set_cid("\xff\xff\xff\xff")
|
||||
device.send_raw(
|
||||
"\x81\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\xff\xff\xff\xff"
|
||||
)
|
||||
cmd, r = device.recv_raw() # timeout
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.INVALID_CHANNEL
|
||||
device.set_cid("\x05\x04\x03\x02")
|
||||
|
||||
def test_keep_alive(self, device, check_timeouts=False):
|
||||
|
||||
precanned_make_credential = unhexlify(
|
||||
'01a401582031323334353637383961626364656630313233343536373'\
|
||||
'8396162636465663002a26269646b6578616d706c652e6f7267646e61'\
|
||||
'6d65694578616d706c65525003a462696446cc2abaf119f26469636f6'\
|
||||
'e781f68747470733a2f2f7777772e77332e6f72672f54522f77656261'\
|
||||
'7574686e2f646e616d657256696e204f6c696d7069612047657272696'\
|
||||
'56b646973706c61794e616d65781c446973706c617965642056696e20'\
|
||||
'4f6c696d706961204765727269650481a263616c672664747970656a7'\
|
||||
'075626c69632d6b6579')
|
||||
|
||||
count = 0
|
||||
def count_keepalive(_x):
|
||||
nonlocal count
|
||||
count += 1
|
||||
|
||||
# We should get a keepalive within .5s
|
||||
try:
|
||||
r = device.send_data(CTAPHID.CBOR, precanned_make_credential, timeout = .50, on_keepalive = count_keepalive)
|
||||
except CtapError as e:
|
||||
assert e.code == CtapError.ERR.KEEPALIVE_CANCEL
|
||||
assert count > 0
|
||||
|
||||
# wait for authnr to get UP or timeout
|
||||
while True:
|
||||
try:
|
||||
r = device.send_data(CTAPHID.CBOR, '\x04') # getInfo
|
||||
break
|
||||
except CtapError as e:
|
||||
assert e.code == CtapError.ERR.CHANNEL_BUSY
|
||||
199
tests/pico-fido/test_hmac_secret.py
Normal file
199
tests/pico-fido/test_hmac_secret.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import pytest
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.ctap2.extensions import HmacSecretExtension
|
||||
from fido2.utils import hmac_sha256
|
||||
from fido2.ctap2.pin import PinProtocolV2
|
||||
from fido2.webauthn import UserVerificationRequirement
|
||||
from utils import *
|
||||
|
||||
salt1 = b"\xa5" * 32
|
||||
salt2 = b"\x96" * 32
|
||||
salt3 = b"\x03" * 32
|
||||
salt4 = b"\x5a" * 16
|
||||
salt5 = b"\x96" * 64
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def MCHmacSecret(resetdevice):
|
||||
res = resetdevice.doMC(extensions={"hmacCreateSecret": True},rk=True)
|
||||
return res['res'].attestation_object
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def hmac(resetdevice):
|
||||
return HmacSecretExtension(resetdevice.client()._backend.ctap2, pin_protocol=PinProtocolV2())
|
||||
|
||||
def test_hmac_secret_make_credential(MCHmacSecret):
|
||||
assert MCHmacSecret.auth_data.extensions
|
||||
assert "hmac-secret" in MCHmacSecret.auth_data.extensions
|
||||
assert MCHmacSecret.auth_data.extensions["hmac-secret"] == True
|
||||
|
||||
def test_hmac_secret_info(info):
|
||||
assert "hmac-secret" in info.extensions
|
||||
|
||||
def test_fake_extension(device):
|
||||
device.doMC(extensions={"tetris": True})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)])
|
||||
def test_hmac_secret_entropy(device, MCHmacSecret, hmac, salts
|
||||
):
|
||||
hout = {'salt1':salts[0]}
|
||||
if (len(salts) > 1):
|
||||
hout['salt2'] = salts[1]
|
||||
|
||||
auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0)
|
||||
ext = auth.extension_results
|
||||
assert ext
|
||||
assert "hmacGetSecret" in ext
|
||||
assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16
|
||||
|
||||
#print(shannon_entropy(auth.authenticator_data.extensions['hmac-secret']))
|
||||
if len(salts) == 1:
|
||||
assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 4.6
|
||||
assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.6
|
||||
if len(salts) == 2:
|
||||
assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 5.4
|
||||
assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.6
|
||||
assert shannon_entropy(ext["hmacGetSecret"]['output2']) > 4.6
|
||||
|
||||
def get_output(device, MCHmacSecret, hmac, salts):
|
||||
hout = {'salt1':salts[0]}
|
||||
if (len(salts) > 1):
|
||||
hout['salt2'] = salts[1]
|
||||
|
||||
auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0)
|
||||
|
||||
ext = auth.extension_results
|
||||
assert ext
|
||||
assert "hmacGetSecret" in ext
|
||||
assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16
|
||||
|
||||
if len(salts) == 2:
|
||||
return ext["hmacGetSecret"]['output1'], ext["hmacGetSecret"]['output2']
|
||||
else:
|
||||
return ext["hmacGetSecret"]['output1']
|
||||
|
||||
def test_hmac_secret_sanity(device, MCHmacSecret, hmac):
|
||||
output1 = get_output(device, MCHmacSecret, hmac, (salt1,))
|
||||
output12 = get_output(
|
||||
device, MCHmacSecret, hmac, (salt1, salt2)
|
||||
)
|
||||
output21 = get_output(
|
||||
device, MCHmacSecret, hmac, (salt2, salt1)
|
||||
)
|
||||
|
||||
assert output12[0] == output1
|
||||
assert output21[1] == output1
|
||||
assert output21[0] == output12[1]
|
||||
assert output12[0] != output12[1]
|
||||
|
||||
def test_missing_keyAgreement(device, hmac):
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
|
||||
|
||||
with pytest.raises(CtapError):
|
||||
device.GA(extensions={"hmac-secret": {2: hout[2], 3: hout[3]}})
|
||||
|
||||
def test_missing_saltAuth(device, hmac):
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2]}})
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_missing_saltEnc(device, hmac):
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.GA(extensions={"hmac-secret": {1: hout[1], 3: hout[3]}})
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_bad_auth(device, hmac, MCHmacSecret):
|
||||
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
|
||||
bad_auth = list(hout[3][:])
|
||||
bad_auth[len(bad_auth) // 2] = bad_auth[len(bad_auth) // 2] ^ 1
|
||||
bad_auth = bytes(bad_auth)
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2], 3: bad_auth, 4: 2}})
|
||||
assert e.value.code == CtapError.ERR.EXTENSION_FIRST
|
||||
|
||||
@pytest.mark.parametrize("salts", [(salt4,), (salt4, salt5)])
|
||||
def test_invalid_salt_length(device, hmac, salts):
|
||||
with pytest.raises(ValueError) as e:
|
||||
if (len(salts) == 2):
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
|
||||
else:
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
|
||||
|
||||
device.doGA(extensions={"hmacGetSecret": hout})
|
||||
|
||||
@pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)])
|
||||
def test_get_next_assertion_has_extension(
|
||||
device, hmac, salts
|
||||
):
|
||||
""" Check that get_next_assertion properly returns extension information for multiple accounts. """
|
||||
if (len(salts) == 2):
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
|
||||
else:
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
|
||||
accounts = 3
|
||||
regs = []
|
||||
auths = []
|
||||
rp = {"id": f"example_salts_{len(salts)}.org", "name": "ExampleRP_2"}
|
||||
fixed_users = [generate_random_user() for _ in range(accounts)]
|
||||
for i in range(accounts):
|
||||
res = device.doMC(extensions={"hmacCreateSecret": True},
|
||||
rk=True,
|
||||
rp=rp,
|
||||
user=fixed_users[i])['res'].attestation_object
|
||||
regs.append(res)
|
||||
|
||||
hout = {'salt1':salts[0]}
|
||||
if (len(salts) > 1):
|
||||
hout['salt2'] = salts[1]
|
||||
|
||||
ga = device.doGA(extensions={"hmacGetSecret": hout}, rp_id=rp['id'])
|
||||
auths = ga['res'].get_assertions()
|
||||
|
||||
for x in auths:
|
||||
assert x.auth_data.flags & (1 << 7) # has extension
|
||||
ext = x.auth_data.extensions
|
||||
assert ext
|
||||
assert "hmac-secret" in ext
|
||||
assert isinstance(ext["hmac-secret"], bytes)
|
||||
assert len(ext["hmac-secret"]) == len(salts) * 32 + 16
|
||||
key = hmac.process_get_output(x)
|
||||
|
||||
|
||||
|
||||
def test_hmac_secret_different_with_uv(device, MCHmacSecret, hmac):
|
||||
salts = [salt1]
|
||||
if (len(salts) == 2):
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
|
||||
else:
|
||||
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
|
||||
|
||||
auth_no_uv = device.GA(extensions={"hmac-secret": hout})['res']
|
||||
assert (auth_no_uv.auth_data.flags & (1 << 2)) == 0
|
||||
|
||||
ext_no_uv = auth_no_uv.auth_data.extensions
|
||||
assert ext_no_uv
|
||||
assert "hmac-secret" in ext_no_uv
|
||||
assert isinstance(ext_no_uv["hmac-secret"], bytes)
|
||||
assert len(ext_no_uv["hmac-secret"]) == len(salts) * 32 + 16
|
||||
|
||||
# Now get same auth with UV
|
||||
hout = {'salt1':salts[0]}
|
||||
if (len(salts) > 1):
|
||||
hout['salt2'] = salts[1]
|
||||
auth_uv = device.doGA(extensions={"hmacGetSecret": hout}, user_verification=UserVerificationRequirement.REQUIRED)['res'].get_response(0)
|
||||
|
||||
assert auth_uv.authenticator_data.flags & (1 << 2)
|
||||
ext_uv = auth_uv.extension_results
|
||||
assert ext_uv
|
||||
assert "hmacGetSecret" in ext_uv
|
||||
assert len(ext_uv["hmacGetSecret"]) == len(salts)
|
||||
|
||||
# Now see if the hmac-secrets are different
|
||||
assert ext_no_uv["hmac-secret"][:32] != ext_uv["hmacGetSecret"]['output1']
|
||||
241
tests/pico-fido/test_pin.py
Normal file
241
tests/pico-fido/test_pin.py
Normal file
@@ -0,0 +1,241 @@
|
||||
import os
|
||||
import pytest
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.client import ClientPin
|
||||
from fido2.webauthn import UserVerificationRequirement
|
||||
from fido2.utils import hmac_sha256
|
||||
|
||||
from utils import *
|
||||
|
||||
def test_lockout(device, resetdevice, client_pin):
|
||||
pin = "TestPin"
|
||||
client_pin.set_pin(pin)
|
||||
|
||||
pin_token = client_pin.get_pin_token(pin)
|
||||
|
||||
for i in range(1, 10):
|
||||
err = CtapError.ERR.PIN_INVALID
|
||||
if i in (3, 6):
|
||||
err = CtapError.ERR.PIN_AUTH_BLOCKED
|
||||
elif i >= 8:
|
||||
err = [CtapError.ERR.PIN_BLOCKED, CtapError.ERR.PIN_INVALID]
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
client_pin.get_pin_token("WrongPin")
|
||||
assert e.value.code == err or e.value.code in err
|
||||
|
||||
attempts = 8 - i
|
||||
if i > 8:
|
||||
attempts = 0
|
||||
|
||||
res = client_pin.get_pin_retries()
|
||||
assert res[0] == attempts
|
||||
|
||||
if err == CtapError.ERR.PIN_AUTH_BLOCKED:
|
||||
device.reboot()
|
||||
client_pin = ClientPin(resetdevice.client()._backend.ctap2)
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC()
|
||||
|
||||
device.reboot()
|
||||
client_pin = ClientPin(resetdevice.client()._backend.ctap2)
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
client_pin.get_pin_token(pin)
|
||||
assert e.value.code == CtapError.ERR.PIN_BLOCKED
|
||||
|
||||
def test_send_zero_length_pin_auth(device):
|
||||
device.reset()
|
||||
with pytest.raises(CtapError) as e:
|
||||
reg = device.MC(pin_uv_param=b"")
|
||||
assert e.value.code == CtapError.ERR.PIN_NOT_SET
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
reg = device.GA(pin_uv_param=b"")
|
||||
assert e.value.code in (CtapError.ERR.PIN_NOT_SET, CtapError.ERR.NO_CREDENTIALS)
|
||||
|
||||
def test_set_pin(device, client_pin):
|
||||
device.reset()
|
||||
client_pin.set_pin("TestPin")
|
||||
device.reset()
|
||||
|
||||
def test_set_pin_too_big(client_pin):
|
||||
with pytest.raises(CtapError) as e:
|
||||
client_pin.set_pin("A" * 64)
|
||||
assert e.value.code == CtapError.ERR.PIN_POLICY_VIOLATION
|
||||
|
||||
def test_get_pin_token_but_no_pin_set(resetdevice, client_pin):
|
||||
with pytest.raises(CtapError) as e:
|
||||
client_pin.get_pin_token("TestPin")
|
||||
assert e.value.code == CtapError.ERR.PIN_NOT_SET
|
||||
|
||||
def test_change_pin_but_no_pin_set(device, client_pin):
|
||||
with pytest.raises(CtapError) as e:
|
||||
client_pin.change_pin("TestPin", "1234")
|
||||
assert e.value.code == CtapError.ERR.PIN_NOT_SET
|
||||
|
||||
def test_setting_pin_and_get_info(device, client_pin, info):
|
||||
device.reset()
|
||||
client_pin.set_pin("TestPin")
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
client_pin.set_pin("TestPin")
|
||||
|
||||
assert info.options["clientPin"]
|
||||
|
||||
pin_token = client_pin.get_pin_token("TestPin")
|
||||
|
||||
res = client_pin.get_pin_retries()
|
||||
assert res[0] == 8
|
||||
|
||||
device.reset()
|
||||
|
||||
PIN1 = "12345678"
|
||||
PIN2 = "ABCDEF"
|
||||
|
||||
@pytest.fixture(scope="class", params=[PIN1])
|
||||
def SetPinRes(request, device, client_pin):
|
||||
device.reset()
|
||||
|
||||
pin = request.param
|
||||
|
||||
client_pin.set_pin(pin)
|
||||
|
||||
res = device.doMC(user_verification=UserVerificationRequirement.REQUIRED)
|
||||
return res
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def CPRes(request, device, client_pin):
|
||||
res = client_pin.ctap.client_pin(2, ClientPin.CMD.GET_KEY_AGREEMENT)
|
||||
return res
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def MCPinRes(device):
|
||||
res = device.doMC(user_verification=UserVerificationRequirement.REQUIRED)
|
||||
return res
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def GAPinRes(device, MCPinRes):
|
||||
res = device.doGA()
|
||||
return res
|
||||
|
||||
def test_pin(CPRes):
|
||||
pass
|
||||
|
||||
def test_set_pin_twice(device, client_pin):
|
||||
""" Setting pin when a pin is already set should result in error NotAllowed. """
|
||||
with pytest.raises(CtapError) as e:
|
||||
client_pin.set_pin('1234')
|
||||
client_pin.set_pin('1234')
|
||||
|
||||
assert e.value.code == CtapError.ERR.NOT_ALLOWED
|
||||
|
||||
|
||||
def test_get_key_agreement_fields(CPRes):
|
||||
key = CPRes[1]
|
||||
assert "Is public key" and key[1] == 2
|
||||
assert "Is P256" and key[-1] == 1
|
||||
assert "Is ALG_ECDH_ES_HKDF_256" and key[3] == -25
|
||||
|
||||
assert "Right key" and len(key[-3]) == 32 and isinstance(key[-3], bytes)
|
||||
|
||||
def test_verify_flag(device, SetPinRes):
|
||||
reg = device.doMC(user_verification=UserVerificationRequirement.REQUIRED)['res'].attestation_object
|
||||
assert reg.auth_data.flags & (1 << 2)
|
||||
|
||||
def test_get_no_pin_auth(device):
|
||||
|
||||
reg = device.doMC()['res'].attestation_object
|
||||
allow_list = [
|
||||
{"type": "public-key", "id": reg.auth_data.credential_data.credential_id}
|
||||
]
|
||||
auth = device.GA(allow_list=allow_list)['res']
|
||||
assert not (auth.auth_data.flags & (1 << 2))
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
reg = device.MC()
|
||||
|
||||
assert e.value.code == CtapError.ERR.PUAT_REQUIRED
|
||||
|
||||
def test_zero_length_pin_auth(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
reg = device.MC(pin_uv_param=b"")
|
||||
assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID
|
||||
|
||||
with pytest.raises(CtapError) as e:
|
||||
reg = device.GA(pin_uv_param=b"")
|
||||
assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID
|
||||
|
||||
def test_make_credential_no_pin(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
reg = device.MC()
|
||||
assert e.value.code == CtapError.ERR.PUAT_REQUIRED
|
||||
|
||||
def test_get_assertion_no_pin(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
reg = device.GA()
|
||||
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
|
||||
|
||||
def test_change_pin(device, client_pin):
|
||||
device.reset()
|
||||
client_pin.set_pin(PIN1)
|
||||
client_pin.change_pin(PIN1, PIN2)
|
||||
|
||||
pin_token = client_pin.get_pin_token(pin=PIN2)
|
||||
cdh = os.urandom(32)
|
||||
pin_auth = hmac_sha256(pin_token, cdh)[:32]
|
||||
|
||||
reg = device.MC(pin_uv_param=pin_auth, pin_uv_protocol=2, client_data_hash=cdh)['res']
|
||||
|
||||
pin_token = client_pin.get_pin_token(pin=PIN2)
|
||||
pin_auth = hmac_sha256(pin_token, cdh)[:32]
|
||||
auth = device.GA(pin_uv_param=pin_auth, pin_uv_protocol=2, client_data_hash=cdh, allow_list=[{
|
||||
"type": "public-key",
|
||||
"id": reg.auth_data.credential_data.credential_id,
|
||||
}])['res']
|
||||
|
||||
assert reg.auth_data.flags & (1 << 2)
|
||||
assert auth.auth_data.flags & (1 << 2)
|
||||
|
||||
verify(reg, auth, client_data_hash=cdh)
|
||||
|
||||
def test_pin_attempts(device, client_pin):
|
||||
# Flip 1 bit
|
||||
pin = PIN1
|
||||
device.reset()
|
||||
client_pin.set_pin(pin)
|
||||
pin_wrong = list(pin)
|
||||
c = pin[len(pin) // 2]
|
||||
|
||||
pin_wrong[len(pin) // 2] = chr(ord(c) ^ 1)
|
||||
pin_wrong = "".join(pin_wrong)
|
||||
|
||||
for i in range(1, 3):
|
||||
with pytest.raises(CtapError) as e:
|
||||
pin_token = client_pin.get_pin_token(pin=pin_wrong)
|
||||
assert e.value.code == CtapError.ERR.PIN_INVALID
|
||||
|
||||
print("Check there is %d pin attempts left" % (8 - i))
|
||||
res = client_pin.get_pin_retries()
|
||||
assert res[0] == (8 - i)
|
||||
|
||||
for i in range(1, 3):
|
||||
with pytest.raises(CtapError) as e:
|
||||
client_pin.get_pin_token(pin_wrong)
|
||||
assert e.value.code == CtapError.ERR.PIN_AUTH_BLOCKED
|
||||
|
||||
device.reboot()
|
||||
client_pin = ClientPin(device.client()._backend.ctap2)
|
||||
|
||||
pin_token = client_pin.get_pin_token(pin=pin)
|
||||
cdh = os.urandom(32)
|
||||
pin_auth = hmac_sha256(pin_token, cdh)[:32]
|
||||
|
||||
reg = device.MC(pin_uv_param=pin_auth, pin_uv_protocol=2, client_data_hash=cdh)['res']
|
||||
|
||||
res = client_pin.get_pin_retries()
|
||||
assert res[0] == (8)
|
||||
164
tests/pico-fido/test_register.py
Normal file
164
tests/pico-fido/test_register.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from fido2.client import CtapError
|
||||
from fido2.cose import ES256
|
||||
import pytest
|
||||
|
||||
|
||||
def test_register(device):
|
||||
device.reset()
|
||||
REGRes,AUTData = device.register()
|
||||
|
||||
def test_make_credential():
|
||||
pass
|
||||
|
||||
def test_attestation_format(MCRes):
|
||||
assert MCRes['res'].attestation_object.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"]
|
||||
|
||||
def test_authdata_length(MCRes):
|
||||
assert len(MCRes['res'].attestation_object.auth_data) >= 77
|
||||
|
||||
def test_missing_cdh(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(client_data_hash=None)
|
||||
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_bad_type_cdh(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(client_data_hash=b'\xff')
|
||||
|
||||
def test_missing_user(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(user=None)
|
||||
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_bad_type_user_user(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(user=b"12345678")
|
||||
|
||||
def test_missing_rp(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(rp=None)
|
||||
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_bad_type_rp(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(rp=b"12345678")
|
||||
|
||||
def test_missing_pubKeyCredParams(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(key_params=None)
|
||||
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_bad_type_pubKeyCredParams(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(key_params=b"12345678")
|
||||
|
||||
def test_bad_type_excludeList(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(exclude_list=8)
|
||||
|
||||
def test_bad_type_extensions(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(extensions=8)
|
||||
|
||||
def test_bad_type_options(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.MC(options=8)
|
||||
|
||||
def test_bad_type_rp_name(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(rp={"id": "test.org", "name": 8, "icon": "icon"})
|
||||
|
||||
def test_bad_type_rp_id(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(rp={"id": 8, "name": "name", "icon": "icon"})
|
||||
|
||||
def test_bad_type_rp_icon(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(rp={"id": "test.org", "name": "name", "icon": 8})
|
||||
|
||||
def test_bad_type_user_name(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(user={"id": b"user_id", "name": 8})
|
||||
|
||||
def test_bad_type_user_id(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(user={"id": "user_id", "name": "name"})
|
||||
|
||||
def test_bad_type_user_displayName(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(user={"id": "user_id", "name": "name", "displayName": 8})
|
||||
|
||||
def test_bad_type_user_icon(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(user={"id": "user_id", "name": "name", "icon": 8})
|
||||
|
||||
def test_bad_type_pubKeyCredParams(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(key_params=["wrong"])
|
||||
|
||||
def test_missing_pubKeyCredParams_type(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(key_params=[{"alg": ES256.ALGORITHM}])
|
||||
|
||||
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
|
||||
|
||||
def test_missing_pubKeyCredParams_alg(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(key_params=[{"type": "public-key"}])
|
||||
|
||||
assert e.value.code in [
|
||||
CtapError.ERR.MISSING_PARAMETER,
|
||||
CtapError.ERR.UNSUPPORTED_ALGORITHM,
|
||||
]
|
||||
|
||||
def test_bad_type_pubKeyCredParams_alg(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(key_params=[{"alg": "7", "type": "public-key"}])
|
||||
|
||||
def test_unsupported_algorithm(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(key_params=[{"alg": 1337, "type": "public-key"}])
|
||||
|
||||
assert e.value.code == CtapError.ERR.UNSUPPORTED_ALGORITHM
|
||||
|
||||
def test_exclude_list(resetdevice):
|
||||
resetdevice.doMC(exclude_list=[{"id": b"1234", "type": "rot13"}])
|
||||
|
||||
def test_exclude_list2(resetdevice):
|
||||
resetdevice.doMC(exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}])
|
||||
|
||||
def test_bad_type_exclude_list(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(exclude_list=["1234"])
|
||||
|
||||
def test_missing_exclude_list_type(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(exclude_list=[{"id": b"1234"}])
|
||||
|
||||
def test_missing_exclude_list_id(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(exclude_list=[{"type": "public-key"}])
|
||||
|
||||
def test_bad_type_exclude_list_id(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(exclude_list=[{"type": "public-key", "id": "1234"}])
|
||||
|
||||
def test_bad_type_exclude_list_type(device):
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(exclude_list=[{"type": b"public-key", "id": b"1234"}])
|
||||
|
||||
def test_exclude_list_excluded(device):
|
||||
res = device.doMC()['res'].attestation_object
|
||||
with pytest.raises(CtapError) as e:
|
||||
device.doMC(exclude_list=[
|
||||
{"id": res.auth_data.credential_data.credential_id, "type": "public-key"}
|
||||
])
|
||||
|
||||
assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED
|
||||
|
||||
def test_unknown_option(resetdevice):
|
||||
resetdevice.MC(options={"unknown": False})
|
||||
110
tests/pico-fido/test_u2f.py
Normal file
110
tests/pico-fido/test_u2f.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import pytest
|
||||
import os
|
||||
from fido2.ctap1 import APDU, ApduError, Ctap1
|
||||
from fido2.webauthn import CollectedClientData
|
||||
from fido2.utils import sha256
|
||||
|
||||
def test_u2f_reg(RegRes):
|
||||
pass
|
||||
|
||||
def test_u2f_auth(RegRes, AuthRes):
|
||||
pass
|
||||
|
||||
def test_u2f_auth_check_only(device, RegRes):
|
||||
with pytest.raises(ApduError) as e:
|
||||
device.ctap1.authenticate(
|
||||
RegRes['req']['client_data'].hash,
|
||||
RegRes['res'].attestation_object.auth_data.rp_id_hash,
|
||||
RegRes['res'].attestation_object.auth_data.credential_data.credential_id,
|
||||
check_only=True,
|
||||
)
|
||||
assert e.value.code == APDU.USE_NOT_SATISFIED
|
||||
|
||||
def test_version(device):
|
||||
assert device.ctap1.get_version() == "U2F_V2"
|
||||
|
||||
def test_bad_ins(device):
|
||||
with pytest.raises(ApduError) as e:
|
||||
device.ctap1.send_apdu(0, 0, 0, 0, b"")
|
||||
assert e.value.code == 0x6D00
|
||||
|
||||
def test_bad_cla(device):
|
||||
with pytest.raises(ApduError) as e:
|
||||
device.ctap1.send_apdu(1, Ctap1.INS.VERSION, 0, 0, b"abc")
|
||||
assert e.value.code == 0x6E00
|
||||
|
||||
@pytest.mark.parametrize("iterations", (5,))
|
||||
def test_u2f_it(device, iterations):
|
||||
lastc = 0
|
||||
|
||||
regs = []
|
||||
|
||||
cd = CollectedClientData.create(
|
||||
type=CollectedClientData.TYPE.CREATE, origin=None, challenge=os.urandom(32)
|
||||
)
|
||||
cdh = cd.hash
|
||||
rih = sha256(device.rp()['id'].encode())
|
||||
|
||||
for i in range(0, iterations):
|
||||
reg = device.ctap1.register(cdh, rih)
|
||||
auth = device.ctap1.authenticate(cdh, rih, reg.key_handle)
|
||||
auth.verify(rih, cdh, reg.public_key)
|
||||
|
||||
regs.append(reg)
|
||||
# check endianness
|
||||
if lastc:
|
||||
assert (auth.counter - lastc) < 256
|
||||
lastc = auth.counter
|
||||
if lastc > 0x80000000:
|
||||
print("WARNING: counter is unusually high: %04x" % lastc)
|
||||
assert 0
|
||||
|
||||
for reg in regs:
|
||||
auth = device.ctap1.authenticate(cdh, rih, reg.key_handle)
|
||||
|
||||
device.reboot()
|
||||
|
||||
for reg in regs:
|
||||
auth = device.ctap1.authenticate(cdh, rih, reg.key_handle)
|
||||
|
||||
for reg in regs:
|
||||
with pytest.raises(ApduError) as e:
|
||||
auth = device.ctap1.authenticate(
|
||||
cdh, rih, reg.key_handle, check_only=True
|
||||
)
|
||||
assert e.value.code == APDU.USE_NOT_SATISFIED
|
||||
|
||||
def test_bad_key_handle(device, RegRes):
|
||||
kh = bytearray(RegRes['res'].attestation_object.auth_data.credential_data.credential_id)
|
||||
kh[0] = kh[0] ^ (0x40)
|
||||
|
||||
with pytest.raises(ApduError) as e:
|
||||
device.ctap1.authenticate(
|
||||
RegRes['res'].client_data.hash, RegRes['res'].attestation_object.auth_data.rp_id_hash, kh, check_only=True
|
||||
)
|
||||
assert e.value.code == APDU.WRONG_DATA
|
||||
|
||||
with pytest.raises(ApduError) as e:
|
||||
device.ctap1.authenticate(
|
||||
RegRes['res'].client_data.hash, RegRes['res'].attestation_object.auth_data.rp_id_hash, kh
|
||||
)
|
||||
assert e.value.code == APDU.WRONG_DATA
|
||||
|
||||
def test_bad_key_handle_length(device, RegRes):
|
||||
kh = bytearray(RegRes['res'].attestation_object.auth_data.credential_data.credential_id)
|
||||
|
||||
with pytest.raises(ApduError) as e:
|
||||
device.ctap1.authenticate(
|
||||
RegRes['res'].client_data.hash, RegRes['res'].attestation_object.auth_data.rp_id_hash, kh[: len(kh) // 2]
|
||||
)
|
||||
assert e.value.code == APDU.WRONG_DATA
|
||||
|
||||
def test_incorrect_appid(device, RegRes):
|
||||
|
||||
badid = bytearray(RegRes['res'].attestation_object.auth_data.rp_id_hash)
|
||||
badid[0] = badid[0] ^ (0x40)
|
||||
with pytest.raises(ApduError) as e:
|
||||
device.ctap1.authenticate(
|
||||
RegRes['res'].client_data.hash, badid, RegRes['res'].attestation_object.auth_data.credential_data.credential_id
|
||||
)
|
||||
assert e.value.code == APDU.WRONG_DATA
|
||||
117
tests/utils.py
Normal file
117
tests/utils.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from fido2.webauthn import AttestedCredentialData
|
||||
import random
|
||||
import string
|
||||
import secrets
|
||||
import math
|
||||
from threading import Event, Timer
|
||||
from numbers import Number
|
||||
|
||||
|
||||
def verify(MC, GA, client_data_hash):
|
||||
credential_data = AttestedCredentialData(MC.auth_data.credential_data)
|
||||
GA.verify(client_data_hash, credential_data.public_key)
|
||||
|
||||
|
||||
def generate_random_user():
|
||||
# https://www.w3.org/TR/webauthn/#user-handle
|
||||
user_id_length = random.randint(1, 64)
|
||||
user_id = secrets.token_bytes(user_id_length)
|
||||
|
||||
# https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity
|
||||
name = "User name"
|
||||
display_name = "Displayed " + name
|
||||
|
||||
return {"id": user_id, "name": name, "displayName": display_name}
|
||||
|
||||
counter = 1
|
||||
def generate_user_maximum():
|
||||
"""
|
||||
Generate RK with the maximum lengths of the fields, according to the minimal requirements of the FIDO2 spec
|
||||
"""
|
||||
global counter
|
||||
|
||||
# https://www.w3.org/TR/webauthn/#user-handle
|
||||
user_id_length = 64
|
||||
user_id = secrets.token_bytes(user_id_length)
|
||||
|
||||
# https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity
|
||||
name = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(64))
|
||||
|
||||
name = f"{counter}: {name}"
|
||||
icon = "https://www.w3.org/TR/webauthn/" + "A" * 128
|
||||
display_name = "Displayed " + name
|
||||
|
||||
name = name[:64]
|
||||
display_name = display_name[:64]
|
||||
icon = icon[:128]
|
||||
|
||||
counter += 1
|
||||
|
||||
return {"id": user_id, "name": name, "icon": icon, "displayName": display_name}
|
||||
|
||||
def shannon_entropy(data):
|
||||
s = 0.0
|
||||
total = len(data)
|
||||
for x in range(0, 256):
|
||||
freq = data.count(x)
|
||||
p = freq / total
|
||||
if p > 0:
|
||||
s -= p * math.log2(p)
|
||||
return s
|
||||
|
||||
|
||||
# Timeout from:
|
||||
# https://github.com/Yubico/python-fido2/blob/f1dc028d6158e1d6d51558f72055c65717519b9b/fido2/utils.py
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
class Timeout(object):
|
||||
"""Utility class for adding a timeout to an event.
|
||||
:param time_or_event: A number, in seconds, or a threading.Event object.
|
||||
:ivar event: The Event associated with the Timeout.
|
||||
:ivar timer: The Timer associated with the Timeout, if any.
|
||||
"""
|
||||
|
||||
def __init__(self, time_or_event):
|
||||
|
||||
if isinstance(time_or_event, Number):
|
||||
self.event = Event()
|
||||
self.timer = Timer(time_or_event, self.event.set)
|
||||
else:
|
||||
self.event = time_or_event
|
||||
self.timer = None
|
||||
|
||||
def __enter__(self):
|
||||
if self.timer:
|
||||
self.timer.start()
|
||||
return self.event
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer.join()
|
||||
394
tools/pico-fido-tool.py
Normal file
394
tools/pico-fido-tool.py
Normal file
@@ -0,0 +1,394 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
/*
|
||||
* This file is part of the Pico Fido distribution (https://github.com/polhenarejos/pico-fido).
|
||||
* Copyright (c) 2022 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import platform
|
||||
from binascii import hexlify
|
||||
from words import words
|
||||
from threading import Event
|
||||
from typing import Mapping, Any, Optional, Callable
|
||||
import struct
|
||||
from enum import IntEnum, unique
|
||||
|
||||
try:
|
||||
from fido2.ctap2.config import Config
|
||||
from fido2.ctap2 import Ctap2
|
||||
from fido2.hid import CtapHidDevice, CTAPHID
|
||||
from fido2.utils import bytes2int, int2bytes
|
||||
from fido2 import cbor
|
||||
from fido2.ctap import CtapDevice, CtapError
|
||||
from fido2.ctap2.pin import PinProtocol, _PinUv
|
||||
from fido2.ctap2.base import args
|
||||
except:
|
||||
print('ERROR: fido2 module not found! Install fido2 package.\nTry with `pip install fido2`')
|
||||
sys.exit(-1)
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
except:
|
||||
print('ERROR: cryptography module not found! Install cryptography package.\nTry with `pip install cryptography`')
|
||||
sys.exit(-1)
|
||||
|
||||
from enum import IntEnum
|
||||
from binascii import hexlify
|
||||
|
||||
if (platform.system() == 'Windows' or platform.system() == 'Linux'):
|
||||
from secure_key import windows as skey
|
||||
elif (platform.system() == 'Darwin'):
|
||||
from secure_key import macos as skey
|
||||
else:
|
||||
print('ERROR: platform not supported')
|
||||
sys.exit(-1)
|
||||
|
||||
class VendorConfig(Config):
|
||||
|
||||
class PARAM(IntEnum):
|
||||
VENDOR_COMMAND_ID = 0x01
|
||||
VENDOR_AUT_CT = 0x02
|
||||
|
||||
class CMD(IntEnum):
|
||||
CONFIG_AUT_ENABLE = 0x03e43f56b34285e2
|
||||
CONFIG_AUT_DISABLE = 0x1831a40f04a25ed9
|
||||
|
||||
class RESP(IntEnum):
|
||||
KEY_AGREEMENT = 0x01
|
||||
|
||||
def __init__(self, ctap, pin_uv_protocol=None, pin_uv_token=None):
|
||||
super().__init__(ctap, pin_uv_protocol, pin_uv_token)
|
||||
|
||||
def enable_device_aut(self, ct):
|
||||
self._call(
|
||||
Config.CMD.VENDOR_PROTOTYPE,
|
||||
{
|
||||
VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_AUT_ENABLE,
|
||||
VendorConfig.PARAM.VENDOR_AUT_CT: ct
|
||||
},
|
||||
)
|
||||
|
||||
def disable_device_aut(self):
|
||||
self._call(
|
||||
Config.CMD.VENDOR_PROTOTYPE,
|
||||
{
|
||||
VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_AUT_DISABLE
|
||||
},
|
||||
)
|
||||
|
||||
class Ctap2Vendor(Ctap2):
|
||||
def __init__(self, device: CtapDevice, strict_cbor: bool = True):
|
||||
super().__init__(device=device, strict_cbor=strict_cbor)
|
||||
|
||||
|
||||
def send_vendor(
|
||||
self,
|
||||
cmd: int,
|
||||
data: Optional[Mapping[int, Any]] = None,
|
||||
*,
|
||||
event: Optional[Event] = None,
|
||||
on_keepalive: Optional[Callable[[int], None]] = None,
|
||||
) -> Mapping[int, Any]:
|
||||
"""Sends a VENDOR message to the device, and waits for a response.
|
||||
|
||||
:param cmd: The command byte of the request.
|
||||
:param data: The payload to send (to be CBOR encoded).
|
||||
:param event: Optional threading.Event used to cancel the request.
|
||||
:param on_keepalive: Optional function called when keep-alive is sent by
|
||||
the authenticator.
|
||||
"""
|
||||
request = struct.pack(">B", cmd)
|
||||
if data is not None:
|
||||
request += cbor.encode(data)
|
||||
response = self.device.call(CTAPHID.VENDOR_FIRST + 1, request, event, on_keepalive)
|
||||
status = response[0]
|
||||
if status != 0x00:
|
||||
raise CtapError(status)
|
||||
enc = response[1:]
|
||||
if not enc:
|
||||
return {}
|
||||
decoded = cbor.decode(enc)
|
||||
if self._strict_cbor:
|
||||
expected = cbor.encode(decoded)
|
||||
if expected != enc:
|
||||
raise ValueError(
|
||||
"Non-canonical CBOR from Authenticator.\n"
|
||||
f"Got: {enc.hex()}\nExpected: {expected.hex()}"
|
||||
)
|
||||
if isinstance(decoded, Mapping):
|
||||
return decoded
|
||||
raise TypeError("Decoded value of wrong type")
|
||||
|
||||
def vendor(
|
||||
self,
|
||||
cmd: int,
|
||||
sub_cmd: int,
|
||||
sub_cmd_params: Optional[Mapping[int, Any]] = None,
|
||||
pin_uv_protocol: Optional[int] = None,
|
||||
pin_uv_param: Optional[bytes] = None,
|
||||
) -> Mapping[int, Any]:
|
||||
"""CTAP2 authenticator vendor command.
|
||||
|
||||
This command is used to configure various authenticator features through the
|
||||
use of its subcommands.
|
||||
|
||||
This method is not intended to be called directly. It is intended to be used by
|
||||
an instance of the Config class.
|
||||
|
||||
:param sub_cmd: A Config sub command.
|
||||
:param sub_cmd_params: Sub command specific parameters.
|
||||
:param pin_uv_protocol: PIN/UV auth protocol version used.
|
||||
:param pin_uv_param: PIN/UV Auth parameter.
|
||||
"""
|
||||
return self.send_vendor(
|
||||
cmd,
|
||||
args(sub_cmd, sub_cmd_params, pin_uv_protocol, pin_uv_param),
|
||||
)
|
||||
|
||||
|
||||
class Vendor:
|
||||
"""Implementation of the CTAP2.1 Authenticator Vendor API. It is vendor implementation.
|
||||
|
||||
:param ctap: An instance of a CTAP2Vendor object.
|
||||
:param pin_uv_protocol: An instance of a PinUvAuthProtocol.
|
||||
:param pin_uv_token: A valid PIN/UV Auth Token for the current CTAP session.
|
||||
"""
|
||||
|
||||
@unique
|
||||
class CMD(IntEnum):
|
||||
VENDOR_BACKUP = 0x01
|
||||
VENDOR_MSE = 0x02
|
||||
VENDOR_UNLOCK = 0x03
|
||||
|
||||
@unique
|
||||
class PARAM(IntEnum):
|
||||
PARAM = 0x01
|
||||
COSE_KEY = 0x02
|
||||
|
||||
class SUBCMD(IntEnum):
|
||||
ENABLE = 0x01
|
||||
DISABLE = 0x02
|
||||
KEY_AGREEMENT = 0x01
|
||||
|
||||
class RESP(IntEnum):
|
||||
PARAM = 0x01
|
||||
COSE_KEY = 0x02
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ctap: Ctap2Vendor,
|
||||
pin_uv_protocol: Optional[PinProtocol] = None,
|
||||
pin_uv_token: Optional[bytes] = None,
|
||||
):
|
||||
self.ctap = ctap
|
||||
self.pin_uv = (
|
||||
_PinUv(pin_uv_protocol, pin_uv_token)
|
||||
if pin_uv_protocol and pin_uv_token
|
||||
else None
|
||||
)
|
||||
self.__key_enc = None
|
||||
self.__iv = None
|
||||
|
||||
self.vcfg = VendorConfig(ctap)
|
||||
|
||||
def _call(self, cmd, sub_cmd, params=None):
|
||||
if params:
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
else:
|
||||
params = None
|
||||
if self.pin_uv:
|
||||
msg = (
|
||||
b"\xff" * 32
|
||||
+ b"\x0d"
|
||||
+ struct.pack("<b", sub_cmd)
|
||||
+ (cbor.encode(params) if params else b"")
|
||||
)
|
||||
pin_uv_protocol = self.pin_uv.protocol.VERSION
|
||||
pin_uv_param = self.pin_uv.protocol.authenticate(self.pin_uv.token, msg)
|
||||
else:
|
||||
pin_uv_protocol = None
|
||||
pin_uv_param = None
|
||||
return self.ctap.vendor(cmd, sub_cmd, params, pin_uv_protocol, pin_uv_param)
|
||||
|
||||
def backup_save(self, filename):
|
||||
ret = self._call(
|
||||
Vendor.CMD.VENDOR_BACKUP,
|
||||
Vendor.SUBCMD.ENABLE,
|
||||
)
|
||||
data = ret[Vendor.RESP.PARAM]
|
||||
d = int.from_bytes(skey.get_secure_key(), 'big')
|
||||
with open(filename, 'wb') as fp:
|
||||
fp.write(b'\x01')
|
||||
fp.write(data)
|
||||
pk = ec.derive_private_key(d, ec.SECP256R1())
|
||||
signature = pk.sign(data, ec.ECDSA(hashes.SHA256()))
|
||||
fp.write(signature)
|
||||
print('Remember the following words in this order:')
|
||||
for c in range(24):
|
||||
coef = (d//(2048**c))%2048
|
||||
print(f'{(c+1):02d} - {words[coef]}')
|
||||
|
||||
def backup_load(self, filename):
|
||||
d = 0
|
||||
if (d == 0):
|
||||
for c in range(24):
|
||||
word = input(f'Introduce word {(c+1):02d}: ')
|
||||
while (word not in words):
|
||||
word = input(f'Word not found. Please, tntroduce the correct word {(c+1):02d}: ')
|
||||
coef = words.index(word)
|
||||
d = d+(2048**c)*coef
|
||||
|
||||
pk = ec.derive_private_key(d, ec.SECP256R1())
|
||||
pb = pk.public_key()
|
||||
with open(filename, 'rb') as fp:
|
||||
format = fp.read(1)[0]
|
||||
if (format == 0x1):
|
||||
data = fp.read(60)
|
||||
signature = fp.read()
|
||||
pb.verify(signature, data, ec.ECDSA(hashes.SHA256()))
|
||||
skey.set_secure_key(pk)
|
||||
return self._call(
|
||||
Vendor.CMD.VENDOR_BACKUP,
|
||||
Vendor.SUBCMD.DISABLE,
|
||||
{
|
||||
Vendor.PARAM.PARAM: data
|
||||
},
|
||||
)
|
||||
|
||||
def mse(self):
|
||||
sk = ec.generate_private_key(ec.SECP256R1())
|
||||
pn = sk.public_key().public_numbers()
|
||||
self.__pb = sk.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
|
||||
key_agreement = {
|
||||
1: 2,
|
||||
3: -25, # Per the spec, "although this is NOT the algorithm actually used"
|
||||
-1: 1,
|
||||
-2: int2bytes(pn.x, 32),
|
||||
-3: int2bytes(pn.y, 32),
|
||||
}
|
||||
|
||||
ret = self._call(
|
||||
Vendor.CMD.VENDOR_MSE,
|
||||
Vendor.SUBCMD.KEY_AGREEMENT,
|
||||
{
|
||||
Vendor.PARAM.COSE_KEY: key_agreement,
|
||||
},
|
||||
)
|
||||
|
||||
peer_cose_key = ret[VendorConfig.RESP.KEY_AGREEMENT]
|
||||
|
||||
x = bytes2int(peer_cose_key[-2])
|
||||
y = bytes2int(peer_cose_key[-3])
|
||||
pk = ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key()
|
||||
shared_key = sk.exchange(ec.ECDH(), pk)
|
||||
|
||||
xkdf = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=12+32,
|
||||
salt=None,
|
||||
info=self.__pb
|
||||
)
|
||||
kdf_out = xkdf.derive(shared_key)
|
||||
self.__key_enc = kdf_out[12:]
|
||||
self.__iv = kdf_out[:12]
|
||||
|
||||
def encrypt_chacha(self, data):
|
||||
chacha = ChaCha20Poly1305(self.__key_enc)
|
||||
ct = chacha.encrypt(self.__iv, data, self.__pb)
|
||||
return ct
|
||||
|
||||
def unlock_device(self):
|
||||
ct = self.get_skey()
|
||||
self._call(
|
||||
Vendor.CMD.VENDOR_UNLOCK,
|
||||
Vendor.SUBCMD.ENABLE,
|
||||
{
|
||||
Vendor.PARAM.PARAM: ct
|
||||
},
|
||||
)
|
||||
|
||||
def _get_key_device(self):
|
||||
return skey.get_secure_key()
|
||||
|
||||
def get_skey(self):
|
||||
self.mse()
|
||||
ct = self.encrypt_chacha(self._get_key_device())
|
||||
return ct
|
||||
|
||||
def enable_device_aut(self):
|
||||
ct = self.get_skey()
|
||||
self.vcfg.enable_device_aut(ct)
|
||||
|
||||
def disable_device_aut(self):
|
||||
self.vcfg.disable_device_aut()
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
subparser = parser.add_subparsers(title="commands", dest="command")
|
||||
parser_secure = subparser.add_parser('secure', help='Manages security of Pico Fido.')
|
||||
parser_secure.add_argument('subcommand', choices=['enable', 'disable', 'unlock'], help='Enables, disables or unlocks the security.')
|
||||
|
||||
parser_backup = subparser.add_parser('backup', help='Manages the backup of Pico Fido.')
|
||||
parser_backup.add_argument('subcommand', choices=['save', 'load'], help='Saves or loads a backup.')
|
||||
parser_backup.add_argument('filename', help='File to save or load the backup.')
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
def secure(vdr, args):
|
||||
if (args.subcommand == 'enable'):
|
||||
vdr.enable_device_aut()
|
||||
elif (args.subcommand == 'unlock'):
|
||||
vdr.unlock_device()
|
||||
elif (args.subcommand == 'disable'):
|
||||
vdr.disable_device_aut()
|
||||
|
||||
def backup(vdr, args):
|
||||
if (args.subcommand == 'save'):
|
||||
vdr.backup_save(args.filename)
|
||||
elif (args.subcommand == 'load'):
|
||||
vdr.backup_load(args.filename)
|
||||
|
||||
|
||||
def main(args):
|
||||
print('Pico Fido Tool v1.2')
|
||||
print('Author: Pol Henarejos')
|
||||
print('Report bugs to https://github.com/polhenarejos/pico-fido/issues')
|
||||
print('')
|
||||
print('')
|
||||
|
||||
dev = next(CtapHidDevice.list_devices(), None)
|
||||
|
||||
vdr = Vendor(Ctap2Vendor(dev))
|
||||
|
||||
if (args.command == 'secure'):
|
||||
secure(vdr, args)
|
||||
elif (args.command == 'backup'):
|
||||
backup(vdr, args)
|
||||
|
||||
def run():
|
||||
args = parse_args()
|
||||
main(args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
59
tools/secure_key/macos.py
Normal file
59
tools/secure_key/macos.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import sys
|
||||
import keyring
|
||||
|
||||
DOMAIN = "PicoKeys.com"
|
||||
USERNAME = "Pico-Fido"
|
||||
|
||||
try:
|
||||
import keyring
|
||||
from keyrings.osx_keychain_keys.backend import OSXKeychainKeysBackend, OSXKeychainKeyType, OSXKeyChainKeyClassType
|
||||
except:
|
||||
print('ERROR: keyring module not found! Install keyring package.\nTry with `pip install keyrings.osx-keychain-keys`')
|
||||
sys.exit(-1)
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
|
||||
except:
|
||||
print('ERROR: cryptography module not found! Install cryptography package.\nTry with `pip install cryptography`')
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def get_backend(use_secure_enclave=False):
|
||||
backend = OSXKeychainKeysBackend(
|
||||
key_type=OSXKeychainKeyType.EC, # Key type, e.g. RSA, RC, DSA, ...
|
||||
key_class_type=OSXKeyChainKeyClassType.Private, # Private key, Public key, Symmetric-key
|
||||
key_size_in_bits=256,
|
||||
is_permanent=True, # If set, saves the key in keychain; else, returns a transient key
|
||||
use_secure_enclave=use_secure_enclave, # Saves the key in the T2 (TPM) chip, requires a code-signed interpreter
|
||||
access_group=None, # Limits key management and retrieval to set group, requires a code-signed interpreter
|
||||
is_extractable=True # If set, private key is extractable; else, it can't be retrieved, but only operated against
|
||||
)
|
||||
return backend
|
||||
|
||||
def generate_secure_key(use_secure_enclave=False):
|
||||
backend = get_backend(use_secure_enclave)
|
||||
backend.set_password(DOMAIN, USERNAME, password=None)
|
||||
return backend.get_password(DOMAIN, USERNAME)
|
||||
|
||||
def get_d(key):
|
||||
return key.private_numbers().private_value.to_bytes(32, 'big')
|
||||
|
||||
def set_secure_key(pk):
|
||||
backend = get_backend(False)
|
||||
try:
|
||||
backend.delete_password(DOMAIN, USERNAME)
|
||||
except:
|
||||
pass
|
||||
backend.set_password(DOMAIN, USERNAME, pk.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
|
||||
|
||||
def get_secure_key():
|
||||
key = None
|
||||
try:
|
||||
backend = get_backend(False)
|
||||
key = backend.get_password(DOMAIN, USERNAME)[0]
|
||||
except keyring.errors.KeyringError:
|
||||
try:
|
||||
key = generate_secure_key(False)[0] # It should be True, but secure enclave causes python segfault
|
||||
except keyring.errors.PasswordSetError:
|
||||
key = generate_secure_key(False)[0]
|
||||
return get_d(key)
|
||||
1
tools/words.py
Normal file
1
tools/words.py
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user