diff --git a/src/emv.c b/src/emv.c index 3b9d62e..f04ab31 100644 --- a/src/emv.c +++ b/src/emv.c @@ -2,7 +2,7 @@ * @file emv.c * @brief High level EMV library interface * - * Copyright (c) 2023 Leon Lynch + * Copyright (c) 2023-2024 Leon Lynch * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,6 +21,8 @@ #include "emv.h" #include "emv_utils_config.h" +#include "emv_tal.h" +#include "emv_app.h" #include "iso7816.h" @@ -44,8 +46,11 @@ const char* emv_error_get_string(enum emv_error_t error) const char* emv_outcome_get_string(enum emv_outcome_t outcome) { + // See EMV 4.4 Book 4, 11.2, table 8 switch (outcome) { - case EMV_OUTCOME_CARD_ERROR: return "Card error"; + case EMV_OUTCOME_CARD_ERROR: return "Card error"; // Message 06 + case EMV_OUTCOME_CARD_BLOCKED: return "Card blocked"; // Not in EMV specification + case EMV_OUTCOME_NOT_ACCEPTED: return "Not accepted"; // Message 0C } return "Invalid outcome"; @@ -276,3 +281,59 @@ int emv_atr_parse(const void* atr, size_t atr_len) return 0; } + +int emv_build_candidate_list( + struct emv_ttl_t* ttl, + const struct emv_tlv_list_t* supported_aids, + struct emv_app_list_t* app_list +) +{ + int r; + + if (!ttl || !supported_aids || !app_list) { + emv_debug_trace_msg("ttl=%p, supported_aids=%p, app_list=%p", ttl, supported_aids, app_list); + emv_debug_error("Invalid parameter"); + return EMV_ERROR_INVALID_PARAMETER; + } + + emv_debug_info("SELECT Payment System Environment (PSE)"); + r = emv_tal_read_pse(ttl, supported_aids, app_list); + if (r < 0) { + emv_debug_trace_msg("emv_tal_read_pse() failed; r=%d", r); + emv_debug_error("Failed to read PSE; terminate session"); + if (r == EMV_TAL_ERROR_CARD_BLOCKED) { + return EMV_OUTCOME_CARD_BLOCKED; + } else { + return EMV_OUTCOME_CARD_ERROR; + } + } + if (r > 0) { + emv_debug_trace_msg("emv_tal_read_pse() failed; r=%d", r); + emv_debug_info("Failed to process PSE; continue session"); + } + + // If PSE failed or no apps found by PSE, use list of AIDs method + // See EMV 4.4 Book 1, 12.3.2, step 5 + if (emv_app_list_is_empty(app_list)) { + emv_debug_info("Discover list of AIDs"); + r = emv_tal_find_supported_apps(ttl, supported_aids, app_list); + if (r) { + emv_debug_trace_msg("emv_tal_find_supported_apps() failed; r=%d", r); + emv_debug_error("Failed to find supported AIDs; terminate session"); + if (r == EMV_TAL_ERROR_CARD_BLOCKED) { + return EMV_OUTCOME_CARD_BLOCKED; + } else { + return EMV_OUTCOME_CARD_ERROR; + } + } + } + + // If there are no mutually supported applications, terminate session + // See EMV 4.4 Book 1, 12.4, step 1 + if (emv_app_list_is_empty(app_list)) { + emv_debug_info("Candidate list empty"); + return EMV_OUTCOME_NOT_ACCEPTED; + } + + return 0; +} diff --git a/src/emv.h b/src/emv.h index 488a8bd..a230396 100644 --- a/src/emv.h +++ b/src/emv.h @@ -2,7 +2,7 @@ * @file emv.h * @brief High level EMV library interface * - * Copyright (c) 2023 Leon Lynch + * Copyright (c) 2023-2024 Leon Lynch * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -27,6 +27,11 @@ __BEGIN_DECLS +// Forward declarations +struct emv_ttl_t; +struct emv_tlv_list_t; +struct emv_app_list_t; + /** * EMV errors * These are typically for internal errors and errors caused by invalid use of @@ -44,6 +49,8 @@ enum emv_error_t { */ enum emv_outcome_t { EMV_OUTCOME_CARD_ERROR = 1, ///< Malfunction of the card or non-conformance to Answer To Reset (ATR) + EMV_OUTCOME_CARD_BLOCKED = 2, ///< Card blocked + EMV_OUTCOME_NOT_ACCEPTED = 3, ///< Card not accepted or no supported applications }; /** @@ -83,6 +90,25 @@ const char* emv_outcome_get_string(enum emv_outcome_t outcome); */ int emv_atr_parse(const void* atr, size_t atr_len); +/** + * Build candidate application list using Payment System Environment (PSE) or + * discovery of supported AIDs + * @remark See EMV 4.4 Book 1, 12.3 + * + * @param ttl EMV Terminal Transport Layer context + * @param supported_aids Supported AID (field 9F06) list including ASI flags + * @param app_list Candidate application list output + * + * @return Zero for success + * @return Less than zero for errors. See @ref emv_error_t + * @return Greater than zero for EMV processing outcome. See @ref emv_outcome_t + */ +int emv_build_candidate_list( + struct emv_ttl_t* ttl, + const struct emv_tlv_list_t* supported_aids, + struct emv_app_list_t* app_list +); + __END_DECLS #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9819192..dc7b721 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,10 @@ if (BUILD_TESTING) target_link_libraries(emv_cvmlist_test PRIVATE emv) add_test(emv_cvmlist_test emv_cvmlist_test) + add_executable(emv_build_candidate_list_test emv_build_candidate_list_test.c emv_cardreader_emul.c) + target_link_libraries(emv_build_candidate_list_test PRIVATE print_helpers emv) + add_test(emv_build_candidate_list_test emv_build_candidate_list_test) + add_executable(isocodes_test isocodes_test.c) find_package(Intl) if(Intl_FOUND) diff --git a/tests/emv_build_candidate_list_test.c b/tests/emv_build_candidate_list_test.c new file mode 100644 index 0000000..aa6836c --- /dev/null +++ b/tests/emv_build_candidate_list_test.c @@ -0,0 +1,551 @@ +/** + * @file emv_build_candidate_list_test.c + * @brief Unit tests for EMV PSE processing and AID discovery + * + * Copyright (c) 2024 Leon Lynch + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + */ + +#include "emv.h" +#include "emv_cardreader_emul.h" +#include "emv_ttl.h" +#include "emv_tlv.h" +#include "emv_app.h" +#include "emv_tags.h" +#include "emv_fields.h" + +#include + +// For debug output +#include "emv_debug.h" +#include "print_helpers.h" + +static const struct xpdu_t test_pse_card_blocked[] = { + { + 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, 0x81 }, // Function not supported + }, + { 0 } +}; + +static const struct xpdu_t test_aid_card_blocked[] = { + { + 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, 0x81 }, // Function not supported + }, + { 0 } +}; + +static const struct xpdu_t test_nothing_found[] = { + { + 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 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_pse_blocked[] = { + { + 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[]){ 0x62, 0x83 }, // Selected file deactivated + }, + { + 5, (uint8_t[]){ 0x00, 0xC0, 0x00, 0x00, 0x00 }, // GET RESPONSE + 2, (uint8_t[]){ 0x6C, 0x1A }, // 36 bytes available + }, + { + 5, (uint8_t[]){ 0x00, 0xC0, 0x00, 0x00, 0x1A }, // GET RESPONSE Le=36 + 36, (uint8_t[]){ 0x6F, 0x20, 0x84, 0x0E, 0x31, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0xA5, 0x0E, 0x88, 0x01, 0x01, 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0x9F, 0x11, 0x01, 0x01, 0x90, 0x00 }, // FCI + }, + { + 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 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_aid_blocked[] = { + { + 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[]){ 0x62, 0x83 }, // Selected file deactivated + }, + { + 5, (uint8_t[]){ 0x00, 0xC0, 0x00, 0x00, 0x00 }, // GET RESPONSE + 2, (uint8_t[]){ 0x6C, 0x33 }, // 51 bytes available + }, + { + 5, (uint8_t[]){ 0x00, 0xC0, 0x00, 0x00, 0x33 }, // GET RESPONSE Le=51 + 51, (uint8_t[]){ 0x6F, 0x2F, 0x84, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x20, 0xA5, 0x24, 0x50, 0x05, 0x56, 0x20, 0x50, 0x41, 0x59, 0x87, 0x01, 0x01, 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0xBF, 0x0C, 0x10, 0x9F, 0x4D, 0x02, 0x0B, 0x05, 0x9F, 0x0A, 0x08, 0x00, 0x01, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00 }, // FCI + }, + { + 12, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x06, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00 }, // SELECT 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_pse_app_not_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 + 36, (uint8_t[]){ 0x6F, 0x20, 0x84, 0x0E, 0x31, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0xA5, 0x0E, 0x88, 0x01, 0x01, 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0x9F, 0x11, 0x01, 0x01, 0x90, 0x00 }, // FCI + }, + { + 5, (uint8_t[]){ 0x00, 0xB2, 0x01, 0x0C, 0x00 }, // READ RECORD 1,1 + 45, (uint8_t[]){ 0x70, 0x29, 0x61, 0x27, 0x4F, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x30, 0x30, 0x50, 0x0B, 0x56, 0x49, 0x53, 0x41, 0x20, 0x43, 0x52, 0x45, 0x44, 0x49, 0x54, 0x87, 0x01, 0x01, 0x9F, 0x12, 0x0B, 0x56, 0x49, 0x53, 0x41, 0x20, 0x43, 0x52, 0x45, 0x44, 0x49, 0x54, 0x90, 0x00 }, // AEF + }, + { + 5, (uint8_t[]){ 0x00, 0xB2, 0x02, 0x0C, 0x00 }, // READ RECORD 1,2 + 2, (uint8_t[]){ 0x6A, 0x83 }, // Record 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 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_pse_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 + 36, (uint8_t[]){ 0x6F, 0x20, 0x84, 0x0E, 0x31, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0xA5, 0x0E, 0x88, 0x01, 0x01, 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0x9F, 0x11, 0x01, 0x01, 0x90, 0x00 }, // FCI + }, + { + 5, (uint8_t[]){ 0x00, 0xB2, 0x01, 0x0C, 0x00 }, // READ RECORD 1,1 + 45, (uint8_t[]){ 0x70, 0x29, 0x61, 0x27, 0x4F, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x10, 0x10, 0x50, 0x0B, 0x56, 0x49, 0x53, 0x41, 0x20, 0x43, 0x52, 0x45, 0x44, 0x49, 0x54, 0x87, 0x01, 0x01, 0x9F, 0x12, 0x0B, 0x56, 0x49, 0x53, 0x41, 0x20, 0x43, 0x52, 0x45, 0x44, 0x49, 0x54, 0x90, 0x00 }, // AEF + }, + { + 5, (uint8_t[]){ 0x00, 0xB2, 0x02, 0x0C, 0x00 }, // READ RECORD 1,2 + 2, (uint8_t[]){ 0x6A, 0x83 }, // Record not found + }, + { 0 } +}; + +static const struct xpdu_t test_pse_multi_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 + 36, (uint8_t[]){ 0x6F, 0x20, 0x84, 0x0E, 0x31, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0xA5, 0x0E, 0x88, 0x01, 0x01, 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0x9F, 0x11, 0x01, 0x01, 0x90, 0x00 }, // FCI + }, + { + 5, (uint8_t[]){ 0x00, 0xB2, 0x01, 0x0C, 0x00 }, // READ RECORD 1,1 + 72, (uint8_t[]){ 0x70, 0x44, 0x61, 0x20, 0x4F, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x20, 0x50, 0x05, 0x56, 0x20, 0x50, 0x41, 0x59, 0x87, 0x01, 0x01, 0x73, 0x0B, 0x9F, 0x0A, 0x08, 0x00, 0x01, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x61, 0x20, 0x4F, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x10, 0x50, 0x05, 0x56, 0x20, 0x50, 0x41, 0x59, 0x87, 0x01, 0x02, 0x73, 0x0B, 0x9F, 0x0A, 0x08, 0x00, 0x01, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00 }, // AEF + }, + { + 5, (uint8_t[]){ 0x00, 0xB2, 0x02, 0x0C, 0x00 }, // READ RECORD 1,2 + 2, (uint8_t[]){ 0x6A, 0x83 }, // Record not found + }, + { 0 } +}; + +static const struct xpdu_t test_aid_multi_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 + 51, (uint8_t[]){ 0x6F, 0x2F, 0x84, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x10, 0xA5, 0x24, 0x50, 0x05, 0x56, 0x20, 0x50, 0x41, 0x59, 0x87, 0x01, 0x02, 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0xBF, 0x0C, 0x10, 0x9F, 0x4D, 0x02, 0x0B, 0x05, 0x9F, 0x0A, 0x08, 0x00, 0x01, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00 }, // FCI + }, + { + 13, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x20, 0x00 }, // SELECT A0000000032020 + 51, (uint8_t[]){ 0x6F, 0x2F, 0x84, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x20, 0xA5, 0x24, 0x50, 0x05, 0x56, 0x20, 0x50, 0x41, 0x59, 0x87, 0x01, 0x01, 0x5F, 0x2D, 0x04, 0x6E, 0x6C, 0x65, 0x6E, 0xBF, 0x0C, 0x10, 0x9F, 0x4D, 0x02, 0x0B, 0x05, 0x9F, 0x0A, 0x08, 0x00, 0x01, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00 }, // FCI + }, + { + 12, (uint8_t[]){ 0x00, 0xA4, 0x04, 0x00, 0x06, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00 }, // SELECT 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 } +}; + +int main(void) +{ + int r; + struct emv_cardreader_emul_ctx_t emul_ctx; + struct emv_ttl_t ttl; + struct emv_tlv_list_t supported_aids = EMV_TLV_LIST_INIT; + struct emv_app_list_t app_list = EMV_APP_LIST_INIT; + + ttl.cardreader.mode = EMV_CARDREADER_MODE_APDU; + ttl.cardreader.ctx = &emul_ctx; + ttl.cardreader.trx = &emv_cardreader_emul; + + // Supported applications + emv_tlv_list_push(&supported_aids, EMV_TAG_9F06_AID, 6, (uint8_t[]){ 0xA0, 0x00, 0x00, 0x00, 0x03, 0x10 }, EMV_ASI_PARTIAL_MATCH); // Visa + emv_tlv_list_push(&supported_aids, EMV_TAG_9F06_AID, 7, (uint8_t[]){ 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x10 }, EMV_ASI_EXACT_MATCH); // Visa Electron + emv_tlv_list_push(&supported_aids, EMV_TAG_9F06_AID, 7, (uint8_t[]){ 0xA0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x20 }, EMV_ASI_EXACT_MATCH); // V Pay + emv_tlv_list_push(&supported_aids, EMV_TAG_9F06_AID, 6, (uint8_t[]){ 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10 }, EMV_ASI_PARTIAL_MATCH); // Mastercard + emv_tlv_list_push(&supported_aids, EMV_TAG_9F06_AID, 6, (uint8_t[]){ 0xA0, 0x00, 0x00, 0x00, 0x04, 0x30 }, EMV_ASI_PARTIAL_MATCH); // Maestro + + r = emv_debug_init( + EMV_DEBUG_SOURCE_ALL, + EMV_DEBUG_CARD, + &print_emv_debug + ); + if (r) { + printf("Failed to initialise EMV debugging\n"); + return 1; + } + + printf("\nTesting PSE card blocked or SELECT not supported...\n"); + emul_ctx.xpdu_list = test_pse_card_blocked; + emul_ctx.xpdu_current = NULL; + emv_app_list_clear(&app_list); + r = emv_build_candidate_list( + &ttl, + &supported_aids, + &app_list + ); + if (r != EMV_OUTCOME_CARD_BLOCKED) { + 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 NOT empty\n"); + for (struct emv_app_t* app = app_list.front; app != NULL; app = app->next) { + print_emv_app(app); + } + r = 1; + goto exit; + } + printf("Success\n"); + + printf("\nTesting PSE not found and AID card blocked or SELECT not supported...\n"); + emul_ctx.xpdu_list = test_aid_card_blocked; + emul_ctx.xpdu_current = NULL; + emv_app_list_clear(&app_list); + r = emv_build_candidate_list( + &ttl, + &supported_aids, + &app_list + ); + if (r != EMV_OUTCOME_CARD_BLOCKED) { + 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 NOT empty\n"); + for (struct emv_app_t* app = app_list.front; app != NULL; app = app->next) { + print_emv_app(app); + } + r = 1; + goto exit; + } + printf("Success\n"); + + printf("\nTesting PSE not found and no supported applications...\n"); + emul_ctx.xpdu_list = test_nothing_found; + emul_ctx.xpdu_current = NULL; + emv_app_list_clear(&app_list); + r = emv_build_candidate_list( + &ttl, + &supported_aids, + &app_list + ); + if (r != EMV_OUTCOME_NOT_ACCEPTED) { + 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 NOT empty\n"); + for (struct emv_app_t* app = app_list.front; app != NULL; app = app->next) { + print_emv_app(app); + } + r = 1; + goto exit; + } + printf("Success\n"); + + printf("\nTesting PSE blocked and no supported applications...\n"); + emul_ctx.xpdu_list = test_pse_blocked; + emul_ctx.xpdu_current = NULL; + emv_app_list_clear(&app_list); + r = emv_build_candidate_list( + &ttl, + &supported_aids, + &app_list + ); + if (r != EMV_OUTCOME_NOT_ACCEPTED) { + 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 NOT empty\n"); + for (struct emv_app_t* app = app_list.front; app != NULL; app = app->next) { + print_emv_app(app); + } + r = 1; + goto exit; + } + printf("Success\n"); + + printf("\nTesting PSE not found and AID blocked...\n"); + emul_ctx.xpdu_list = test_aid_blocked; + emul_ctx.xpdu_current = NULL; + emv_app_list_clear(&app_list); + r = emv_build_candidate_list( + &ttl, + &supported_aids, + &app_list + ); + if (r != EMV_OUTCOME_NOT_ACCEPTED) { + 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 NOT empty\n"); + for (struct emv_app_t* app = app_list.front; app != NULL; app = app->next) { + print_emv_app(app); + } + r = 1; + goto exit; + } + printf("Success\n"); + + printf("\nTesting PSE app not supported...\n"); + emul_ctx.xpdu_list = test_pse_app_not_supported; + emul_ctx.xpdu_current = NULL; + emv_app_list_clear(&app_list); + r = emv_build_candidate_list( + &ttl, + &supported_aids, + &app_list + ); + if (r != EMV_OUTCOME_NOT_ACCEPTED) { + 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 NOT empty\n"); + for (struct emv_app_t* app = app_list.front; app != NULL; app = app->next) { + print_emv_app(app); + } + r = 1; + goto exit; + } + printf("Success\n"); + + printf("\nTesting PSE app supported...\n"); + emul_ctx.xpdu_list = test_pse_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); + } + printf("Success\n"); + + printf("\nTesting PSE multiple apps supported...\n"); + emul_ctx.xpdu_list = test_pse_multi_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); + } + printf("Success\n"); + + printf("\nTesting PSE not found and multiple AIDs supported...\n"); + emul_ctx.xpdu_list = test_aid_multi_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); + } + printf("Success\n"); + + // Success + r = 0; + goto exit; + +exit: + emv_tlv_list_clear(&supported_aids); + emv_app_list_clear(&app_list); + + return r; +} diff --git a/tests/emv_cardreader_emul.c b/tests/emv_cardreader_emul.c new file mode 100644 index 0000000..f1251d1 --- /dev/null +++ b/tests/emv_cardreader_emul.c @@ -0,0 +1,69 @@ +/** + * @file emv_cardreader_emul.c + * @brief Basic card reader emulation for unit tests + * + * Copyright (c) 2024 Leon Lynch + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * . + */ + +#include "emv_cardreader_emul.h" + +#include +#include +#include + +int emv_cardreader_emul( + void* ctx, + const void* tx_buf, + size_t tx_buf_len, + void* rx_buf, + size_t* rx_buf_len +) { + struct emv_cardreader_emul_ctx_t* emul_ctx = ctx; + const struct xpdu_t* xpdu; + + // NOTE: This function calls exit() on error to ensure that they are + // detected because the TTL interprets the failure of this function as a + // hardware or card error. + + if (!emul_ctx->xpdu_current) { + emul_ctx->xpdu_current = emul_ctx->xpdu_list; + } + if (!emul_ctx->xpdu_current->c_xpdu_len) { + fprintf(stderr, "Invalid transmission\n"); + exit(1); + return -101; + } + xpdu = emul_ctx->xpdu_current; + + if (tx_buf_len != xpdu->c_xpdu_len) { + fprintf(stderr, "Incorrect transmit length\n"); + exit(1); + return -102; + } + if (memcmp(tx_buf, xpdu->c_xpdu, xpdu->c_xpdu_len) != 0) { + fprintf(stderr, "Incorrect transmit data\n"); + exit(1); + return -103; + } + + memcpy(rx_buf, xpdu->r_xpdu, xpdu->r_xpdu_len); + *rx_buf_len = xpdu->r_xpdu_len; + + emul_ctx->xpdu_current++; + + return 0; +} diff --git a/tests/emv_cardreader_emul.h b/tests/emv_cardreader_emul.h new file mode 100644 index 0000000..d0e0e08 --- /dev/null +++ b/tests/emv_cardreader_emul.h @@ -0,0 +1,59 @@ +/** + * @file emv_cardreader_emul.h + * @brief Basic card reader emulation for unit tests + * + * Copyright (c) 2024 Leon Lynch + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * . + */ + +#ifndef EMV_CARDREADER_EMUL_H +#define EMV_CARDREADER_EMUL_H + +#include +#include + +/// Transport/Application Protocol Data Unit (xPDU) +struct xpdu_t { + size_t c_xpdu_len; + const uint8_t* c_xpdu; + size_t r_xpdu_len; + const uint8_t* r_xpdu; +}; + +/// Card reader emulator context +struct emv_cardreader_emul_ctx_t { + const struct xpdu_t* xpdu_list; + const struct xpdu_t* xpdu_current; +}; + +/** + * Emulate card reader transceive + * + * @param ctx Card reader emulator context + * @param tx_buf Transmit buffer + * @param tx_buf_len Length of transmit buffer in bytes + * @param rx_buf Receive buffer + * @param rx_buf_len Length of receive buffer in bytes + */ +int emv_cardreader_emul( + void* ctx, + const void* tx_buf, + size_t tx_buf_len, + void* rx_buf, + size_t* rx_buf_len +); + +#endif diff --git a/tools/emv-tool.c b/tools/emv-tool.c index 8924d4f..ed56b11 100644 --- a/tools/emv-tool.c +++ b/tools/emv-tool.c @@ -2,7 +2,7 @@ * @file emv-tool.c * @brief Simple EMV processing tool * - * Copyright (c) 2021, 2023 Leon Lynch + * Copyright (c) 2021, 2023-2024 Leon Lynch * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -26,7 +26,7 @@ #include "emv_fields.h" #include "emv_strings.h" #include "emv_tlv.h" -#include "emv_tal.h" +#include "emv_app.h" #define EMV_DEBUG_SOURCE EMV_DEBUG_SOURCE_APP #include "emv_debug.h" @@ -41,7 +41,7 @@ // HACK: remove #include "emv_dol.h" #include "emv_ttl.h" -#include "emv_app.h" +#include "emv_tal.h" #include "iso7816_strings.h" // Forward declarations @@ -525,25 +525,15 @@ int main(int argc, char** argv) // Candidate applications for selection struct emv_app_list_t app_list = EMV_APP_LIST_INIT; - printf("\nSELECT Payment System Environment (PSE)\n"); - r = emv_tal_read_pse(&emv_txn.ttl, &emv_txn.supported_aids, &app_list); + printf("\nBuild candidate list\n"); + r = emv_build_candidate_list(&emv_txn.ttl, &emv_txn.supported_aids, &app_list); if (r < 0) { - printf("Failed to read PSE; terminate session\n"); + printf("ERROR: %s\n", emv_error_get_string(r)); goto emv_exit; } if (r > 0) { - printf("Failed to read PSE; continue session\n"); - } - - // If PSE failed or no apps found by PSE - // See EMV 4.3 Book 1, 12.3.2, step 5 - if (r > 0 || emv_app_list_is_empty(&app_list)) { - printf("\nFind supported AIDs\n"); - r = emv_tal_find_supported_apps(&emv_txn.ttl, &emv_txn.supported_aids, &app_list); - if (r) { - printf("Failed to find supported AIDs; terminate session\n"); - goto emv_exit; - } + printf("OUTCOME: %s\n", emv_outcome_get_string(r)); + goto emv_exit; } if (emv_app_list_is_empty(&app_list)) { @@ -722,6 +712,7 @@ int main(int argc, char** argv) printf("\nCard deactivated\n"); emv_exit: + emv_app_list_clear(&app_list); emv_txn_destroy(&emv_txn); pcsc_exit: pcsc_release(&pcsc);