Skip to content

Commit

Permalink
src: Add support for NFC over U2F and FIDO2
Browse files Browse the repository at this point in the history
Tested with Android on Pixel 5 and iOS.
Some todo remains.
  • Loading branch information
Xavier Chapron authored and lpascal-ledger committed May 29, 2024
1 parent 40a3a95 commit b4dbb76
Show file tree
Hide file tree
Showing 18 changed files with 757 additions and 54 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ DEFINES += HAVE_DEBUG_THROWS

#DEFINES += HAVE_CBOR_DEBUG


ENABLE_NFC = 1

##############
# Compiler #
##############
Expand Down
14 changes: 13 additions & 1 deletion include/ctap2.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@
#define CMD_IS_OVER_U2F_CMD (G_io_app.apdu_state != APDU_IDLE)
#define CMD_IS_OVER_CTAP2_CBOR_CMD (G_io_app.apdu_state == APDU_IDLE)

#define CMD_IS_OVER_U2F_USB (G_io_u2f.media == U2F_MEDIA_USB)

#ifdef HAVE_NFC
#define CMD_IS_OVER_U2F_NFC (G_io_app.apdu_media == IO_APDU_MEDIA_NFC)
void nfc_idle_work2(void);
#else
#define CMD_IS_OVER_U2F_NFC false
#endif

extern const uint8_t AAGUID[16];

