Skip to content

Commit

Permalink
Stringify ISO 7816 SELECT command
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
leonlynch committed Apr 1, 2024
1 parent 4b3d31b commit 15ce8ee
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 4 deletions.
8 changes: 8 additions & 0 deletions src/iso7816_apdu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
119 changes: 118 additions & 1 deletion src/iso7816_strings.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "iso7816_strings.h"
#include "iso7816_apdu.h"

#include <stdbool.h>
#include <stdio.h>
#include <string.h>

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
92 changes: 89 additions & 3 deletions tests/emv_build_candidate_list_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 15ce8ee

Please sign in to comment.