Merge branch 'polhenarejos:main' into main

This commit is contained in:
oxygen
2026-01-20 20:53:02 +08:00
committed by GitHub
11 changed files with 121 additions and 50 deletions

View File

@@ -1,7 +1,7 @@
# Pico FIDO # Pico FIDO
This project transforms your Raspberry Pi Pico or ESP32 microcontroller into an integrated FIDO Passkey, functioning like a standard USB Passkey for authentication. This project transforms your Raspberry Pi Pico or ESP32 microcontroller into an integrated FIDO Passkey, functioning like a standard USB Passkey for authentication.
If you are looking for a Fido + OpenPGP, see: https://github.com/polhenarejos/pico-fido2 If you are looking for a OpenPGP + Fido, see: https://github.com/polhenarejos/pico-fido2. Available through [PicoKey App](https://www.picokeys.com/picokeyapp/ "PicoKey App").
## Features ## Features
Pico FIDO includes the following features: Pico FIDO includes the following features:
@@ -36,12 +36,13 @@ Pico FIDO includes the following features:
- Challenge-response generation - Challenge-response generation
- Emulated keyboard interface - Emulated keyboard interface
- Button press generates an OTP that is directly typed - Button press generates an OTP that is directly typed
- Yubico Authenticator app compatible
- Yubico YKMAN compatible - Yubico YKMAN compatible
- Nitrokey nitropy and nitroapp compatible - Nitrokey nitropy and nitroapp compatible
- Secure Boot and Secure Lock in RP2350 and ESP32-S3 MCUs - Secure Boot and Secure Lock in RP2350 and ESP32-S3 MCUs
- One Time Programming to store the master key that encrypts all resident keys and seeds. - One Time Programming to store the master key that encrypts all resident keys and seeds.
- Rescue interface to allow recovery of the device if it becomes unresponsive or undetectable. - Rescue interface to allow recovery of the device if it becomes unresponsive or undetectable.
- LED customization with Pico Commissioner. - LED customization with PicoKey App.
All features comply with the specifications. If you encounter unexpected behavior or deviations from the specifications, please open an issue. All features comply with the specifications. If you encounter unexpected behavior or deviations from the specifications, please open an issue.
@@ -55,11 +56,11 @@ Microcontrollers RP2350 and ESP32-S3 are designed to support secure environments
If you own a Raspberry Pico (RP2040 or RP2350), go to [Download page](https://www.picokeys.com/getting-started/), select your vendor and model and download the proper firmware; or go to [Release page](https://www.github.com/polhenarejos/pico-fido/releases/) and download the UF2 file for your board. If you own a Raspberry Pico (RP2040 or RP2350), go to [Download page](https://www.picokeys.com/getting-started/), select your vendor and model and download the proper firmware; or go to [Release page](https://www.github.com/polhenarejos/pico-fido/releases/) 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 plan to use it with other proprietary tools, you should modify Info.plist of CCID driver to add these VID/PID or use the [Pico Commissioner](https://www.picokeys.com/pico-commissioner/ "Pico Commissioner"). Note that UF2 files are shiped with a dummy VID/PID to avoid license issues (FEFF:FCFD). If you plan to use it with OpenSC or similar tools, you should modify Info.plist of CCID driver to add these VID/PID or use the [PicoKey App](https://www.picokeys.com/picokeyapp/ "PicoKey App").
You can use whatever VID/PID (i.e., 234b:0000 from FISJ), but remember that you are not authorized to distribute the binary with a VID/PID that you do not own. You can use whatever VID/PID (i.e., 234b:0000 from FISJ), but remember that you are not authorized to distribute the binary with a VID/PID that you do not own.
Note that the pure-browser option [Pico Commissioner](https://www.picokeys.com/pico-commissioner/ "Pico Commissioner") is the most recommended. Note that the [PicoKey App](https://www.picokeys.com/picokeyapp/ "PicoKey App") is the most recommended.
## Build for Raspberry Pico ## Build for Raspberry Pico
Before building, ensure you have installed the toolchain for the Pico and that the Pico SDK is properly located on your drive. Before building, ensure you have installed the toolchain for the Pico and that the Pico SDK is properly located on your drive.

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
VERSION_MAJOR="7" VERSION_MAJOR="7"
VERSION_MINOR="0" VERSION_MINOR="2"
SUFFIX="${VERSION_MAJOR}.${VERSION_MINOR}" SUFFIX="${VERSION_MAJOR}.${VERSION_MINOR}"
#if ! [[ -z "${GITHUB_SHA}" ]]; then #if ! [[ -z "${GITHUB_SHA}" ]]; then
# SUFFIX="${SUFFIX}.${GITHUB_SHA}" # SUFFIX="${SUFFIX}.${GITHUB_SHA}"

View File

@@ -41,6 +41,7 @@ int cbor_cred_mgmt(const uint8_t *data, size_t len);
int cbor_config(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); int cbor_vendor(const uint8_t *data, size_t len);
int cbor_large_blobs(const uint8_t *data, size_t len); int cbor_large_blobs(const uint8_t *data, size_t len);
extern void reset_gna_state();
extern int cmd_read_config(); extern int cmd_read_config();
@@ -59,6 +60,9 @@ int cbor_parse(uint8_t cmd, const uint8_t *data, size_t len) {
} }
if (cap_supported(CAP_FIDO2)) { if (cap_supported(CAP_FIDO2)) {
if (cmd == CTAPHID_CBOR) { if (cmd == CTAPHID_CBOR) {
if (data[0] != CTAP_GET_NEXT_ASSERTION) {
reset_gna_state();
}
if (data[0] == CTAP_MAKE_CREDENTIAL) { if (data[0] == CTAP_MAKE_CREDENTIAL) {
return cbor_make_credential(data + 1, len - 1); return cbor_make_credential(data + 1, len - 1);
} }

View File

@@ -42,6 +42,22 @@ uint32_t timerx = 0;
uint8_t *datax = NULL; uint8_t *datax = NULL;
size_t lenx = 0; size_t lenx = 0;
void reset_gna_state() {
for (int i = 0; i < MAX_CREDENTIAL_COUNT_IN_LIST; i++) {
credential_free(&credsx[i]);
}
if (datax) {
free(datax);
datax = NULL;
}
lenx = 0;
residentx = false;
timerx = 0;
flagsx = 0;
credentialCounter = 0;
numberOfCredentialsx = 0;
}
int cbor_get_next_assertion(const uint8_t *data, size_t len) { int cbor_get_next_assertion(const uint8_t *data, size_t len) {
(void) data; (void) data;
(void) len; (void) len;
@@ -57,19 +73,7 @@ int cbor_get_next_assertion(const uint8_t *data, size_t len) {
credentialCounter++; credentialCounter++;
err: err:
if (error != CborNoError || credentialCounter == numberOfCredentialsx) { if (error != CborNoError || credentialCounter == numberOfCredentialsx) {
for (int i = 0; i < MAX_CREDENTIAL_COUNT_IN_LIST; i++) { reset_gna_state();
credential_free(&credsx[i]);
}
if (datax) {
free(datax);
datax = NULL;
}
lenx = 0;
residentx = false;
timerx = 0;
flagsx = 0;
credentialCounter = 0;
numberOfCredentialsx = 0;
if (error == CborErrorImproperValue) { if (error == CborErrorImproperValue) {
return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; return CTAP2_ERR_CBOR_UNEXPECTED_TYPE;
} }

View File

@@ -613,24 +613,6 @@ int cbor_make_credential(const uint8_t *data, size_t len) {
CBOR_ERROR(CTAP2_ERR_PROCESSING); CBOR_ERROR(CTAP2_ERR_PROCESSING);
} }
if (user.id.len > 0 && user.parent.name.len > 0 && user.displayName.len > 0) {
if (memcmp(user.parent.name.data, "+pico", 5) == 0) {
options.rk = pfalse;
#ifndef ENABLE_EMULATION
uint8_t *p = (uint8_t *)user.parent.name.data + 5;
if (memcmp(p, "CommissionProfile", 17) == 0) {
ret = phy_unserialize_data(user.id.data, (uint16_t)user.id.len, &phy_data);
if (ret == PICOKEY_OK) {
ret = phy_save();
}
}
#endif
if (ret != PICOKEY_OK) {
CBOR_ERROR(CTAP2_ERR_PROCESSING);
}
}
}
uint8_t largeBlobKey[32] = {0}; uint8_t largeBlobKey[32] = {0};
if (extensions.largeBlobKey == ptrue && options.rk == ptrue) { if (extensions.largeBlobKey == ptrue && options.rk == ptrue) {
ret = credential_derive_large_blob_key(cred_id, cred_id_len, largeBlobKey); ret = credential_derive_large_blob_key(cred_id, cred_id_len, largeBlobKey);

View File

@@ -319,7 +319,7 @@ int credential_store(const uint8_t *cred_id, size_t cred_id_len, const uint8_t *
credential_free(&rcred); credential_free(&rcred);
continue; continue;
} }
if (memcmp(rcred.userId.data, cred.userId.data, MIN(rcred.userId.len, cred.userId.len)) == 0) { if (rcred.userId.len == cred.userId.len && memcmp(rcred.userId.data, cred.userId.data, rcred.userId.len) == 0) {
sloti = i; sloti = i;
credential_free(&rcred); credential_free(&rcred);
new_record = false; new_record = false;

View File

@@ -40,6 +40,8 @@
#define EF_OATH_CODE 0xBAFF #define EF_OATH_CODE 0xBAFF
#define EF_OTP_SLOT1 0xBB00 #define EF_OTP_SLOT1 0xBB00
#define EF_OTP_SLOT2 0xBB01 #define EF_OTP_SLOT2 0xBB01
#define EF_OTP_SLOT3 0xBB02
#define EF_OTP_SLOT4 0xBB03
#define EF_OTP_PIN 0x10A0 // Nitrokey OTP PIN #define EF_OTP_PIN 0x10A0 // Nitrokey OTP PIN
extern file_t *ef_keydev; extern file_t *ef_keydev;

View File

@@ -49,6 +49,11 @@ void append_keyboard_buffer(const uint8_t *buf, size_t len) {}
#define CONFIG_LED_INV 0x10 #define CONFIG_LED_INV 0x10
#define CONFIG_STATUS_MASK 0x1f #define CONFIG_STATUS_MASK 0x1f
#define CONFIG3_VALID 0x01
#define CONFIG4_VALID 0x02
#define CONFIG3_TOUCH 0x04
#define CONFIG4_TOUCH 0x08
/* EXT Flags */ /* EXT Flags */
#define SERIAL_BTN_VISIBLE 0x01 // Serial number visible at startup (button press) #define SERIAL_BTN_VISIBLE 0x01 // Serial number visible at startup (button press)
#define SERIAL_USB_VISIBLE 0x02 // Serial number visible in USB iSerial field #define SERIAL_USB_VISIBLE 0x02 // Serial number visible in USB iSerial field
@@ -161,7 +166,7 @@ extern void scan_all();
void init_otp() { void init_otp() {
if (scanned == false) { if (scanned == false) {
scan_all(); scan_all();
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 4; i++) {
file_t *ef = search_dynamic_file(EF_OTP_SLOT1 + i); file_t *ef = search_dynamic_file(EF_OTP_SLOT1 + i);
uint8_t *data = file_get_data(ef); uint8_t *data = file_get_data(ef);
otp_config_t *otp_config = (otp_config_t *) data; otp_config_t *otp_config = (otp_config_t *) data;
@@ -207,7 +212,8 @@ int otp_button_pressed(uint8_t slot) {
if (!cap_supported(CAP_OTP)) { if (!cap_supported(CAP_OTP)) {
return 3; return 3;
} }
file_t *ef = search_dynamic_file(slot == 1 ? EF_OTP_SLOT1 : EF_OTP_SLOT2); uint16_t slot_ef = EF_OTP_SLOT1 + slot - 1;
file_t *ef = search_dynamic_file(slot_ef);
const uint8_t *data = file_get_data(ef); const uint8_t *data = file_get_data(ef);
otp_config_t *otp_config = (otp_config_t *) data; otp_config_t *otp_config = (otp_config_t *) data;
if (file_has_data(ef) == false) { if (file_has_data(ef) == false) {
@@ -333,6 +339,39 @@ int otp_unload() {
} }
uint8_t status_byte = 0x0; uint8_t status_byte = 0x0;
uint16_t otp_status_ext() {
for (int i = 0; i < 4; i++) {
file_t *ef = search_dynamic_file(EF_OTP_SLOT1 + i);
if (file_has_data(ef)) {
res_APDU[res_APDU_size++] = 0xB0 + i;
res_APDU[res_APDU_size++] = 0; // Filled later
uint8_t *p = res_APDU + res_APDU_size;
otp_config_t *otp_config = (otp_config_t *)file_get_data(ef);
*p++ = 0xA0;
*p++ = 2;
*p++ = otp_config->tkt_flags;
*p++ = otp_config->cfg_flags;
if (otp_config->cfg_flags & CHAL_YUBICO && otp_config->tkt_flags & CHAL_RESP) {
}
else if (otp_config->tkt_flags & OATH_HOTP) {
}
else if (otp_config->cfg_flags & SHORT_TICKET || otp_config->cfg_flags & STATIC_TICKET) {
}
else {
*p++ = 0xC0;
*p++ = 6;
memcpy(p, otp_config->fixed_data, 6);
p += 6;
}
uint8_t len = p - (res_APDU + res_APDU_size);
res_APDU[res_APDU_size - 1] = len;
res_APDU_size += len;
}
}
return SW_OK();
}
uint16_t otp_status(bool is_otp) { uint16_t otp_status(bool is_otp) {
if (scanned == false) { if (scanned == false) {
scan_all(); scan_all();
@@ -384,12 +423,13 @@ bool check_crc(const otp_config_t *data) {
bool _is_otp = false; bool _is_otp = false;
int cmd_otp() { int cmd_otp() {
uint8_t p1 = P1(apdu), p2 = P2(apdu); uint8_t p1 = P1(apdu), p2 = P2(apdu);
if (p2 != 0x00) {
return SW_INCORRECT_P1P2();
}
if (p1 == 0x01 || p1 == 0x03) { // Configure slot if (p1 == 0x01 || p1 == 0x03) { // Configure slot
otp_config_t *odata = (otp_config_t *) apdu.data; otp_config_t *odata = (otp_config_t *) apdu.data;
file_t *ef = file_new(p1 == 0x01 ? EF_OTP_SLOT1 : EF_OTP_SLOT2); if (p1 == 0x03 && p2 != 0x0) {
return SW_INCORRECT_P1P2();
}
uint16_t slot = (p1 == 0x01 ? EF_OTP_SLOT1 : EF_OTP_SLOT2) + p2;
file_t *ef = file_new(slot);
if (file_has_data(ef)) { if (file_has_data(ef)) {
otp_config_t *otpc = (otp_config_t *) file_get_data(ef); otp_config_t *otpc = (otp_config_t *) file_get_data(ef);
if (memcmp(otpc->acc_code, apdu.data + otp_config_size, ACC_CODE_SIZE) != 0) { if (memcmp(otpc->acc_code, apdu.data + otp_config_size, ACC_CODE_SIZE) != 0) {
@@ -415,10 +455,14 @@ int cmd_otp() {
} }
else if (p1 == 0x04 || p1 == 0x05) { // Update slot else if (p1 == 0x04 || p1 == 0x05) { // Update slot
otp_config_t *odata = (otp_config_t *) apdu.data; otp_config_t *odata = (otp_config_t *) apdu.data;
if (p1 == 0x05 && p2 != 0x0) {
return SW_INCORRECT_P1P2();
}
uint16_t slot = (p1 == 0x04 ? EF_OTP_SLOT1 : EF_OTP_SLOT2) + p2;
if (odata->rfu[0] != 0 || odata->rfu[1] != 0 || check_crc(odata) == false) { if (odata->rfu[0] != 0 || odata->rfu[1] != 0 || check_crc(odata) == false) {
return SW_WRONG_DATA(); return SW_WRONG_DATA();
} }
file_t *ef = search_dynamic_file(p1 == 0x04 ? EF_OTP_SLOT1 : EF_OTP_SLOT2); file_t *ef = search_dynamic_file(slot);
if (file_has_data(ef)) { if (file_has_data(ef)) {
otp_config_t *otpc = (otp_config_t *) file_get_data(ef); otp_config_t *otpc = (otp_config_t *) file_get_data(ef);
if (memcmp(otpc->acc_code, apdu.data + otp_config_size, ACC_CODE_SIZE) != 0) { if (memcmp(otpc->acc_code, apdu.data + otp_config_size, ACC_CODE_SIZE) != 0) {
@@ -446,8 +490,16 @@ int cmd_otp() {
else if (p1 == 0x06) { // Swap slots else if (p1 == 0x06) { // Swap slots
uint8_t tmp[otp_config_size + 8]; uint8_t tmp[otp_config_size + 8];
bool ef1_data = false; bool ef1_data = false;
file_t *ef1 = file_new(EF_OTP_SLOT1); uint16_t slot1 = EF_OTP_SLOT1, slot2 = EF_OTP_SLOT2;
file_t *ef2 = file_new(EF_OTP_SLOT2); if (apdu.ne > 0) {
if (apdu.ne != 2) {
return SW_WRONG_LENGTH();
}
slot1 += apdu.data[0];
slot2 += apdu.data[1];
}
file_t *ef1 = file_new(slot1);
file_t *ef2 = file_new(slot2);
if (file_has_data(ef1)) { if (file_has_data(ef1)) {
memcpy(tmp, file_get_data(ef1), file_get_size(ef1)); memcpy(tmp, file_get_data(ef1), file_get_size(ef1));
ef1_data = true; ef1_data = true;
@@ -458,7 +510,7 @@ int cmd_otp() {
else { else {
delete_file(ef1); delete_file(ef1);
// When a dynamic file is deleted, existing referenes are invalidated // When a dynamic file is deleted, existing referenes are invalidated
ef2 = file_new(EF_OTP_SLOT2); ef2 = file_new(slot2);
} }
if (ef1_data) { if (ef1_data) {
file_put_data(ef2, tmp, sizeof(tmp)); file_put_data(ef2, tmp, sizeof(tmp));
@@ -478,8 +530,15 @@ int cmd_otp() {
else if (p1 == 0x13) { // Get config else if (p1 == 0x13) { // Get config
man_get_config(); man_get_config();
} }
else if (p1 == 0x14) {
otp_status_ext();
}
else if (p1 == 0x30 || p1 == 0x38 || p1 == 0x20 || p1 == 0x28) { // Calculate OTP else if (p1 == 0x30 || p1 == 0x38 || p1 == 0x20 || p1 == 0x28) { // Calculate OTP
file_t *ef = search_dynamic_file(p1 == 0x30 || p1 == 0x20 ? EF_OTP_SLOT1 : EF_OTP_SLOT2); if ((p1 == 0x38 || p1 == 0x28) && p2 != 0x0) {
return SW_INCORRECT_P1P2();
}
uint16_t slot = (p1 == 0x30 || p1 == 0x20 ? EF_OTP_SLOT1 : EF_OTP_SLOT2) + p2;
file_t *ef = search_dynamic_file(slot);
if (file_has_data(ef)) { if (file_has_data(ef)) {
otp_config_t *otp_config = (otp_config_t *) file_get_data(ef); otp_config_t *otp_config = (otp_config_t *) file_get_data(ef);
if (!(otp_config->tkt_flags & CHAL_RESP)) { if (!(otp_config->tkt_flags & CHAL_RESP)) {

View File

@@ -18,7 +18,7 @@
#ifndef __VERSION_H_ #ifndef __VERSION_H_
#define __VERSION_H_ #define __VERSION_H_
#define PICO_FIDO_VERSION 0x0700 #define PICO_FIDO_VERSION 0x0702
#define PICO_FIDO_VERSION_MAJOR ((PICO_FIDO_VERSION >> 8) & 0xff) #define PICO_FIDO_VERSION_MAJOR ((PICO_FIDO_VERSION >> 8) & 0xff)
#define PICO_FIDO_VERSION_MINOR (PICO_FIDO_VERSION & 0xff) #define PICO_FIDO_VERSION_MINOR (PICO_FIDO_VERSION & 0xff)

View File

@@ -202,10 +202,29 @@ def test_rk_with_allowlist_of_different_rp(resetdevice):
assert e.value.code == CtapError.ERR.NO_CREDENTIALS assert e.value.code == CtapError.ERR.NO_CREDENTIALS
def test_same_prefix_userId(device):
"""
A make credential request with two different UserIds that share the same prefix should NOT overwrite.
"""
rp = {"id": "sameprefix.org", "name": "Example"}
user1 = {"id": b"user_12", "name": "A fixed name", "displayName": "A fixed display name"}
user2 = {"id": b"user_123", "name": "A fixed name", "displayName": "A fixed display name"}
mc_res1 = device.MC(rp = rp, options={"rk":True}, user = user1)
# Should not overwrite the first credential.
mc_res2 = device.MC(rp = rp, options={"rk":True}, user = user2)
ga_res = device.GA(rp_id=rp['id'])['res']
assert ga_res.number_of_credentials == 2
def test_same_userId_overwrites_rk(resetdevice): 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. A make credential request with a UserId & Rp that is the same as an existing one should overwrite.
""" """
resetdevice.reset()
rp = {"id": "overwrite.org", "name": "Example"} rp = {"id": "overwrite.org", "name": "Example"}
user = generate_random_user() user = generate_random_user()