From 15ce8ee4908110264270cb30deaeb81c883f70c7 Mon Sep 17 00:00:00 2001 From: Leon Lynch Date: Mon, 1 Apr 2024 19:02:05 +0200 Subject: [PATCH] Stringify ISO 7816 SELECT command This is a simple implementation that only stringifies the file identifier and the file occurence bits of P2 to provide a one-line description to iso7816_capdu_get_string(). This commit also adds testing for partial AID matching which illustrates the file occurence bits of P2. --- src/iso7816_apdu.h | 8 ++ src/iso7816_strings.c | 119 +++++++++++++++++++++++++- tests/emv_build_candidate_list_test.c | 92 +++++++++++++++++++- 3 files changed, 215 insertions(+), 4 deletions(-) diff --git a/src/iso7816_apdu.h b/src/iso7816_apdu.h index ef2167b..c598908 100644 --- a/src/iso7816_apdu.h +++ b/src/iso7816_apdu.h @@ -47,6 +47,14 @@ __BEGIN_DECLS #define ISO7816_CLA_INTERINDUSTRY (0x00) ///< ISO 7816 C-APDU interindustry class #define ISO7816_CLA_PROPRIETARY (0x80) ///< ISO 7816 C-APDU proprietary class +// ISO 7816 SELECT (A4) command, P2 byte +// See ISO 7816-4:2005, 7.1.1, table 40 +#define ISO7816_SELECT_P2_FILE_OCCURRENCE_MASK (0x03) ///< ISO 7816 SELECT P2 mask for file occurrence +#define ISO7816_SELECT_P2_FILE_OCCURRENCE_FIRST (0x00) ///< ISO 7816 SELECT: First occurrence +#define ISO7816_SELECT_P2_FILE_OCCURRENCE_LAST (0x01) ///< ISO 7816 SELECT: Last occurrence +#define ISO7816_SELECT_P2_FILE_OCCURRENCE_NEXT (0x02) ///< ISO 7816 SELECT: Next occurrence +#define ISO7816_SELECT_P2_FILE_OCCURRENCE_PREVIOUS (0x03) ///< ISO 7816 SELECT: Previous occurrence + /// ISO 7816 C-APDU cases. See ISO 7816-3:2006, 12.1.3 enum iso7816_apdu_case_t { ISO7816_APDU_CASE_1, ///< ISO 7816 C-APDU case 1: CLA, INS, P1, P2 diff --git a/src/iso7816_strings.c b/src/iso7816_strings.c index fd02ede..5ed7de2 100644 --- a/src/iso7816_strings.c +++ b/src/iso7816_strings.c @@ -22,6 +22,7 @@ #include "iso7816_strings.h" #include "iso7816_apdu.h" +#include #include #include @@ -61,6 +62,122 @@ static void iso7816_str_list_add(struct str_itr_t* itr, const char* str) *itr->ptr = 0; } +static const char* iso7816_capdu_select_get_string( + const uint8_t* c_apdu, + size_t c_apdu_len, + char* str, + size_t str_len +) +{ + enum iso7816_apdu_case_t apdu_case; + const struct iso7816_apdu_case_2s_t* apdu_case_2s = NULL; + const struct iso7816_apdu_case_4s_t* apdu_case_4s = NULL; + bool file_id_printable; + char file_id_str[ISO7816_CAPDU_DATA_MAX * 2 + 1]; + const char* occurence_str; + + if (!c_apdu || !c_apdu_len || !str || !str_len) { + // Invalid parameters + return NULL; + } + + if ((c_apdu[0] & ISO7816_CLA_PROPRIETARY) != 0 || + c_apdu[1] != 0xA4 + ) { + // Not SELECT + return NULL; + } + + // SELECT must be APDU case 2S or case 4S + apdu_case = iso7816_apdu_case(c_apdu, c_apdu_len); + switch (apdu_case) { + case ISO7816_APDU_CASE_2S: + apdu_case_2s = (void*)c_apdu; + break; + + case ISO7816_APDU_CASE_4S: + apdu_case_4s = (void*)c_apdu; + break; + + default: + // Invalid APDU case for SELECT + return NULL; + } + + // Determine whether master file is being selected + // See ISO 7816-4:2005, 7.1.1 + if (c_apdu[2] == 0x00 && c_apdu[3] == 0x00) { // If P1 and P2 are zero + // If no command data field or command data field is 3F00 + if (apdu_case_2s || + ( + apdu_case_4s && + apdu_case_4s->Lc == 2 && + apdu_case_4s->data[0] == 0x3F && + apdu_case_4s->data[1] == 0x00 + ) + ) { + snprintf(str, str_len, "SELECT Master File"); + return str; + } + } else if (!apdu_case_4s) { + // Must be APDU case 4S if P1 or P2 are non-zero + return NULL; + } + + // Determine whether file identifier is printable + file_id_printable = true; + for (size_t i = 0; i < apdu_case_4s->Lc; ++i) { + if (apdu_case_4s->data[i] < 0x20 || apdu_case_4s->data[i] > 0x7E) { + file_id_printable = false; + break; + } + } + if (file_id_printable) { + snprintf(file_id_str, sizeof(file_id_str), "%.*s", apdu_case_4s->Lc, apdu_case_4s->data); + } else { + for (size_t i = 0; i < apdu_case_4s->Lc; ++i) { + snprintf( + file_id_str + (i * 2), + sizeof(file_id_str) - 1 - (i * 2), + "%02X", + apdu_case_4s->data[i] + ); + } + } + + // Decode P2 for file occurence + // See ISO 7816-4:2005, 7.1.1, table 40 + switch (apdu_case_4s->P2 & ISO7816_SELECT_P2_FILE_OCCURRENCE_MASK) { + case ISO7816_SELECT_P2_FILE_OCCURRENCE_FIRST: + occurence_str = "first"; + break; + + case ISO7816_SELECT_P2_FILE_OCCURRENCE_LAST: + occurence_str = "last"; + break; + + case ISO7816_SELECT_P2_FILE_OCCURRENCE_NEXT: + occurence_str = "next"; + break; + + case ISO7816_SELECT_P2_FILE_OCCURRENCE_PREVIOUS: + occurence_str = "previous"; + break; + + default: + // Invalid P2 + return NULL; + } + + // Stringify + snprintf(str, str_len, "SELECT %s %s", + occurence_str, + file_id_str + ); + + return str; +} + const char* iso7816_capdu_get_string( const void* c_apdu, size_t c_apdu_len, @@ -121,7 +238,7 @@ const char* iso7816_capdu_get_string( case 0xA0: case 0xA1: ins_str = "SEARCH BINARY"; break; case 0xA2: ins_str = "SEARCH RECORD"; break; - case 0xA4: ins_str = "SELECT"; break; + case 0xA4: return iso7816_capdu_select_get_string(c_apdu, c_apdu_len, str, str_len); case 0xB0: case 0xB1: ins_str = "READ BINARY"; break; case 0xB2: diff --git a/tests/emv_build_candidate_list_test.c b/tests/emv_build_candidate_list_test.c index 331583d..599b564 100644 --- a/tests/emv_build_candidate_list_test.c +++ b/tests/emv_build_candidate_list_test.c @@ -222,7 +222,7 @@ static const struct xpdu_t test_pse_multi_app_supported[] = { { 0 } }; -static const struct xpdu_t test_aid_multi_app_supported[] = { +static const struct xpdu_t test_aid_multi_exact_match_app_supported[] = { { 20, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x0E, 0x31, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0x00 }, // SELECT 1PAY.SYS.DDF01 2, (uint8_t[]){ 0x6A, 0x82 }, // File or application not found @@ -250,6 +250,58 @@ static const struct xpdu_t test_aid_multi_app_supported[] = { { 0 } }; +static const struct xpdu_t test_aid_multi_partial_match_app_supported[] = { + { + 20, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x0E, 0x31, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0x00 }, // SELECT 1PAY.SYS.DDF01 + 2, (uint8_t[]){ 0x6A, 0x82 }, // File or application not found + }, + { + 12, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x06, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x10, 0x00 }, // SELECT A00000000310 + 2, (uint8_t[]){ 0x6A, 0x82 }, // File or application not found + }, + { + 13, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x10, 0x00 }, // SELECT A0000000032010 + 2, (uint8_t[]){ 0x6A, 0x82 }, // File or application not found + }, + { + 13, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x20, 0x00 }, // SELECT A0000000032020 + 2, (uint8_t[]){ 0x6A, 0x82 }, // File or application not found + }, + { + 12, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x06, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00 }, // SELECT first A00000000410 + 72, (uint8_t[]){ + 0x6F, 0x44, 0x84, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10, 0xA5, + 0x39, 0x50, 0x09, 0x4D, 0x43, 0x20, 0x43, 0x52, 0x45, 0x44, 0x49, 0x54, + 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0x87, 0x01, 0x01, 0x9F, 0x11, + 0x01, 0x01, 0x9F, 0x12, 0x0A, 0x4D, 0x41, 0x53, 0x54, 0x45, 0x52, 0x43, + 0x41, 0x52, 0x44, 0xBF, 0x0C, 0x10, 0x9F, 0x4D, 0x02, 0x0B, 0x0A, 0x9F, + 0x0A, 0x08, 0x00, 0x01, 0x05, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x90, 0x00, + }, // FCI + }, + { + 12, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x02, 0x06, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00 }, // SELECT next A00000000410 + 72, (uint8_t[]){ + 0x6F, 0x44, 0x84, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x20, 0xA5, + 0x39, 0x50, 0x09, 0x4D, 0x43, 0x20, 0x43, 0x52, 0x45, 0x44, 0x49, 0x54, + 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0x87, 0x01, 0x02, 0x9F, 0x11, + 0x01, 0x01, 0x9F, 0x12, 0x0A, 0x4D, 0x41, 0x53, 0x54, 0x45, 0x52, 0x43, + 0x41, 0x52, 0x44, 0xBF, 0x0C, 0x10, 0x9F, 0x4D, 0x02, 0x0B, 0x0A, 0x9F, + 0x0A, 0x08, 0x00, 0x01, 0x05, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x90, 0x00, + }, // FCI + }, + { + 12, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x02, 0x06, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00 }, // SELECT next A00000000410 + 2, (uint8_t[]){ 0x6A, 0x82 }, // File or application not found + }, + { + 12, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x06, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x30, 0x00 }, // SELECT A00000000430 + 2, (uint8_t[]){ 0x6A, 0x82 }, // File or application not found + }, + { 0 } +}; + static const struct xpdu_t test_sorted_app_priority[] = { { 20, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x0E, 0x31, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0x00 }, // SELECT 1PAY.SYS.DDF01 @@ -596,8 +648,42 @@ int main(void) } printf("Success\n"); - printf("\nTesting PSE not found and multiple AIDs supported...\n"); - emul_ctx.xpdu_list = test_aid_multi_app_supported; + printf("\nTesting PSE not found and multiple exact match AIDs supported...\n"); + emul_ctx.xpdu_list = test_aid_multi_exact_match_app_supported; + emul_ctx.xpdu_current = NULL; + emv_app_list_clear(&app_list); + r = emv_build_candidate_list( + &ttl, + &supported_aids, + &app_list + ); + if (r) { + fprintf(stderr, "Unexpected emv_build_candidate_list() result; error %d: %s\n", r, r < 0 ? emv_error_get_string(r) : emv_outcome_get_string(r)); + r = 1; + goto exit; + } + if (emul_ctx.xpdu_current->c_xpdu_len != 0) { + fprintf(stderr, "Incomplete card interaction\n"); + r = 1; + goto exit; + } + if (emv_app_list_is_empty(&app_list)) { + fprintf(stderr, "Candidate list unexpectedly empty\n"); + r = 1; + goto exit; + } + for (struct emv_app_t* app = app_list.front; app != NULL; app = app->next) { + print_emv_app(app); + } + if (!emv_app_list_selection_is_required(&app_list)) { + fprintf(stderr, "Cardholder application selection unexpectedly NOT required\n"); + r = 1; + goto exit; + } + printf("Success\n"); + + printf("\nTesting PSE not found and multiple partial match AIDs supported...\n"); + emul_ctx.xpdu_list = test_aid_multi_partial_match_app_supported; emul_ctx.xpdu_current = NULL; emv_app_list_clear(&app_list); r = emv_build_candidate_list(