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);