typedef struct ctap2_register_data_s {
Expand Down Expand Up @@ -177,7 +186,10 @@ void ctap2_send_keepalive_processing(void);
// Correspond to FIDO2.1 spec performBuiltInUv() operation
void performBuiltInUv(void);

void ctap2_make_credential_handle(u2f_service_t *service, uint8_t *buffer, uint16_t length);
void ctap2_make_credential_handle(u2f_service_t *service,
uint8_t *buffer,
uint16_t length,
bool *immediateReply);
void ctap2_get_assertion_handle(u2f_service_t *service,
uint8_t *buffer,
uint16_t length,
Expand Down
44 changes: 44 additions & 0 deletions include/nfc_io.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
*******************************************************************************
* Ledger App Security Key
* (c) 2024 Ledger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
********************************************************************************/

#ifdef HAVE_NFC
void nfc_io_set_le(uint32_t le);
void nfc_io_set_response_ready(uint16_t sw, uint16_t len, const char *status);
bool nfc_io_is_response_pending(void);
int nfc_io_send_prepared_response(void);

#else
static inline void nfc_io_set_le(uint32_t le __attribute__((unused))) {
return;
}

static inline void nfc_io_set_response_ready(uint16_t sw, uint16_t len, const char *status) {
UNUSED(sw);
UNUSED(len);
UNUSED(status);
return;
}

static inline bool nfc_io_is_response_pending(void) {
return false;
}

static inline int nfc_io_send_prepared_response(void) {
return -1;
}
#endif
6 changes: 5 additions & 1 deletion include/u2f_process.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ typedef struct u2f_data_t {

int u2f_handle_apdu(uint8_t *rx, int length);

#endif
#ifdef HAVE_NFC
void nfc_idle_work(void);
#endif // HAVE_NFC

#endif // __U2F_PROCESS_H__
4 changes: 4 additions & 0 deletions src/app_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ void app_ticker_event_callback(void) {
if (ctap2UxState != CTAP2_UX_STATE_NONE) {
u2f_transport_ctap2_send_keepalive(&G_io_u2f, KEEPALIVE_REASON_TUP_NEEDED);
}
#ifdef HAVE_NFC
nfc_idle_work();
nfc_idle_work2();
#endif
}

/**
Expand Down
14 changes: 12 additions & 2 deletions src/ctap2_get_assertion.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ static int parse_getAssert_authnr_rpid(cbipDecoder_t *decoder, cbipItem_t *mapIt
}

#ifdef HAVE_FIDO2_RPID_FILTER
if (CMD_IS_OVER_U2F_CMD) {
if (CMD_IS_OVER_U2F_CMD && !CMD_IS_OVER_U2F_NFC) {
if (ctap2_check_rpid_filter(ctap2AssertData->rpId, ctap2AssertData->rpIdLen)) {
PRINTF("rpId denied by filter\n");
return ERROR_PROP_RPID_MEDIA_DENIED;
Expand Down Expand Up @@ -380,7 +380,17 @@ void ctap2_get_assertion_handle(u2f_service_t *service,
goto exit;
}

if (!ctap2AssertData->userPresenceRequired && !ctap2AssertData->pinRequired) {
if (CMD_IS_OVER_U2F_NFC) {
// No up nor uv requested, skip UX and reply immediately
// TODO: is this what we want?
// TODO: Handle cases where availableCredentials is != 1
// -> which credentials should be chosen?
// -> when credentials comes from allowListPresent, I think the spec allow to choose for
// the user
// -> when credentials comes from rk, the spec ask to use authenticatorGetNextAssertion
// features
*immediateReply = true;
} else if (!ctap2AssertData->userPresenceRequired && !ctap2AssertData->pinRequired) {
// No up nor uv required, skip UX and reply immediately
*immediateReply = true;
} else {
Expand Down
102 changes: 90 additions & 12 deletions src/ctap2_make_credential.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ static int parse_makeCred_authnr_rp(cbipDecoder_t *decoder, cbipItem_t *mapItem)
}

#ifdef HAVE_FIDO2_RPID_FILTER
if (CMD_IS_OVER_U2F_CMD) {
if (CMD_IS_OVER_U2F_CMD && !CMD_IS_OVER_U2F_NFC) {
if (ctap2_check_rpid_filter(ctap2RegisterData->rpId, ctap2RegisterData->rpIdLen)) {
PRINTF("rpId denied by filter\n");
return ERROR_PROP_RPID_MEDIA_DENIED;
Expand Down Expand Up @@ -409,14 +409,18 @@ static int process_makeCred_authnr_pin(cbipDecoder_t *decoder, cbipItem_t *mapIt
return 0;
}

void ctap2_make_credential_handle(u2f_service_t *service, uint8_t *buffer, uint16_t length) {
void ctap2_make_credential_handle(u2f_service_t *service,
uint8_t *buffer,
uint16_t length,
bool *immediateReply) {
ctap2_register_data_t *ctap2RegisterData = globals_get_ctap2_register_data();
cbipDecoder_t decoder;
cbipItem_t mapItem;
int status;

PRINTF("ctap2_make_credential_handle\n");

*immediateReply = false;
memset(ctap2RegisterData, 0, sizeof(ctap2_register_data_t));
ctap2RegisterData->buffer = buffer;

Expand Down Expand Up @@ -482,7 +486,13 @@ void ctap2_make_credential_handle(u2f_service_t *service, uint8_t *buffer, uint1
goto exit;
}

ctap2_make_credential_ux();
if (CMD_IS_OVER_U2F_NFC) {
// No up nor uv requested, skip UX and reply immediately
// TODO: is this what we want?
*immediateReply = true;
} else {
ctap2_make_credential_ux();
}

exit:
if (status != 0) {
Expand All @@ -492,23 +502,83 @@ void ctap2_make_credential_handle(u2f_service_t *service, uint8_t *buffer, uint1
return;
}

static int generate_pubkey(const uint8_t *nonce, int coseAlgorithm, cx_ecfp_public_key_t *pubkey) {
cx_ecfp_private_key_t privateKey;
cx_curve_t bolosCurve = cose_alg_to_cx(coseAlgorithm);

if (crypto_generate_private_key(nonce, &privateKey, bolosCurve) != 0) {
return -1;
}
if (cx_ecfp_generate_pair_no_throw(bolosCurve, pubkey, &privateKey, 1) != CX_OK) {
return -1;
}

return 0;
}

#ifdef HAVE_NFC
static bool nfc_nonce_and_pubkey_ready;
static uint8_t nfc_nonce[CREDENTIAL_NONCE_SIZE];
static cx_ecfp_public_key_t nfc_pubkey_ES256;
static cx_ecfp_public_key_t nfc_pubkey_ES256K;
static cx_ecfp_public_key_t nfc_pubkey_EDDSA;

void nfc_idle_work2(void) {
// Generate a new nonce/pubkey pair only if not already available and in idle
if (nfc_nonce_and_pubkey_ready) {
return;
}

cx_rng_no_throw(nfc_nonce, CREDENTIAL_NONCE_SIZE);

if (generate_pubkey(nfc_nonce, COSE_ALG_ES256, &nfc_pubkey_ES256) != 0) {
return;
}

if (generate_pubkey(nfc_nonce, COSE_ALG_ES256K, &nfc_pubkey_ES256K) != 0) {
return;
}

if (generate_pubkey(nfc_nonce, COSE_ALG_EDDSA, &nfc_pubkey_EDDSA) != 0) {
return;
}

nfc_nonce_and_pubkey_ready = true;
}
#endif

static int encode_makeCred_public_key(const uint8_t *nonce,
int coseAlgorithm,
uint8_t *buffer,
uint32_t bufferLength) {
cbipEncoder_t encoder;
cx_ecfp_private_key_t privateKey;
cx_ecfp_public_key_t publicKey;
cx_curve_t bolosCurve;
int status;

bolosCurve = cose_alg_to_cx(coseAlgorithm);
#ifdef HAVE_NFC
// Spare response time by pre-generating part of the answer
if (nfc_nonce_and_pubkey_ready) {
switch (coseAlgorithm) {
case COSE_ALG_ES256:
memcpy(&publicKey, &nfc_pubkey_ES256, sizeof(publicKey));
break;
case COSE_ALG_ES256K:
memcpy(&publicKey, &nfc_pubkey_ES256K, sizeof(publicKey));
break;
case COSE_ALG_EDDSA:
memcpy(&publicKey, &nfc_pubkey_EDDSA, sizeof(publicKey));
break;
default:
return -1;
}

if (crypto_generate_private_key(nonce, &privateKey, bolosCurve) != 0) {
return -1;
}
if (cx_ecfp_generate_pair_no_throw(bolosCurve, &publicKey, &privateKey, 1) != CX_OK) {
return -1;
nfc_nonce_and_pubkey_ready = false;
} else
#endif
{
if (generate_pubkey(nonce, coseAlgorithm, &publicKey) != 0) {
return -1;
}
}

cbip_encoder_init(&encoder, buffer, bufferLength);
Expand Down Expand Up @@ -672,7 +742,15 @@ void ctap2_make_credential_confirm() {
ctap2_send_keepalive_processing();

// Generate nonce
cx_rng_no_throw(nonce, CREDENTIAL_NONCE_SIZE);
#ifdef HAVE_NFC
// Spare response time by pre-generating part of the answer
if (nfc_nonce_and_pubkey_ready) {
memcpy(nonce, nfc_nonce, CREDENTIAL_NONCE_SIZE);
} else
#endif
{
cx_rng_no_throw(nonce, CREDENTIAL_NONCE_SIZE);
}

// Build auth data
status =
Expand Down
39 changes: 28 additions & 11 deletions src/ctap2_processing.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,20 @@
#include "fido_known_apps.h"
#include "ui_shared.h"
#include "sw_code.h"
#include "nfc_io.h"

static uint8_t cmdType;

#define RPID_FILTER "webctap."
#define RPID_FILTER_SIZE (sizeof(RPID_FILTER) - 1)

#define CBOR_MAKE_CREDENTIAL 0x01
#define CBOR_GET_ASSERTION 0x02
#define CBOR_GET_NEXT_ASSERTION 0x08
#define CBOR_GET_INFO 0x04
#define CBOR_CLIENT_PIN 0x06
#define CBOR_RESET 0x07

bool ctap2_check_rpid_filter(const char *rpId, uint32_t rpIdLen) {
if ((rpIdLen < RPID_FILTER_SIZE) || (memcmp(rpId, RPID_FILTER, RPID_FILTER_SIZE) != 0)) {
return true;
Expand All @@ -50,7 +60,16 @@ void send_cbor_error(u2f_service_t *service, uint8_t error) {
}

void send_cbor_response(u2f_service_t *service, uint32_t length) {
if (CMD_IS_OVER_U2F_CMD) {
if (CMD_IS_OVER_U2F_NFC) {
const char *status = NULL;
if (cmdType == CBOR_MAKE_CREDENTIAL) {
status = "Registration details\nsent";
} else if (cmdType == CBOR_GET_ASSERTION) {
status = "Login request signed";
}
nfc_io_set_response_ready(SW_NO_ERROR, length, status);
nfc_io_send_prepared_response();
} else if (CMD_IS_OVER_U2F_CMD) {
io_send_response_pointer(responseBuffer, length, SW_NO_ERROR);
} else {
u2f_message_reply(service, CTAP2_CMD_CBOR, responseBuffer, length);
Expand All @@ -69,13 +88,6 @@ void performBuiltInUv(void) {
// No-op as the user is verified through the session PIN.
}

#define CBOR_MAKE_CREDENTIAL 0x01
#define CBOR_GET_ASSERTION 0x02
#define CBOR_GET_NEXT_ASSERTION 0x08
#define CBOR_GET_INFO 0x04
#define CBOR_CLIENT_PIN 0x06
#define CBOR_RESET 0x07

void ctap2_handle_cmd_cbor(u2f_service_t *service, uint8_t *buffer, uint16_t length) {
int status;
// PRINTF("cmd_cbor %d %.*H\n", length, length, buffer);
Expand All @@ -96,11 +108,16 @@ void ctap2_handle_cmd_cbor(u2f_service_t *service, uint8_t *buffer, uint16_t len
send_cbor_error(service, ERROR_INVALID_CBOR);
return;
}
cmdType = buffer[0];

switch (buffer[0]) {
case CBOR_MAKE_CREDENTIAL:
ctap2_make_credential_handle(service, buffer + 1, length - 1);
break;
case CBOR_MAKE_CREDENTIAL: {
bool immediateReply;
ctap2_make_credential_handle(service, buffer + 1, length - 1, &immediateReply);
if (immediateReply) {
ctap2_make_credential_confirm();
}
} break;
case CBOR_GET_ASSERTION: {
bool immediateReply;
ctap2_get_assertion_handle(service, buffer + 1, length - 1, &immediateReply);
Expand Down
6 changes: 6 additions & 0 deletions src/ctap2_reset.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ void ctap2_reset_handle(u2f_service_t *service, uint8_t *buffer, uint16_t length
UNUSED(buffer);
UNUSED(length);

if (CMD_IS_OVER_U2F_NFC) {
// Denied authenticatorReset over NFC as it can't be approved by the user.
// Note, this is a behavior allowed by the FIDO spec.
send_cbor_error(&G_io_u2f, ERROR_OPERATION_DENIED);
}

PRINTF("ctap2_reset_handle\n");
ctap2_reset_ux();
}
Expand Down
Loading

0 comments on commit b4dbb76

Please sign in to comment.