diff --git a/CHANGELOG.md b/CHANGELOG.md index 84206c270..46d73a48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Added `hf mfd verifycert` command (@kormax) - Added `hf calypso dump` command (@kormax) - Added `pm3trace_edit.py` script for editing of pm3 trace files (@iceman1001) - Added `hf calypso info` command (@kormax) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index cee826793..4774bfe51 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -313,6 +313,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/common/hitag2/hitag2_crypto.c ${PM3_ROOT}/client/src/crypto/asn1dump.c ${PM3_ROOT}/client/src/crypto/asn1utils.c + ${PM3_ROOT}/client/src/crypto/duoxcrypto.c ${PM3_ROOT}/client/src/crypto/libpcrypto.c ${PM3_ROOT}/client/src/crypto/originality.c ${PM3_ROOT}/client/src/emv/test/cda_test.c diff --git a/client/Makefile b/client/Makefile index edb4c830a..a71d9efb8 100644 --- a/client/Makefile +++ b/client/Makefile @@ -770,6 +770,7 @@ SRCS = mifare/aiddesfire.c \ comms.c \ crypto/asn1dump.c \ crypto/asn1utils.c\ + crypto/duoxcrypto.c\ crypto/libpcrypto.c\ crypto/originality.c\ emv/cmdemv.c \ diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index 44426f515..f077e8963 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -233,6 +233,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/common/hitag2/hitag2_crypto.c ${PM3_ROOT}/client/src/crypto/asn1dump.c ${PM3_ROOT}/client/src/crypto/asn1utils.c + ${PM3_ROOT}/client/src/crypto/duoxcrypto.c ${PM3_ROOT}/client/src/crypto/libpcrypto.c ${PM3_ROOT}/client/src/crypto/originality.c ${PM3_ROOT}/client/src/emv/test/cda_test.c diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index f128c5492..42892b87b 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -30,6 +30,7 @@ #include "cmdhf14a.h" #include "aes.h" #include "crypto/libpcrypto.h" +#include "crypto/duoxcrypto.h" #include "protocols.h" #include "cmdtrace.h" #include "cliparser.h" @@ -76,8 +77,6 @@ // DUOX ISO Internal Authenticate #define DUOX_INTAUTH_CHALLENGE_LEN 16 #define DUOX_INTAUTH_SIG_LEN 64 -#define DUOX_VDE_CHALLENGE_LEN 32 -#define DUOX_VDE_SIG_LEN 64 #define DUOX_MAX_KEY_INPUT 8192 // TLV tags for ISO Internal Authenticate #define DUOX_TAG_OPTSA 0x80 @@ -87,15 +86,18 @@ #define DUOX_INTAUTH_MSG_PREFIX 0xF0 // LEAF Verified Open Application -#define LEAF_VERIFIED_DEFAULT_AID 0xF51CD6U // 0xD61CF5 in user-facing (wire bytes D6 1C F5) +#define LEAF_VERIFIED_DEFAULT_AID 0xF51CD6U #define LEAF_VERIFIED_CERT_FILE 0x02 #define LEAF_VERIFIED_MAX_CERT_LEN 4096 #define LEAF_COMMUNITY_ROOT_KEY_PATH "duox_trust/leaf_community/leaf_community-root-public-key.der" +#define DUOX_VDE_DEFAULT_AID 0x1010F6U +#define DUOX_VDE_CERT_FILE 0x00 static const uint8_t kDuoxVDEDefaultDFName[] = { 0xA0, 0x00, 0x00, 0x08, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; +#define MFDES_VERIFYCERT_MAX_CAS DUOX_MAX_CERTIFICATE_ANCHORS static uint8_t saved_mfclicense[192]; static size_t saved_mfclicense_len = 0; @@ -113,6 +115,65 @@ typedef struct mfd_app_select { uint32_t aid; } mfd_app_select; +typedef enum { + MFDES_VALIDATE_METHOD_AUTO = 0, + MFDES_VALIDATE_METHOD_INTAUTH, + MFDES_VALIDATE_METHOD_VDE, + MFDES_VALIDATE_METHOD_SKIP, +} mfdes_validate_method_t; + +typedef enum { + MFDES_READ_METHOD_AUTO = 0, + MFDES_READ_METHOD_DESFIRE, +} mfdes_read_method_t; + +#define MFDES_VERIFYCERT_CERTIFICATE_PROFILE_MAX_DATA 4 +#define MFDES_VERIFYCERT_PROFILE_MAX_NAMES 4 +#define MFDES_VERIFYCERT_PROFILE_MAX_METHODS 3 +#define MFDES_VERIFYCERT_NO_FID 0xFF + +typedef struct { + char label[32]; + char value[DUOX_CERT_TEXT_LEN]; +} mfdes_certificate_profile_value_t; + +typedef struct { + duox_certificate_format_t format; + mbedtls_ecp_group_id curveid; + uint8_t pubkey[DUOX_EC_PUBKEY_MAX_LEN]; + size_t pubkey_len; + char issuer[DUOX_CERT_TEXT_LEN]; + char subject[DUOX_CERT_TEXT_LEN]; + char serial[DUOX_CERT_TEXT_LEN]; + char valid_from[DUOX_CERT_TEXT_LEN]; + char valid_to[DUOX_CERT_TEXT_LEN]; + char certificate_profile_note[DUOX_CERT_TEXT_LEN]; + mfdes_certificate_profile_value_t certificate_profile_data[MFDES_VERIFYCERT_CERTIFICATE_PROFILE_MAX_DATA]; + size_t certificate_profile_data_count; +} mfdes_verified_cert_t; + +typedef enum { + MFDES_VERIFYCERT_CERTIFICATE_PROFILE_DATA_SUBJECT_DN = 0, + MFDES_VERIFYCERT_CERTIFICATE_PROFILE_DATA_ISSUER_DN, +} mfdes_certificate_profile_data_source_t; + +typedef struct { + const char *label; + mfdes_certificate_profile_data_source_t source; + const char *dn_component; +} mfdes_certificate_profile_data_t; + +typedef struct { + const char *names[MFDES_VERIFYCERT_PROFILE_MAX_NAMES]; + mfd_app_select cert_select; + bool key_select_present; + mfd_app_select key_select; + uint8_t fid; + uint8_t keyidx; + mfdes_validate_method_t validate_methods[MFDES_VERIFYCERT_PROFILE_MAX_METHODS]; + const mfdes_certificate_profile_data_t *certificate_profile_data; +} mfdes_verifycert_profile_t; + #define status(x) ( ((uint16_t)(0x91 << 8)) + (uint16_t)x ) /* static uint8_t desdefaultkeys[3][8] = {{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, //Official @@ -7790,17 +7851,35 @@ static void MfdSelectionPrint(const mfd_app_select *select) { } } -static int MfdSelectionSelectApplication(DesfireContext_t *dctx, const mfd_app_select *select, bool verbose) { +static void MfdSelectionPrintPrefixed(const mfd_app_select *select, const char *prefix) { + if (select == NULL) { + return; + } + if (prefix == NULL) { + prefix = ""; + } + if (select->dfname_present) { + PrintAndLogEx(INFO, "%sDF name...........: " _YELLOW_("%s"), prefix, sprint_hex_inrow(select->dfname, select->dfname_len)); + } + if (select->isoid_present) { + PrintAndLogEx(INFO, "%sISO DF ID.........: " _YELLOW_("%04X"), prefix, select->isoid); + } + if (select->aid_present) { + uint8_t aid_bytes[3] = {0}; + DesfireAIDUintToByte(select->aid, aid_bytes); + PrintAndLogEx(INFO, "%sAID...............: " _YELLOW_("%02X%02X%02X"), prefix, aid_bytes[0], aid_bytes[1], aid_bytes[2]); + } +} + +static int MfdSelectionSelectApplicationEx(DesfireContext_t *dctx, const mfd_app_select *select, bool verbose, bool fieldon) { if (dctx == NULL || MfdSelectionHasAny(select) == false) { return PM3_EINVARG; } - bool fieldon = true; - if (select->dfname_present) { uint8_t resp[250] = {0}; size_t resplen = 0; - if (DesfireISOSelect(dctx, ISSDFName, (uint8_t *)select->dfname, select->dfname_len, resp, &resplen) != PM3_SUCCESS) { + if (DesfireISOSelectEx(dctx, fieldon, ISSDFName, (uint8_t *)select->dfname, select->dfname_len, resp, &resplen) != PM3_SUCCESS) { PrintAndLogEx(ERR, "Select DF name " _RED_("failed")); return PM3_ESOFT; } @@ -7817,7 +7896,10 @@ static int MfdSelectionSelectApplication(DesfireContext_t *dctx, const mfd_app_s if (select->aid_present) { if (DesfireSelectEx(dctx, fieldon, ISW6bAID, select->aid, NULL) != PM3_SUCCESS) { - PrintAndLogEx(ERR, "Select application %06X " _RED_("failed"), select->aid); + uint8_t aid_bytes[3] = {0}; + DesfireAIDUintToByte(select->aid, aid_bytes); + PrintAndLogEx(ERR, "Select application %02X%02X%02X " _RED_("failed"), + aid_bytes[0], aid_bytes[1], aid_bytes[2]); return PM3_ESOFT; } } @@ -7828,8 +7910,12 @@ static int MfdSelectionSelectApplication(DesfireContext_t *dctx, const mfd_app_s return PM3_SUCCESS; } +static int MfdSelectionSelectApplication(DesfireContext_t *dctx, const mfd_app_select *select, bool verbose) { + return MfdSelectionSelectApplicationEx(dctx, select, verbose, true); +} + // Build and send ISO Internal Authenticate (INS 88), then parse the TLV response. -// Assumes the application is already selected (field on). Calls DropField before returning. +// Assumes the application is already selected. // On PM3_SUCCESS: out_card_random[DUOX_INTAUTH_CHALLENGE_LEN] and out_sig_rs[DUOX_INTAUTH_SIG_LEN] are filled. static int duox_intauth_exchange(bool apdu_logging, bool verbose, uint8_t keynum, const uint8_t *challenge, @@ -7861,7 +7947,6 @@ static int duox_intauth_exchange(bool apdu_logging, bool verbose, uint8_t keynum int encoded_len = 0; if (APDUEncodeS(&apdu, false, APDU_INCLUDE_LE_00, encoded, &encoded_len)) { PrintAndLogEx(ERR, "APDU encoding error"); - DropField(); return PM3_ESOFT; } @@ -7873,15 +7958,12 @@ static int duox_intauth_exchange(bool apdu_logging, bool verbose, uint8_t keynum int res = ExchangeAPDU14a(encoded, encoded_len, false, true, response, sizeof(response), &resplen); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "APDU exchange " _RED_("failed") " (%d)", res); - DropField(); return res; } if (apdu_logging) PrintAndLogEx(SUCCESS, "<<<< %s", sprint_hex(response, resplen)); - DropField(); - // Check status word if (resplen < 2) { PrintAndLogEx(ERR, "Response too short"); @@ -8090,6 +8172,7 @@ static int CmdHF14ADesIntAuth(const char *Cmd) { uint8_t card_random[DUOX_INTAUTH_CHALLENGE_LEN] = {0}; uint8_t signature_rs[DUOX_INTAUTH_SIG_LEN] = {0}; int res = duox_intauth_exchange(APDULogging, verbose, (uint8_t)keynum, challenge, card_random, signature_rs); + DropField(); if (res != PM3_SUCCESS) return res; @@ -8460,6 +8543,7 @@ static int CmdHF14ADesLeaf(const char *Cmd) { uint8_t card_random[DUOX_INTAUTH_CHALLENGE_LEN] = {0}; uint8_t signature_rs[DUOX_INTAUTH_SIG_LEN] = {0}; res = duox_intauth_exchange(APDULogging, verbose, (uint8_t)keynum, challenge, card_random, signature_rs); + DropField(); if (res != PM3_SUCCESS) return res; @@ -8487,6 +8571,994 @@ static int CmdHF14ADesLeaf(const char *Cmd) { return (root_ok && card_ok) ? PM3_SUCCESS : PM3_ESOFT; } +static const CLIParserOption mfdesValidateMethodOpts[] = { + {MFDES_VALIDATE_METHOD_AUTO, "auto"}, + {MFDES_VALIDATE_METHOD_INTAUTH, "intauth"}, + {MFDES_VALIDATE_METHOD_INTAUTH, "internal-auth"}, + {MFDES_VALIDATE_METHOD_INTAUTH, "internal"}, + {MFDES_VALIDATE_METHOD_VDE, "vde"}, + {MFDES_VALIDATE_METHOD_VDE, "vdesign"}, + {MFDES_VALIDATE_METHOD_VDE, "vde-sign"}, + {MFDES_VALIDATE_METHOD_SKIP, "skip"}, + {MFDES_VALIDATE_METHOD_SKIP, "none"}, + {-1, NULL}, +}; + +static const CLIParserOption mfdesReadMethodOpts[] = { + {MFDES_READ_METHOD_AUTO, "auto"}, + {MFDES_READ_METHOD_DESFIRE, "desfire"}, + {-1, NULL}, +}; + +static const mfdes_certificate_profile_data_t kMfdesVerifyCertLeafCertificateProfileData[] = { + { + .label = "Open ID", + .source = MFDES_VERIFYCERT_CERTIFICATE_PROFILE_DATA_SUBJECT_DN, + .dn_component = "serialNumber", + }, + {0}, +}; + +static const mfdes_verifycert_profile_t kMfdesVerifyCertProfiles[] = { + { + .names = {"generic"}, + .cert_select = {.aid_present = true, .aid = 0x000000}, + .fid = MFDES_VERIFYCERT_NO_FID, + .keyidx = 0x00, + .validate_methods = {MFDES_VALIDATE_METHOD_INTAUTH, MFDES_VALIDATE_METHOD_VDE}, + }, + { + .names = {"leaf"}, + .cert_select = {.aid_present = true, .aid = LEAF_VERIFIED_DEFAULT_AID}, + .fid = LEAF_VERIFIED_CERT_FILE, + .keyidx = 0x00, + .validate_methods = {MFDES_VALIDATE_METHOD_INTAUTH}, + .certificate_profile_data = kMfdesVerifyCertLeafCertificateProfileData, + }, + { + .names = {"vde"}, + .cert_select = {.aid_present = true, .aid = DUOX_VDE_DEFAULT_AID}, + .fid = DUOX_VDE_CERT_FILE, + .keyidx = 0x00, + .validate_methods = {MFDES_VALIDATE_METHOD_VDE, MFDES_VALIDATE_METHOD_INTAUTH}, + }, + { + .names = {"orig", "originality"}, + .cert_select = {.aid_present = true, .aid = 0x000000}, + .fid = 0x01, + .keyidx = 0x01, + .validate_methods = {MFDES_VALIDATE_METHOD_INTAUTH, MFDES_VALIDATE_METHOD_VDE}, + }, + { + .names = {"picc"}, + .cert_select = {.aid_present = true, .aid = 0x000000}, + .fid = 0x00, + .keyidx = 0x00, + .validate_methods = {MFDES_VALIDATE_METHOD_SKIP}, + }, +}; + +static const mfdes_verifycert_profile_t *mfdes_find_verifycert_profile(const char *value) { + if (value == NULL || value[0] == '\0') { + return &kMfdesVerifyCertProfiles[0]; + } + + for (size_t i = 0; i < ARRAYLEN(kMfdesVerifyCertProfiles); i++) { + const mfdes_verifycert_profile_t *profile = &kMfdesVerifyCertProfiles[i]; + for (size_t n = 0; n < ARRAYLEN(profile->names); n++) { + if (profile->names[n] != NULL && str_equal_case_insensitive(value, profile->names[n])) { + return profile; + } + } + } + return NULL; +} + +static int mfdes_prepare_data_file_read(DesfireContext_t *dctx, uint8_t fnum, bool noauth, bool verbose, bool *auth_needed) { + if (dctx == NULL) { + return PM3_EINVARG; + } + if (auth_needed != NULL) { + *auth_needed = false; + } + + dctx->isoChaining |= (dctx->secureChannel == DACLRP); + + FileSettings_t fsettings = {0}; + DesfireCommunicationMode comm_mode = dctx->commMode; + DesfireSetCommMode(dctx, DCMMACed); + int res = DesfireFileSettingsStruct(dctx, fnum, &fsettings); + DesfireSetCommMode(dctx, comm_mode); + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "GetFileSettings error. Reading certificate file with current communication mode."); + return PM3_SUCCESS; + } + + if (fsettings.fileType != 0x00 && fsettings.fileType != 0x01) { + PrintAndLogEx(ERR, "Certificate file must be a standard or backup data file, got %s", + GetDesfireFileType(fsettings.fileType)); + return PM3_EINVARG; + } + + bool read_free = (fsettings.rAccess == 0x0e || fsettings.rwAccess == 0x0e); + bool read_denied = (fsettings.rAccess == 0x0f && fsettings.rwAccess == 0x0f); + bool read_needs_auth = (!read_free && !read_denied && (fsettings.rAccess < 0x0e || fsettings.rwAccess < 0x0e)); + + comm_mode = fsettings.commMode; + if (read_free && !DesfireIsAuthenticated(dctx)) { + comm_mode = DCMPlain; + } + DesfireSetCommMode(dctx, comm_mode); + + if (fsettings.fileCommMode != 0 && noauth && !read_free) { + PrintAndLogEx(WARNING, "File needs communication mode `%s` but there is no authentication", + CLIGetOptionListStr(DesfireCommunicationModeOpts, fsettings.commMode)); + } + if (auth_needed != NULL && read_needs_auth && !DesfireIsAuthenticated(dctx)) { + *auth_needed = true; + } + if ((fsettings.rAccess < 0x0e && fsettings.rAccess != dctx->keyNum) && + (fsettings.rwAccess < 0x0e && fsettings.rwAccess != dctx->keyNum)) { + PrintAndLogEx(WARNING, "File needs to be authenticated with key 0x%02x or 0x%02x but current authentication key is 0x%02x", + fsettings.rAccess, fsettings.rwAccess, dctx->keyNum); + } + if (fsettings.rAccess == 0x0f && fsettings.rwAccess == 0x0f) { + PrintAndLogEx(WARNING, "File access denied. All read access rights is 0x0F"); + } + + if (verbose) { + PrintAndLogEx(INFO, _CYAN_("Certificate file type:") " %s comm mode: %s", + GetDesfireFileType(fsettings.fileType), + CLIGetOptionListStr(DesfireCommunicationModeOpts, fsettings.commMode)); + } + return PM3_SUCCESS; +} + +static int mfdes_read_cert_file_once(const DesfireContext_t *base_dctx, DesfireSecureChannel securechann, + uint8_t fid, bool authenticate, bool verbose, + uint8_t *cert_buf, size_t *cert_buf_len, bool *auth_needed) { + if (base_dctx == NULL || cert_buf == NULL || cert_buf_len == NULL) { + return PM3_EINVARG; + } + + DesfireContext_t dctx = *base_dctx; + int res = PM3_SUCCESS; + + if (authenticate) { + res = DesfireAuthenticate(&dctx, securechann, verbose); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Desfire authenticate " _RED_("error") ". Result: [%d] %s", res, DesfireAuthErrorToStr(res)); + return res; + } + if (!DesfireIsAuthenticated(&dctx)) { + return 201; + } + } + + res = mfdes_prepare_data_file_read(&dctx, fid, !authenticate, verbose, auth_needed); + if (res != PM3_SUCCESS) { + return res; + } + + *cert_buf_len = 0; + res = DesfireReadFile(&dctx, fid, 0, 0, cert_buf, cert_buf_len); + return res; +} + +static int mfdes_read_cert_file_desfire(const DesfireContext_t *base_dctx, DesfireSecureChannel securechann, + uint8_t fid, bool noauth, bool force_auth, bool verbose, + uint8_t *cert_buf, size_t *cert_buf_len) { + if (base_dctx == NULL || cert_buf == NULL || cert_buf_len == NULL) { + return PM3_EINVARG; + } + + bool auth_needed = false; + int res = PM3_SUCCESS; + if (noauth || !force_auth) { + res = mfdes_read_cert_file_once(base_dctx, securechann, fid, false, verbose, + cert_buf, cert_buf_len, &auth_needed); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + if (noauth) { + return res; + } + if (auth_needed) { + PrintAndLogEx(INFO, "File settings indicate authentication is needed; retrying with authentication"); + } else { + PrintAndLogEx(WARNING, "Unauthenticated ReadFile failed (%d); retrying with authentication", res); + } + } + + res = mfdes_read_cert_file_once(base_dctx, securechann, fid, true, verbose, + cert_buf, cert_buf_len, NULL); + return res; +} + +static void mfdes_print_certificate_info(const mfdes_verified_cert_t *cert_info) { + if (cert_info == NULL) { + return; + } + + PrintAndLogEx(INFO, _CYAN_("Certificate")); + PrintAndLogEx(INFO, " format............: " _YELLOW_("%s"), duox_certificate_format_name(cert_info->format)); + PrintAndLogEx(INFO, " issuer............: " _YELLOW_("%s"), (cert_info->issuer[0] != '\0') ? cert_info->issuer : "n/a"); + PrintAndLogEx(INFO, " subject...........: " _YELLOW_("%s"), (cert_info->subject[0] != '\0') ? cert_info->subject : "n/a"); + PrintAndLogEx(INFO, " serial number.....: " _YELLOW_("%s"), (cert_info->serial[0] != '\0') ? cert_info->serial : "n/a"); + PrintAndLogEx(INFO, " valid from........: " _YELLOW_("%s"), (cert_info->valid_from[0] != '\0') ? cert_info->valid_from : "n/a"); + PrintAndLogEx(INFO, " valid to..........: " _YELLOW_("%s"), (cert_info->valid_to[0] != '\0') ? cert_info->valid_to : "n/a"); + if (cert_info->certificate_profile_note[0] != '\0') { + PrintAndLogEx(INFO, " profile data......: " _YELLOW_("%s"), cert_info->certificate_profile_note); + } + for (size_t i = 0; i < cert_info->certificate_profile_data_count && i < ARRAYLEN(cert_info->certificate_profile_data); i++) { + const mfdes_certificate_profile_value_t *item = &cert_info->certificate_profile_data[i]; + if (item->label[0] != '\0' && item->value[0] != '\0') { + PrintAndLogEx(INFO, " profile data......: " _YELLOW_("%s %s"), item->label, item->value); + } + } + PrintAndLogEx(INFO, " public key........: " _YELLOW_("%s"), (cert_info->pubkey_len > 0) ? sprint_hex_inrow(cert_info->pubkey, cert_info->pubkey_len) : "n/a"); +} + +static void mfdes_cert_info_from_duox(const duox_cert_info_t *src, mfdes_verified_cert_t *dst) { + if (src == NULL || dst == NULL) { + return; + } + + memset(dst, 0, sizeof(*dst)); + dst->format = src->format; + dst->curveid = src->curveid; + dst->pubkey_len = MIN(src->pubkey_len, sizeof(dst->pubkey)); + if (dst->pubkey_len > 0) { + memcpy(dst->pubkey, src->pubkey, dst->pubkey_len); + } + snprintf(dst->issuer, sizeof(dst->issuer), "%s", src->issuer); + snprintf(dst->subject, sizeof(dst->subject), "%s", src->subject); + snprintf(dst->serial, sizeof(dst->serial), "%s", src->serial); + snprintf(dst->valid_from, sizeof(dst->valid_from), "%s", src->valid_from); + snprintf(dst->valid_to, sizeof(dst->valid_to), "%s", src->valid_to); + snprintf(dst->certificate_profile_note, sizeof(dst->certificate_profile_note), "%s", src->certificate_profile_note); +} + +static bool mfdes_extract_dn_component(const char *dn, const char *key, char *out, size_t out_len) { + if (dn == NULL || key == NULL || out == NULL || out_len == 0) { + return false; + } + + out[0] = '\0'; + const char *p = strstr(dn, key); + if (p == NULL) { + return false; + } + p += strlen(key); + while (*p == ' ' || *p == '=') { + p++; + } + if (*p == '"') { + p++; + } + + size_t n = 0; + while (p[n] != '\0' && p[n] != ',' && p[n] != '"' && n + 1 < out_len) { + out[n] = p[n]; + n++; + } + out[n] = '\0'; + return n > 0; +} + +static bool mfdes_add_certificate_profile_data(mfdes_verified_cert_t *cert_info, const char *label, const char *value) { + if (cert_info == NULL || label == NULL || value == NULL || value[0] == '\0' || + cert_info->certificate_profile_data_count >= ARRAYLEN(cert_info->certificate_profile_data)) { + return false; + } + + mfdes_certificate_profile_value_t *item = &cert_info->certificate_profile_data[cert_info->certificate_profile_data_count++]; + snprintf(item->label, sizeof(item->label), "%s", label); + snprintf(item->value, sizeof(item->value), "%s", value); + return true; +} + +static void mfdes_apply_certificate_profile_data(const mfdes_verifycert_profile_t *profile, mfdes_verified_cert_t *cert_info) { + if (profile == NULL || cert_info == NULL || profile->certificate_profile_data == NULL) { + return; + } + + cert_info->certificate_profile_data_count = 0; + memset(cert_info->certificate_profile_data, 0, sizeof(cert_info->certificate_profile_data)); + + for (const mfdes_certificate_profile_data_t *item = profile->certificate_profile_data; item->label != NULL; item++) { + if (item->dn_component == NULL) { + continue; + } + + const char *dn = NULL; + switch (item->source) { + case MFDES_VERIFYCERT_CERTIFICATE_PROFILE_DATA_SUBJECT_DN: + dn = cert_info->subject; + break; + case MFDES_VERIFYCERT_CERTIFICATE_PROFILE_DATA_ISSUER_DN: + dn = cert_info->issuer; + break; + default: + break; + } + if (dn == NULL || dn[0] == '\0') { + continue; + } + + char value[DUOX_CERT_TEXT_LEN] = {0}; + if (mfdes_extract_dn_component(dn, item->dn_component, value, sizeof(value))) { + mfdes_add_certificate_profile_data(cert_info, item->label, value); + } + } +} + +static int mfdes_verify_certificate_variants(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count, + bool verify_signature, + bool verbose, mfdes_verified_cert_t *out, size_t *matched_index) { + duox_cert_info_t cert = {0}; + int res = duox_parse_or_verify_certificate_variants(data, data_len, + ca_anchors, ca_anchor_count, + verify_signature, + verbose, &cert, matched_index); + if (res == PM3_SUCCESS || res == PM3_ECRYPTO) { + mfdes_cert_info_from_duox(&cert, out); + } + return res; +} + +static int duox_vde_sign_exchange(bool apdu_logging, bool verbose, + const uint8_t challenge[DUOX_VDE_CHALLENGE_LEN], + uint8_t signature_rs[DUOX_VDE_SIG_LEN]) { + if (challenge == NULL || signature_rs == NULL) { + return PM3_EINVARG; + } + + sAPDU_t apdu = {0x80, 0x03, 0x0C, 0x09, DUOX_VDE_CHALLENGE_LEN, (uint8_t *)challenge}; + uint8_t encoded[64] = {0}; + int encoded_len = 0; + if (APDUEncodeS(&apdu, false, APDU_INCLUDE_LE_00, encoded, &encoded_len)) { + PrintAndLogEx(ERR, "APDU encoding error"); + return PM3_ESOFT; + } + + if (apdu_logging) { + PrintAndLogEx(SUCCESS, ">>>> %s", sprint_hex(encoded, encoded_len)); + } + + uint8_t response[PM3_CMD_DATA_SIZE] = {0}; + int resplen = 0; + int res = ExchangeAPDU14a(encoded, encoded_len, false, true, response, sizeof(response), &resplen); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "APDU exchange " _RED_("failed") " (%d)", res); + return res; + } + + if (apdu_logging) { + PrintAndLogEx(SUCCESS, "<<<< %s", sprint_hex(response, resplen)); + } + + if (resplen < 2) { + PrintAndLogEx(ERR, "Response too short"); + return PM3_ESOFT; + } + + uint16_t sw = get_sw(response, resplen); + if (sw != ISO7816_OK) { + PrintAndLogEx(ERR, "VDE_ECDSASign " _RED_("failed") " (%04X - %s)", sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); + return PM3_ESOFT; + } + + int resp_data_len = resplen - 2; + if (resp_data_len != DUOX_VDE_SIG_LEN) { + PrintAndLogEx(ERR, "Unexpected signature length: %d bytes", resp_data_len); + return PM3_ESOFT; + } + + memcpy(signature_rs, response, DUOX_VDE_SIG_LEN); + PrintAndLogEx(SUCCESS, "VDE_ECDSASign " _GREEN_("ok") " (SW=%04X)", sw); + if (verbose) { + PrintAndLogEx(INFO, "Signature.... %s", sprint_hex_inrow(signature_rs, DUOX_VDE_SIG_LEN)); + } + return PM3_SUCCESS; +} + +static int mfdes_verify_vde_signature(const uint8_t challenge[DUOX_VDE_CHALLENGE_LEN], + const uint8_t signature_rs[DUOX_VDE_SIG_LEN], + const uint8_t pubkey_point[65], + bool verbose) { + if (challenge == NULL || signature_rs == NULL || pubkey_point == NULL) { + return PM3_EINVARG; + } + + if (verbose) { + PrintAndLogEx(INFO, "Verify msg... %s", sprint_hex_inrow(challenge, DUOX_VDE_CHALLENGE_LEN)); + } + + int sig_res = ecdsa_signature_r_s_verify( + MBEDTLS_ECP_DP_BP256R1, + (uint8_t *)pubkey_point, + (uint8_t *)challenge, + DUOX_VDE_CHALLENGE_LEN, + (uint8_t *)signature_rs, + DUOX_VDE_SIG_LEN, + true + ); + return (sig_res == PM3_SUCCESS) ? PM3_SUCCESS : PM3_ESOFT; +} + +static bool mfdes_validate_method_is_compatible(mfdes_validate_method_t method, const mfdes_verified_cert_t *cert_info, int keyidx, char *reason, size_t reason_len) { + if (cert_info == NULL) { + return false; + } + if (reason != NULL && reason_len > 0) { + reason[0] = '\0'; + } + + switch (method) { + case MFDES_VALIDATE_METHOD_INTAUTH: + if (cert_info->curveid != MBEDTLS_ECP_DP_SECP256R1 || cert_info->pubkey_len != 65) { + if (reason != NULL && reason_len > 0) { + snprintf(reason, reason_len, "Internal Authenticate needs a secp256r1 65-byte public key"); + } + return false; + } + return true; + case MFDES_VALIDATE_METHOD_VDE: + if (keyidx != 0) { + if (reason != NULL && reason_len > 0) { + snprintf(reason, reason_len, "VDE_ECDSASign supports key index 00"); + } + return false; + } + if (cert_info->curveid != MBEDTLS_ECP_DP_BP256R1 || cert_info->pubkey_len != 65) { + if (reason != NULL && reason_len > 0) { + snprintf(reason, reason_len, "VDE_ECDSASign needs a BrainpoolP256r1 65-byte public key"); + } + return false; + } + return true; + case MFDES_VALIDATE_METHOD_SKIP: + return true; + case MFDES_VALIDATE_METHOD_AUTO: + default: + return false; + } +} + +static size_t mfdes_copy_validate_methods(const mfdes_verifycert_profile_t *profile, + mfdes_validate_method_t *methods, size_t max_methods) { + if (profile == NULL || methods == NULL || max_methods == 0) { + return 0; + } + + size_t count = 0; + while (count < max_methods && profile->validate_methods[count] != MFDES_VALIDATE_METHOD_AUTO) { + methods[count] = profile->validate_methods[count]; + count++; + } + return count; +} + +static int mfdes_run_intauth_validation(bool APDULogging, bool verbose, const mfd_app_select *key_select, + int keyidx, const mfdes_verified_cert_t *cert_info) { + uint8_t challenge[DUOX_INTAUTH_CHALLENGE_LEN] = {0}; + int res = pcrypto_rng_fill_oneshot(challenge, sizeof(challenge), "hf_mfdes_verifycert_intauth"); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to generate challenge"); + return res; + } + + if (verbose) { + PrintAndLogEx(INFO, " method............: " _YELLOW_("ISO Internal Authenticate")); + } + + uint8_t card_random[DUOX_INTAUTH_CHALLENGE_LEN] = {0}; + uint8_t signature_rs[DUOX_INTAUTH_SIG_LEN] = {0}; + DesfireContext_t verify_dctx = {0}; + verify_dctx.commMode = DCMPlain; + verify_dctx.cmdSet = DCCNativeISO; + res = MfdSelectionSelectApplicationEx(&verify_dctx, key_select, verbose, false); + if (res != PM3_SUCCESS) { + return res; + } + + if (verbose) { + PrintAndLogEx(INFO, "Challenge.... " _YELLOW_("%s"), sprint_hex_inrow(challenge, DUOX_INTAUTH_CHALLENGE_LEN)); + } + res = duox_intauth_exchange(APDULogging, verbose, (uint8_t)keyidx, challenge, card_random, signature_rs); + if (res != PM3_SUCCESS) { + return res; + } + + if (verbose) { + PrintAndLogEx(INFO, "Card random...... " _YELLOW_("%s"), sprint_hex_inrow(card_random, DUOX_INTAUTH_CHALLENGE_LEN)); + PrintAndLogEx(INFO, "Signature r...... " _YELLOW_("%s"), sprint_hex_inrow(signature_rs, DUOX_INTAUTH_SIG_LEN / 2)); + PrintAndLogEx(INFO, "Signature s...... " _YELLOW_("%s"), sprint_hex_inrow(signature_rs + DUOX_INTAUTH_SIG_LEN / 2, DUOX_INTAUTH_SIG_LEN / 2)); + } + + res = duox_intauth_verify_sig(verbose, cert_info->pubkey, challenge, card_random, signature_rs); + if (res == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "Internal Authenticate signature " _GREEN_("verified")); + return PM3_SUCCESS; + } + + PrintAndLogEx(ERR, "Internal Authenticate signature " _RED_("verification failed")); + return PM3_ESOFT; +} + +static int mfdes_run_vde_validation(bool APDULogging, bool verbose, const mfd_app_select *key_select, + const mfdes_verified_cert_t *cert_info) { + uint8_t challenge[DUOX_VDE_CHALLENGE_LEN] = {0}; + int res = pcrypto_rng_fill_oneshot(challenge, sizeof(challenge), "hf_mfdes_verifycert_vde"); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to generate VDE challenge"); + return res; + } + + if (verbose) { + PrintAndLogEx(INFO, " method............: " _YELLOW_("VDE_ECDSASign")); + } + + uint8_t signature_rs[DUOX_VDE_SIG_LEN] = {0}; + DesfireContext_t verify_dctx = {0}; + verify_dctx.commMode = DCMPlain; + verify_dctx.cmdSet = DCCNativeISO; + res = MfdSelectionSelectApplicationEx(&verify_dctx, key_select, verbose, false); + if (res != PM3_SUCCESS) { + return res; + } + + if (verbose) { + PrintAndLogEx(INFO, "Challenge.... " _YELLOW_("%s"), sprint_hex_inrow(challenge, DUOX_VDE_CHALLENGE_LEN)); + } + res = duox_vde_sign_exchange(APDULogging, verbose, challenge, signature_rs); + if (res != PM3_SUCCESS) { + return res; + } + + if (verbose) { + PrintAndLogEx(INFO, "VDE signature r... " _YELLOW_("%s"), sprint_hex_inrow(signature_rs, DUOX_VDE_SIG_LEN / 2)); + PrintAndLogEx(INFO, "VDE signature s... " _YELLOW_("%s"), sprint_hex_inrow(signature_rs + DUOX_VDE_SIG_LEN / 2, DUOX_VDE_SIG_LEN / 2)); + } + + res = mfdes_verify_vde_signature(challenge, signature_rs, cert_info->pubkey, verbose); + if (res == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "VDE_ECDSASign signature " _GREEN_("verified")); + return PM3_SUCCESS; + } + + PrintAndLogEx(ERR, "VDE_ECDSASign signature " _RED_("verification failed")); + return PM3_ESOFT; +} + +static int mfdes_run_key_possession_verification(bool APDULogging, bool verbose, + const mfdes_validate_method_t *methods, + size_t method_count, + bool forced_method, + const mfd_app_select *key_select, + int keyidx, + const mfdes_verified_cert_t *cert_info) { + if (methods == NULL || method_count == 0 || key_select == NULL || cert_info == NULL) { + return PM3_EINVARG; + } + + if (method_count == 1 && methods[0] == MFDES_VALIDATE_METHOD_SKIP) { + PrintAndLogEx(SUCCESS, "Key possession verification " _YELLOW_("skipped")); + return PM3_SUCCESS; + } + + int last_res = PM3_ENOTIMPL; + bool tried_any = false; + for (size_t i = 0; i < method_count; i++) { + char reason[128] = {0}; + if (!mfdes_validate_method_is_compatible(methods[i], cert_info, keyidx, reason, sizeof(reason))) { + if (forced_method) { + PrintAndLogEx(ERR, "%s is not compatible with this certificate/key selection: %s", + CLIGetOptionListStr(mfdesValidateMethodOpts, methods[i]), reason); + return PM3_ENOTIMPL; + } + if (verbose && reason[0] != '\0') { + PrintAndLogEx(INFO, "Skipping %s: %s", CLIGetOptionListStr(mfdesValidateMethodOpts, methods[i]), reason); + } + continue; + } + + tried_any = true; + if (methods[i] == MFDES_VALIDATE_METHOD_INTAUTH) { + last_res = mfdes_run_intauth_validation(APDULogging, verbose, key_select, keyidx, cert_info); + } else if (methods[i] == MFDES_VALIDATE_METHOD_VDE) { + last_res = mfdes_run_vde_validation(APDULogging, verbose, key_select, cert_info); + } + + if (last_res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + if (forced_method) { + return last_res; + } + PrintAndLogEx(WARNING, "%s validation failed; trying next compatible method", CLIGetOptionListStr(mfdesValidateMethodOpts, methods[i])); + } + + if (!tried_any) { + PrintAndLogEx(ERR, "Certificate key is unsupported for key possession verification"); + PrintAndLogEx(INFO, "Supported methods: secp256r1 via ISO Internal Authenticate, or BrainpoolP256r1 key index 00 via VDE_ECDSASign"); + return PM3_ENOTIMPL; + } + + return last_res; +} + +static int CmdHF14ADesVerifyCert(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfdes verifycert", + "Read a certificate from DESFire file,\n" + "validate it against root CA keys, extract device public key,\n" + "then verify key possession with ISO Internal Authenticate or VDE_ECDSASign.", + "hf mfdes verifycert leaf\n" + "hf mfdes verifycert vde\n" + "hf mfdes verifycert orig\n" + "hf mfdes verifycert picc\n" + "hf mfdes verifycert --fid 01\n" + "hf mfdes verifycert leaf --aid D61CF5 --keyidx 0\n" + "hf mfdes verifycert vde --dfname A0000008450000000000000000000001 --aid F61010\n" + "hf mfdes verifycert orig --ca nxp-orig-e200\n" + "hf mfdes verifycert vde --ca skip\n" + "hf mfdes verifycert --fid 01 --ca ./rootca.pem --ca ./backup_root.der\n" + "hf mfdes verifycert --fid 01 --ca 04AABB... (public key)\n" + "hf mfdes verifycert --fid 01 --keyaid 123456 --keyidx 2"); + + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, NULL, "", "Profile: leaf, vde, orig, originality, picc"), // 1 + arg_lit0("a", "apdu", "Show APDU requests and responses"), // 2 + arg_lit0("v", "verbose", "Verbose output"), // 3 + arg_int0("n", "keyno", "", "Key number for DESFire authentication"), // 4 + arg_str0("t", "algo", "", "DESFire auth crypt algo"), // 5 + arg_str0("k", "key", "", "DESFire authentication key"), // 6 + arg_str0(NULL, "kdf", "", "Key Derivation Function (KDF)"), // 7 + arg_str0("i", "kdfi", "", "KDF input (1-31 hex bytes)"), // 8 + arg_str0("m", "cmode", "", "Communication mode"), // 9 + arg_str0("c", "ccset", "", "Communication command set"), // 10 + arg_str0(NULL, "schann", "", "Secure channel (verifycert supports only ev2 for auth)"), // 11 + arg_str0(NULL, "aid", "", "Certificate application ID (3 bytes)"), // 12 + arg_str0(NULL, "isoid", "", "Certificate application ISO DF ID (2 bytes)"), // 13 + arg_str0(NULL, "dfname", "", "Certificate application DF name (1-16 bytes)"), // 14 + arg_str0(NULL, "fid", "", "Certificate file ID (1 byte)"), // 15 + arg_lit0(NULL, "no-auth", "Read certificate file without authentication"), // 16 + arg_strn(NULL, "ca", "", 0, MFDES_VERIFYCERT_MAX_CAS, + "CA input, or `skip` to skip certificate signature validation. Repeat --ca for multiple entries"), // 17 + arg_str0(NULL, "keyaid", "", "Key application ID (default: cert app)"), // 18 + arg_str0(NULL, "keyisoid", "", "Key application ISO DF ID (2 bytes)"), // 19 + arg_str0(NULL, "keydfname", "", "Key application DF name (default: cert app)"), // 20 + arg_int0(NULL, "keyidx", "", "Key index for possession verification (profile default)"), // 21 + arg_str0(NULL, "validatemethod", "", "Key possession verification method (default: profile/auto)"), // 22 + arg_str0(NULL, "readmethod", "", "Certificate read method (reserved for future methods)"), // 23 + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + bool APDULogging = arg_get_lit(ctx, 2); + bool verbose = arg_get_lit(ctx, 3); + bool noauth = arg_get_lit(ctx, 16); + bool secure_channel_present = (arg_get_str(ctx, 11)->count > 0); + bool auth_params_present = (arg_get_int_count(ctx, 4) > 0 || + arg_get_str(ctx, 5)->count > 0 || + arg_get_str(ctx, 6)->count > 0 || + arg_get_str(ctx, 7)->count > 0 || + arg_get_str(ctx, 8)->count > 0 || + arg_get_str(ctx, 9)->count > 0 || + arg_get_str(ctx, 10)->count > 0 || + secure_channel_present); + bool force_auth = auth_params_present && !noauth; + + const char *profile_arg = (arg_get_str(ctx, 1)->count > 0) ? arg_get_str(ctx, 1)->sval[0] : NULL; + const mfdes_verifycert_profile_t *profile = mfdes_find_verifycert_profile(profile_arg); + if (profile == NULL) { + PrintAndLogEx(ERR, "Unknown verifycert profile `%s`", profile_arg); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + mfd_app_select cert_select = profile->cert_select; + mfd_app_select key_select = profile->key_select_present ? profile->key_select : profile->cert_select; + uint8_t cert_fid = profile->fid; + uint8_t default_keyidx = profile->keyidx; + mfdes_validate_method_t validate_methods[MFDES_VERIFYCERT_PROFILE_MAX_METHODS] = {0}; + size_t validate_method_count = mfdes_copy_validate_methods(profile, validate_methods, ARRAYLEN(validate_methods)); + bool validate_method_forced = false; + int res = PM3_SUCCESS; + + if (MfdSelectionApplyCmdParameters(ctx, 12, 13, 14, &cert_select) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + bool key_selector_present = (arg_get_str(ctx, 18)->count > 0 || + arg_get_str(ctx, 19)->count > 0 || + arg_get_str(ctx, 20)->count > 0); + if (!key_selector_present) { + if (!profile->key_select_present) { + key_select = cert_select; + } + } else if (MfdSelectionApplyCmdParameters(ctx, 18, 19, 20, &key_select) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + bool fid_present = (arg_get_str(ctx, 15)->count > 0); + if (fid_present) { + uint32_t parsed_fid = 0; + if (CLIGetUint32Hex(ctx, 15, 0, &parsed_fid, NULL, 1, "File ID must have 1 byte length")) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + cert_fid = parsed_fid & 0xFF; + } else if (cert_fid == MFDES_VERIFYCERT_NO_FID) { + PrintAndLogEx(ERR, "Verifycert profile has no default certificate file; specify --fid"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + if (cert_fid == MFDES_VERIFYCERT_NO_FID || cert_fid > 0x1F) { + PrintAndLogEx(ERR, "File number range is invalid (exp 0x00 - 0x1f), got 0x%02x", cert_fid); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + int keyidx = arg_get_int_def(ctx, 21, default_keyidx); + if (keyidx < 0 || keyidx > 255) { + PrintAndLogEx(ERR, "Key index must be 0..255"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + mfdes_validate_method_t cli_validate_method = MFDES_VALIDATE_METHOD_AUTO; + if (CLIGetOptionList(arg_get_str(ctx, 22), mfdesValidateMethodOpts, (int *)&cli_validate_method)) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + if (arg_get_str(ctx, 22)->count > 0 && cli_validate_method != MFDES_VALIDATE_METHOD_AUTO) { + validate_methods[0] = cli_validate_method; + validate_method_count = 1; + validate_method_forced = true; + } + if (validate_method_count == 0) { + PrintAndLogEx(ERR, "Verifycert profile has no validation methods configured"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + mfdes_read_method_t read_method = MFDES_READ_METHOD_AUTO; + if (CLIGetOptionList(arg_get_str(ctx, 23), mfdesReadMethodOpts, (int *)&read_method)) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + char ca_inputs[MFDES_VERIFYCERT_MAX_CAS][DUOX_MAX_KEY_INPUT] = {{0}}; + int ca_input_count = arg_get_str(ctx, 17)->count; + bool ca_validation_skipped = false; + if (ca_input_count > MFDES_VERIFYCERT_MAX_CAS) { + PrintAndLogEx(ERR, "Too many --ca options (%d), max %d", ca_input_count, MFDES_VERIFYCERT_MAX_CAS); + CLIParserFree(ctx); + return PM3_EINVARG; + } + for (int i = 0; i < ca_input_count; i++) { + const char *src = arg_get_str(ctx, 17)->sval[i]; + if (src == NULL || src[0] == '\0') { + PrintAndLogEx(ERR, "Empty --ca argument"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + if (str_equal_case_insensitive(src, "skip")) { + if (ca_input_count != 1) { + PrintAndLogEx(ERR, "`--ca skip` cannot be combined with other CA inputs"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + ca_validation_skipped = true; + continue; + } + snprintf(ca_inputs[i], sizeof(ca_inputs[i]), "%s", src); + } + + DesfireContext_t dctx = {0}; + int securechann = defaultSecureChannel; + res = CmdDesGetSessionParameters(ctx, &dctx, + 4, 5, 6, 7, 8, 9, 10, 11, + 0, 0, 0, + &securechann, + force_auth ? DCMMACed : DCMPlain, + NULL, NULL); + if (res) { + CLIParserFree(ctx); + return res; + } + if (secure_channel_present && securechann != DACEV2) { + PrintAndLogEx(ERR, "DUOX verifycert supports only EV2 secure channel, got %s", + CLIGetOptionListStr(DesfireSecureChannelOpts, securechann)); + CLIParserFree(ctx); + return PM3_EINVARG; + } + securechann = DACEV2; + if (!noauth && !auth_params_present) { + uint8_t zero_key[CRYPTO_AES128_KEY_SIZE] = {0}; + DesfireSetKeyNoClear(&dctx, dctx.keyNum, T_AES, zero_key); + } + + SetAPDULogging(APDULogging); + CLIParserFree(ctx); + + duox_certificate_anchor_t ca_list[MFDES_VERIFYCERT_MAX_CAS] = {0}; + size_t ca_count = 0; + if (ca_validation_skipped) { + ca_count = 0; + } else if (ca_input_count > 0) { + for (int i = 0; i < ca_input_count; i++) { + if (ca_count >= ARRAYLEN(ca_list)) { + PrintAndLogEx(ERR, "Too many CA entries, max %d", MFDES_VERIFYCERT_MAX_CAS); + return PM3_EOVFLOW; + } + res = duox_load_certificate_anchor_from_input(ca_inputs[i], DUOX_DEFAULT_CA_DIR, &ca_list[ca_count]); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Unable to parse `--ca %s` as X.509 certificate or EC public key", ca_inputs[i]); + return PM3_EINVARG; + } + ca_count++; + } + } else { + res = duox_load_certificate_anchors_from_store(DUOX_DEFAULT_CA_DIR, ca_list, ARRAYLEN(ca_list), &ca_count); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Unable to load default DUOX CA files from resources"); + return PM3_ESOFT; + } + } + + if (!ca_validation_skipped && ca_count == 0) { + PrintAndLogEx(ERR, "No CA entries available for certificate validation"); + return PM3_EINVARG; + } + + if (verbose) { + PrintAndLogEx(INFO, "--- " _CYAN_("Certificate read")); + MfdSelectionPrintPrefixed(&cert_select, " "); + PrintAndLogEx(INFO, " file id...........: " _YELLOW_("%02X"), (uint8_t)cert_fid); + PrintAndLogEx(INFO, " read method.......: " _YELLOW_("%s"), CLIGetOptionListStr(mfdesReadMethodOpts, read_method)); + PrintAndLogEx(INFO, " auth mode.........: " _YELLOW_("%s"), noauth ? "no-auth" : (force_auth ? "forced" : "auto")); + } + + uint8_t *cert_buf = calloc(DESFIRE_BUFFER_SIZE, sizeof(uint8_t)); + if (cert_buf == NULL) { + PrintAndLogEx(ERR, "Failed to allocate memory"); + return PM3_EMALLOC; + } + size_t cert_buf_len = 0; + mfdes_verified_cert_t cert_info = {0}; + size_t matched_ca_index = 0; + + int retval = PM3_SUCCESS; + bool field_active = false; + + res = MfdSelectionSelectApplication(&dctx, &cert_select, verbose); + if (res != PM3_SUCCESS) { + DropField(); + PrintAndLogEx(ERR, "Certificate application selection failed"); + retval = res; + goto out; + } + field_active = true; + + res = mfdes_read_cert_file_desfire(&dctx, (DesfireSecureChannel)securechann, cert_fid, + noauth, force_auth, verbose, cert_buf, &cert_buf_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Certificate file read " _RED_("failed") ". Result: %d", res); + retval = res; + goto out; + } + + if (cert_buf_len == 0) { + PrintAndLogEx(ERR, "Certificate file is empty"); + retval = PM3_ESOFT; + goto out; + } + + if (verbose) { + PrintAndLogEx(INFO, " file size.........: " _YELLOW_("%zu bytes"), cert_buf_len); + PrintAndLogEx(INFO, " raw certificate data:"); + print_hex_break(cert_buf, cert_buf_len, 16); + } + + res = mfdes_verify_certificate_variants(cert_buf, cert_buf_len, + ca_list, ca_count, + !ca_validation_skipped, + verbose, &cert_info, &matched_ca_index); + if (res == PM3_ENODATA && !noauth && !auth_params_present) { + PrintAndLogEx(WARNING, "Certificate data was not parseable; retrying read with default AES authentication"); + DesfireContext_t aes_dctx = dctx; + uint8_t zero_key[CRYPTO_AES128_KEY_SIZE] = {0}; + DesfireSetKeyNoClear(&aes_dctx, aes_dctx.keyNum, T_AES, zero_key); + cert_buf_len = 0; + int read_res = mfdes_read_cert_file_desfire(&aes_dctx, (DesfireSecureChannel)securechann, cert_fid, + false, true, verbose, cert_buf, &cert_buf_len); + if (read_res == PM3_SUCCESS && cert_buf_len > 0) { + if (verbose) { + PrintAndLogEx(INFO, " retry file size....: " _YELLOW_("%zu bytes"), cert_buf_len); + PrintAndLogEx(INFO, " retry certificate data:"); + print_hex_break(cert_buf, cert_buf_len, 16); + } + memset(&cert_info, 0, sizeof(cert_info)); + matched_ca_index = 0; + res = mfdes_verify_certificate_variants(cert_buf, cert_buf_len, + ca_list, ca_count, + !ca_validation_skipped, + verbose, &cert_info, &matched_ca_index); + } else if (verbose) { + PrintAndLogEx(INFO, "Default AES authenticated retry did not return certificate data (%d)", read_res); + } + } + + if (cert_info.format != DUOX_CERTIFICATE_FORMAT_UNKNOWN) { + mfdes_apply_certificate_profile_data(profile, &cert_info); + mfdes_print_certificate_info(&cert_info); + } + + if (res != PM3_SUCCESS) { + if (res == PM3_ENODATA) { + PrintAndLogEx(ERR, "Unable to parse certificate file data as X.509 or GP VDE certificate"); + } else if (res == PM3_ECRYPTO) { + PrintAndLogEx(ERR, ca_validation_skipped + ? "Certificate parsed, but public key extraction failed" + : "Certificate parsed, but signature validation failed with CA entries"); + } else { + PrintAndLogEx(ERR, "Certificate validation failed (%d)", res); + } + retval = res; + goto out; + } + + if (ca_validation_skipped) { + PrintAndLogEx(WARNING, "Certificate signature validation skipped (--ca skip)"); + } else { + const duox_certificate_anchor_t *matched_ca = &ca_list[matched_ca_index]; + const char *verified_ca_name = duox_certificate_anchor_display_name(matched_ca); + PrintAndLogEx(SUCCESS, "Certificate signature verified successfully using " _GREEN_("%s"), verified_ca_name); + } + + if (verbose) { + PrintAndLogEx(INFO, _CYAN_("Key possession verification")); + MfdSelectionPrintPrefixed(&key_select, " "); + PrintAndLogEx(INFO, " key index.........: " _YELLOW_("%d"), keyidx); + PrintAndLogEx(INFO, " method............: " _YELLOW_("%s"), + (validate_method_count == 1) ? CLIGetOptionListStr(mfdesValidateMethodOpts, validate_methods[0]) : "auto"); + } + + res = mfdes_run_key_possession_verification(APDULogging, verbose, validate_methods, + validate_method_count, validate_method_forced, + &key_select, keyidx, &cert_info); + if (res != PM3_SUCCESS) { + retval = res; + goto out; + } + + bool key_possession_skipped = (validate_method_count == 1 && validate_methods[0] == MFDES_VALIDATE_METHOD_SKIP); + if (ca_validation_skipped && key_possession_skipped) { + PrintAndLogEx(SUCCESS, "Certificate parsed; signature validation and key possession verification " _GREEN_("skipped")); + } else if (ca_validation_skipped) { + PrintAndLogEx(SUCCESS, "Certificate parsed and key possession " _GREEN_("verified")); + } else if (key_possession_skipped) { + PrintAndLogEx(SUCCESS, "Certificate chain validated; key possession verification " _GREEN_("skipped")); + } else { + PrintAndLogEx(SUCCESS, "Certificate chain validated; key possession " _GREEN_("verified")); + } + +out: + if (field_active) { + DropField(); + } + free(cert_buf); + return retval; +} + static int CmdHF14ADesTest(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfdes test", @@ -8554,6 +9626,7 @@ static command_t CommandTable[] = { {"clearrecfile", CmdHF14ADesClearRecordFile, IfPm3Iso14443a, "Clear record File"}, {"makemfclicense", CmdHF14ADesMakeMFCLicense, AlwaysAvailable, "Generate a Mifare Classic license for DESFire EV3C"}, {"-----------", CmdHelp, IfPm3Iso14443a, "----------------------- " _CYAN_("DUOX") " ------------------------"}, + {"verifycert", CmdHF14ADesVerifyCert, IfPm3Iso14443a, "Validate cert from file and verify key possession"}, {"intauth", CmdHF14ADesIntAuth, IfPm3Iso14443a, "ISO Internal Authenticate (ECDSA challenge-response)"}, {"vdesign", CmdHF14ADesVdeSign, IfPm3Iso14443a, "VDE ECDSASign (EV charging signature over 32-byte challenge)"}, {"leaf", CmdHF14ADesLeaf, IfPm3Iso14443a, "LEAF Verified credential read + cert + auth check"}, diff --git a/client/src/crypto/duoxcrypto.c b/client/src/crypto/duoxcrypto.c new file mode 100644 index 000000000..ae7088131 --- /dev/null +++ b/client/src/crypto/duoxcrypto.c @@ -0,0 +1,1130 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 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 General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// MIFARE DUOX certificate crypto helpers +//----------------------------------------------------------------------------- + +#include "crypto/duoxcrypto.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "commonutil.h" +#include "crypto/asn1utils.h" +#include "crypto/libpcrypto.h" +#include "fileutils.h" +#include "ui.h" +#include "util.h" +#include "x509_crt.h" + +#define DUOX_CERTIFICATE_ANCHOR_INPUT_LEN 8192 + +static int duox_cert_info_from_x509_crt(const mbedtls_x509_crt *cert, duox_cert_info_t *out); + +const char *duox_certificate_format_name(duox_certificate_format_t format) { + switch (format) { + case DUOX_CERTIFICATE_FORMAT_X509: + return "X.509"; + case DUOX_CERTIFICATE_FORMAT_GP_VDE: + return "GP VDE"; + case DUOX_CERTIFICATE_FORMAT_UNKNOWN: + default: + return "unknown"; + } +} + +const char *duox_cert_info_format_name(const duox_cert_info_t *cert) { + if (cert == NULL) { + return duox_certificate_format_name(DUOX_CERTIFICATE_FORMAT_UNKNOWN); + } + return duox_certificate_format_name(cert->format); +} + +static void duox_trim_ascii_inplace(char *text) { + if (text == NULL) { + return; + } + + size_t start = 0; + size_t len = strlen(text); + while (start < len && isspace((unsigned char)text[start])) { + start++; + } + while (len > start && isspace((unsigned char)text[len - 1])) { + len--; + } + + if (start > 0) { + memmove(text, text + start, len - start); + } + text[len - start] = '\0'; +} + +static int duox_copy_without_whitespace(const char *src, char *dst, size_t dst_size, size_t *dst_len) { + if (src == NULL || dst == NULL || dst_len == NULL || dst_size == 0) { + return PM3_EINVARG; + } + + size_t out = 0; + for (size_t i = 0; src[i] != '\0'; i++) { + if (isspace((unsigned char)src[i])) { + continue; + } + if ((out + 1) >= dst_size) { + return PM3_EOVFLOW; + } + dst[out++] = src[i]; + } + dst[out] = '\0'; + *dst_len = out; + return PM3_SUCCESS; +} + +static bool duox_path_is_directory(const char *path) { + if (path == NULL) { + return false; + } + struct stat st; + if (stat(path, &st) != 0) { + return false; + } + return S_ISDIR(st.st_mode) != 0; +} + +static bool duox_path_is_regular_file(const char *path) { + if (path == NULL) { + return false; + } + struct stat st; + if (stat(path, &st) != 0) { + return false; + } + return S_ISREG(st.st_mode) != 0; +} + +static const char *duox_path_basename(const char *path) { + if (path == NULL) { + return ""; + } + + const char *base = strrchr(path, '/'); + const char *base_win = strrchr(path, '\\'); + if (base == NULL || (base_win != NULL && base_win > base)) { + base = base_win; + } + return (base == NULL) ? path : (base + 1); +} + +static void duox_path_basename_without_ext(const char *path, char *out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + out[0] = '\0'; + + const char *base = duox_path_basename(path); + if (base[0] == '\0') { + return; + } + + snprintf(out, out_len, "%s", base); + char *dot = strrchr(out, '.'); + if (dot != NULL && dot != out) { + *dot = '\0'; + } +} + +static int duox_qsort_path_cmp(const void *a, const void *b) { + const char *pa = (const char *)a; + const char *pb = (const char *)b; + return strcmp(pa, pb); +} + +static int duox_collect_certificate_anchor_paths_recursive(const char *dirpath, + char paths[][DUOX_CERTIFICATE_ANCHOR_PATH_LEN], + size_t max_paths, size_t *count) { + if (dirpath == NULL || paths == NULL || count == NULL) { + return PM3_EINVARG; + } + + DIR *dir = opendir(dirpath); + if (dir == NULL) { + return PM3_EFILE; + } + + struct dirent *entry = NULL; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0 || entry->d_name[0] == '.') { + continue; + } + + char fullpath[DUOX_CERTIFICATE_ANCHOR_PATH_LEN] = {0}; + if (snprintf(fullpath, sizeof(fullpath), "%s/%s", dirpath, entry->d_name) >= (int)sizeof(fullpath)) { + continue; + } + + if (duox_path_is_directory(fullpath)) { + int res = duox_collect_certificate_anchor_paths_recursive(fullpath, paths, max_paths, count); + if (res != PM3_SUCCESS) { + closedir(dir); + return res; + } + continue; + } + + if (!duox_path_is_regular_file(fullpath)) { + continue; + } + if (*count >= max_paths) { + closedir(dir); + return PM3_EOVFLOW; + } + + snprintf(paths[*count], DUOX_CERTIFICATE_ANCHOR_PATH_LEN, "%s", fullpath); + (*count)++; + } + + closedir(dir); + return PM3_SUCCESS; +} + +static int duox_collect_certificate_anchor_paths(const char *anchor_store_dir, + char paths[][DUOX_CERTIFICATE_ANCHOR_PATH_LEN], + size_t max_paths, size_t *count) { + if (anchor_store_dir == NULL || anchor_store_dir[0] == '\0' || paths == NULL || count == NULL) { + return PM3_EINVARG; + } + + char *rootdir = NULL; + int res = searchFile(&rootdir, RESOURCES_SUBDIR, anchor_store_dir, "", true); + if (res != PM3_SUCCESS) { + return res; + } + + if (!duox_path_is_directory(rootdir)) { + free(rootdir); + return PM3_EFILE; + } + + *count = 0; + res = duox_collect_certificate_anchor_paths_recursive(rootdir, paths, max_paths, count); + free(rootdir); + if (res != PM3_SUCCESS) { + return res; + } + + qsort(paths, *count, sizeof(paths[0]), duox_qsort_path_cmp); + return PM3_SUCCESS; +} + +int duox_certificate_anchor_public_key(const duox_certificate_anchor_t *anchor, + mbedtls_ecp_group_id *curveid, + const uint8_t **pubkey, size_t *pubkey_len) { + if (anchor == NULL || curveid == NULL || pubkey == NULL || pubkey_len == NULL) { + return PM3_EINVARG; + } + + switch (anchor->type) { + case DUOX_CERTIFICATE_ANCHOR_MATERIAL_NONE: + return PM3_EINVARG; + case DUOX_CERTIFICATE_ANCHOR_MATERIAL_PUBLIC_KEY: + *curveid = anchor->material.key.curveid; + *pubkey = anchor->material.key.pubkey; + *pubkey_len = anchor->material.key.pubkey_len; + break; + case DUOX_CERTIFICATE_ANCHOR_MATERIAL_CERT: + *curveid = anchor->material.cert.curveid; + *pubkey = anchor->material.cert.pubkey; + *pubkey_len = anchor->material.cert.pubkey_len; + break; + default: + return PM3_EINVARG; + } + + return (*curveid != MBEDTLS_ECP_DP_NONE && *pubkey != NULL && *pubkey_len > 0) ? PM3_SUCCESS : PM3_EINVARG; +} + +const char *duox_certificate_anchor_subject(const duox_certificate_anchor_t *anchor) { + if (anchor == NULL || anchor->type != DUOX_CERTIFICATE_ANCHOR_MATERIAL_CERT) { + return ""; + } + return anchor->material.cert.subject; +} + +const char *duox_certificate_anchor_display_name(const duox_certificate_anchor_t *anchor) { + if (anchor == NULL) { + return "unknown certificate anchor"; + } + if (anchor->name[0] != '\0') { + return anchor->name; + } + if (anchor->source[0] != '\0') { + return anchor->source; + } + if (anchor->type == DUOX_CERTIFICATE_ANCHOR_MATERIAL_PUBLIC_KEY) { + return "provided public key"; + } + return "provided certificate"; +} + +static int duox_load_x509_certificate_input(const char *input, mbedtls_x509_crt *cert) { + if (input == NULL || cert == NULL) { + return PM3_EINVARG; + } + + char normalized[DUOX_CERTIFICATE_ANCHOR_INPUT_LEN] = {0}; + size_t input_len = strlen(input); + if (input_len >= sizeof(normalized)) { + return PM3_EOVFLOW; + } + memcpy(normalized, input, input_len + 1); + duox_trim_ascii_inplace(normalized); + if (normalized[0] == '\0') { + return PM3_EINVARG; + } + + char *resolved_path = NULL; + if (searchFile(&resolved_path, RESOURCES_SUBDIR, normalized, "", true) == PM3_SUCCESS) { + int ret = mbedtls_x509_crt_parse_file(cert, resolved_path); + free(resolved_path); + if (ret == 0) { + return PM3_SUCCESS; + } + } + + if (mbedtls_x509_crt_parse(cert, (const unsigned char *)normalized, strlen(normalized) + 1) == 0) { + return PM3_SUCCESS; + } + + char compact[DUOX_CERTIFICATE_ANCHOR_INPUT_LEN] = {0}; + size_t compact_len = 0; + if (duox_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) != PM3_SUCCESS || compact_len == 0) { + return PM3_EINVARG; + } + + size_t der_capacity = (compact_len / 2) + 1; + uint8_t *der = calloc(der_capacity, sizeof(uint8_t)); + if (der == NULL) { + return PM3_EMALLOC; + } + + int der_len = hex_to_bytes(compact, der, der_capacity); + int res = (der_len > 0 && mbedtls_x509_crt_parse_der(cert, der, (size_t)der_len) == 0) ? PM3_SUCCESS : PM3_EINVARG; + free(der); + return res; +} + +static int duox_load_certificate_anchor_from_certificate_input(const char *input, const char *name_hint, + const char *source, duox_certificate_anchor_t *anchor) { + if (input == NULL || anchor == NULL) { + return PM3_EINVARG; + } + + duox_certificate_anchor_t candidate = {0}; + mbedtls_x509_crt cert; + mbedtls_x509_crt_init(&cert); + int res = duox_load_x509_certificate_input(input, &cert); + if (res == PM3_SUCCESS) { + res = duox_cert_info_from_x509_crt(&cert, &candidate.material.cert); + } + mbedtls_x509_crt_free(&cert); + if (res != PM3_SUCCESS) { + return res; + } + + candidate.type = DUOX_CERTIFICATE_ANCHOR_MATERIAL_CERT; + if (source != NULL && source[0] != '\0') { + str_copy(candidate.source, sizeof(candidate.source), source); + } + if (name_hint != NULL && name_hint[0] != '\0') { + str_copy(candidate.name, sizeof(candidate.name), name_hint); + } else if (candidate.material.cert.subject[0] != '\0') { + str_copy(candidate.name, sizeof(candidate.name), candidate.material.cert.subject); + } else { + str_copy(candidate.name, sizeof(candidate.name), "x509 certificate anchor"); + } + + *anchor = candidate; + return PM3_SUCCESS; +} + +static int duox_load_certificate_anchor_from_pubkey_input(const char *input, const char *name_hint, + const char *source, duox_certificate_anchor_t *anchor) { + if (input == NULL || anchor == NULL) { + return PM3_EINVARG; + } + + static const mbedtls_ecp_group_id curves[] = { + MBEDTLS_ECP_DP_SECP256R1, + MBEDTLS_ECP_DP_SECP384R1, + MBEDTLS_ECP_DP_SECP521R1, + MBEDTLS_ECP_DP_SECP224R1, + MBEDTLS_ECP_DP_SECP192R1, + MBEDTLS_ECP_DP_BP256R1, + MBEDTLS_ECP_DP_SECP256K1, + }; + + for (size_t i = 0; i < ARRAYLEN(curves); i++) { + const mbedtls_ecp_curve_info *curve_info = mbedtls_ecp_curve_info_from_grp_id(curves[i]); + if (curve_info == NULL) { + continue; + } + + duox_certificate_anchor_t candidate = {0}; + candidate.type = DUOX_CERTIFICATE_ANCHOR_MATERIAL_PUBLIC_KEY; + if (source != NULL && source[0] != '\0') { + str_copy(candidate.source, sizeof(candidate.source), source); + } + if (name_hint != NULL && name_hint[0] != '\0') { + str_copy(candidate.name, sizeof(candidate.name), name_hint); + } else { + str_copy(candidate.name, sizeof(candidate.name), "provided public key"); + } + + size_t coord_len = (curve_info->bit_size + 7) / 8; + size_t pubkey_len = 1 + (2 * coord_len); + if (pubkey_len > sizeof(candidate.material.key.pubkey)) { + continue; + } + + if (ensure_ec_public_key(input, curves[i], candidate.material.key.pubkey, pubkey_len) == PM3_SUCCESS) { + candidate.material.key.curveid = curves[i]; + candidate.material.key.pubkey_len = pubkey_len; + *anchor = candidate; + return PM3_SUCCESS; + } + } + + return PM3_EINVARG; +} + +static int duox_load_certificate_anchor_from_file_path(const char *filepath, duox_certificate_anchor_t *anchor) { + if (filepath == NULL || anchor == NULL) { + return PM3_EINVARG; + } + + char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0}; + duox_path_basename_without_ext(filepath, filename_anchor_name, sizeof(filename_anchor_name)); + if (filename_anchor_name[0] == '\0') { + return PM3_EINVARG; + } + + int res = duox_load_certificate_anchor_from_certificate_input(filepath, filename_anchor_name, filepath, anchor); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + + return duox_load_certificate_anchor_from_pubkey_input(filepath, filename_anchor_name, filepath, anchor); +} + +static int duox_load_named_certificate_anchor_from_store(const char *token, const char *anchor_store_dir, + duox_certificate_anchor_t *anchor) { + if (token == NULL || anchor_store_dir == NULL || anchor == NULL) { + return PM3_EINVARG; + } + + char paths[DUOX_CERTIFICATE_ANCHOR_MAX_PATHS][DUOX_CERTIFICATE_ANCHOR_PATH_LEN] = {{0}}; + size_t path_count = 0; + int res = duox_collect_certificate_anchor_paths(anchor_store_dir, paths, ARRAYLEN(paths), &path_count); + if (res != PM3_SUCCESS) { + return res; + } + + for (size_t i = 0; i < path_count; i++) { + char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0}; + duox_path_basename_without_ext(paths[i], filename_anchor_name, sizeof(filename_anchor_name)); + if (filename_anchor_name[0] == '\0') { + continue; + } + if (!str_equal_case_insensitive(token, filename_anchor_name)) { + continue; + } + return duox_load_certificate_anchor_from_file_path(paths[i], anchor); + } + + const char *matched_path = NULL; + for (size_t i = 0; i < path_count; i++) { + char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0}; + duox_path_basename_without_ext(paths[i], filename_anchor_name, sizeof(filename_anchor_name)); + if (filename_anchor_name[0] == '\0' || !str_startswith_case_insensitive(filename_anchor_name, token)) { + continue; + } + if (matched_path != NULL) { + return PM3_EOVFLOW; + } + matched_path = paths[i]; + } + + if (matched_path != NULL) { + return duox_load_certificate_anchor_from_file_path(matched_path, anchor); + } + + return PM3_EINVARG; +} + +int duox_load_certificate_anchor_from_input(const char *input, const char *anchor_store_dir, + duox_certificate_anchor_t *anchor) { + if (input == NULL || anchor == NULL) { + return PM3_EINVARG; + } + + char normalized[DUOX_CERTIFICATE_ANCHOR_INPUT_LEN] = {0}; + snprintf(normalized, sizeof(normalized), "%s", input); + duox_trim_ascii_inplace(normalized); + if (normalized[0] == '\0') { + return PM3_EINVARG; + } + + char *resolved_path = NULL; + if (searchFile(&resolved_path, RESOURCES_SUBDIR, normalized, "", true) == PM3_SUCCESS) { + int res = PM3_EINVARG; + if (duox_path_is_regular_file(resolved_path)) { + res = duox_load_certificate_anchor_from_file_path(resolved_path, anchor); + } + free(resolved_path); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + } + + if (anchor_store_dir != NULL && anchor_store_dir[0] != '\0') { + int res = duox_load_named_certificate_anchor_from_store(normalized, anchor_store_dir, anchor); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + } + + int res = duox_load_certificate_anchor_from_certificate_input(normalized, NULL, "inline certificate", anchor); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + + return duox_load_certificate_anchor_from_pubkey_input(normalized, NULL, "inline public key", anchor); +} + +int duox_load_certificate_anchors_from_store(const char *anchor_store_dir, + duox_certificate_anchor_t *anchors, + size_t max_anchors, size_t *out_count) { + if (anchor_store_dir == NULL || anchors == NULL || out_count == NULL) { + return PM3_EINVARG; + } + + char paths[DUOX_CERTIFICATE_ANCHOR_MAX_PATHS][DUOX_CERTIFICATE_ANCHOR_PATH_LEN] = {{0}}; + size_t path_count = 0; + int res = duox_collect_certificate_anchor_paths(anchor_store_dir, paths, ARRAYLEN(paths), &path_count); + if (res != PM3_SUCCESS) { + return res; + } + if (path_count == 0) { + return PM3_EFILE; + } + + size_t count = 0; + for (size_t i = 0; i < path_count && count < max_anchors; i++) { + res = duox_load_certificate_anchor_from_file_path(paths[i], &anchors[count]); + if (res == PM3_SUCCESS) { + count++; + } + } + + if (count == 0) { + return PM3_EFILE; + } + + *out_count = count; + return PM3_SUCCESS; +} + +static int duox_asn1_seq_full_length(const uint8_t *data, size_t datalen, size_t *fulllen) { + if (data == NULL || fulllen == NULL || datalen < 2) { + return PM3_EINVARG; + } + + unsigned char *p = (unsigned char *)data; + const unsigned char *end = data + datalen; + size_t body_len = 0; + int res = mbedtls_asn1_get_tag(&p, end, &body_len, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE); + if (res != 0 || body_len > (size_t)(end - p)) { + return PM3_ESOFT; + } + + *fulllen = (size_t)(p - data) + body_len; + return PM3_SUCCESS; +} + +static int duox_parse_x509_der_crt(const uint8_t *data, size_t data_len, bool verbose, mbedtls_x509_crt *cert) { + if (data == NULL || data_len == 0 || cert == NULL) { + return PM3_EINVARG; + } + + size_t detected_len = 0; + if (duox_asn1_seq_full_length(data, data_len, &detected_len) != PM3_SUCCESS || detected_len != data_len) { + return PM3_ENODATA; + } + + int xres = mbedtls_x509_crt_parse_der(cert, data, data_len); + if (xres == 0) { + return PM3_SUCCESS; + } + + if (verbose) { + PrintAndLogEx(INFO, "X.509 parser rejected certificate data (0x%x)", + (xres < 0) ? -xres : xres); + } + return PM3_ENODATA; +} + +static int duox_x509_public_key_from_cert(const mbedtls_x509_crt *cert, + mbedtls_ecp_group_id *curveid, + uint8_t *out_pub, size_t out_pub_capacity, size_t *out_pub_len) { + if (cert == NULL || out_pub == NULL || out_pub_len == NULL || out_pub_capacity == 0) { + return PM3_EINVARG; + } + + if (!mbedtls_pk_can_do(&cert->pk, MBEDTLS_PK_ECKEY) && + !mbedtls_pk_can_do(&cert->pk, MBEDTLS_PK_ECDSA)) { + return PM3_ENOTIMPL; + } + + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(cert->pk); + if (ec == NULL) { + return PM3_ESOFT; + } + + size_t coord_len = (ec->grp.nbits + 7) / 8; + size_t expected_len = 1 + (2 * coord_len); + if (expected_len > out_pub_capacity) { + return PM3_EOVFLOW; + } + + size_t written = 0; + if (mbedtls_ecp_point_write_binary(&ec->grp, &ec->Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &written, out_pub, out_pub_capacity) != 0 || + written != expected_len) { + return PM3_ESOFT; + } + + if (curveid != NULL) { + *curveid = ec->grp.id; + } + *out_pub_len = written; + return PM3_SUCCESS; +} + +static int duox_cert_info_from_x509_crt(const mbedtls_x509_crt *cert, duox_cert_info_t *out) { + if (cert == NULL || out == NULL) { + return PM3_EINVARG; + } + + duox_cert_info_t info = {0}; + info.format = DUOX_CERTIFICATE_FORMAT_X509; + mbedtls_x509_dn_gets(info.issuer, sizeof(info.issuer), &cert->issuer); + mbedtls_x509_dn_gets(info.subject, sizeof(info.subject), &cert->subject); + snprintf(info.serial, sizeof(info.serial), "%s", sprint_hex_inrow(cert->serial.p, cert->serial.len)); + snprintf(info.valid_from, sizeof(info.valid_from), "%04d-%02d-%02d %02d:%02d:%02d", + cert->valid_from.year, cert->valid_from.mon, cert->valid_from.day, + cert->valid_from.hour, cert->valid_from.min, cert->valid_from.sec); + snprintf(info.valid_to, sizeof(info.valid_to), "%04d-%02d-%02d %02d:%02d:%02d", + cert->valid_to.year, cert->valid_to.mon, cert->valid_to.day, + cert->valid_to.hour, cert->valid_to.min, cert->valid_to.sec); + + int res = duox_x509_public_key_from_cert(cert, &info.curveid, info.pubkey, sizeof(info.pubkey), &info.pubkey_len); + *out = info; + return res; +} + +int duox_parse_x509_certificate(const uint8_t *data, size_t data_len, + bool verbose, duox_cert_info_t *out) { + if (data == NULL || data_len == 0 || out == NULL) { + return PM3_EINVARG; + } + + mbedtls_x509_crt cert; + mbedtls_x509_crt_init(&cert); + int res = duox_parse_x509_der_crt(data, data_len, verbose, &cert); + if (res == PM3_SUCCESS) { + res = (duox_cert_info_from_x509_crt(&cert, out) == PM3_SUCCESS) ? PM3_SUCCESS : PM3_ECRYPTO; + } + mbedtls_x509_crt_free(&cert); + return res; +} + +static int duox_verify_x509_signature_with_public_key(const mbedtls_x509_crt *cert, + mbedtls_ecp_group_id signer_curveid, + const uint8_t *signer_pubkey, size_t signer_pubkey_len, + bool verbose) { + if (cert == NULL || signer_pubkey == NULL || signer_pubkey_len == 0 || + signer_curveid == MBEDTLS_ECP_DP_NONE) { + return PM3_EINVARG; + } + if (cert->sig_md == MBEDTLS_MD_NONE || mbedtls_md_info_from_type(cert->sig_md) == NULL) { + if (verbose) { + PrintAndLogEx(WARNING, "Unsupported X.509 signature hash"); + } + return PM3_ENOTIMPL; + } + if (cert->sig_pk != MBEDTLS_PK_ECDSA) { + if (verbose) { + PrintAndLogEx(WARNING, "Unsupported X.509 signature public key type (need ECDSA)"); + } + return PM3_ENOTIMPL; + } + + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(cert->sig_md); + uint8_t digest[MBEDTLS_MD_MAX_SIZE] = {0}; + size_t digest_len = mbedtls_md_get_size(md_info); + if (digest_len == 0 || digest_len > sizeof(digest) || + mbedtls_md(md_info, cert->tbs.p, cert->tbs.len, digest) != 0) { + return PM3_ESOFT; + } + + mbedtls_ecdsa_context ctx; + mbedtls_ecdsa_init(&ctx); + int status = PM3_ESOFT; + if (mbedtls_ecp_group_load(&ctx.grp, signer_curveid) != 0) { + goto out; + } + size_t expected_key_len = 1 + 2 * ((ctx.grp.nbits + 7) / 8); + if (signer_pubkey_len != expected_key_len || + mbedtls_ecp_point_read_binary(&ctx.grp, &ctx.Q, signer_pubkey, signer_pubkey_len) != 0 || + mbedtls_ecp_check_pubkey(&ctx.grp, &ctx.Q) != 0) { + status = PM3_EINVARG; + goto out; + } + + status = (mbedtls_ecdsa_read_signature(&ctx, digest, digest_len, cert->sig.p, cert->sig.len) == 0) ? PM3_SUCCESS : PM3_ESOFT; + +out: + mbedtls_ecdsa_free(&ctx); + return status; +} + +static int duox_verify_x509_signature_with_anchor(const mbedtls_x509_crt *cert, + const duox_certificate_anchor_t *anchor, + bool verbose) { + if (cert == NULL || anchor == NULL) { + return PM3_EINVARG; + } + + mbedtls_ecp_group_id curveid = MBEDTLS_ECP_DP_NONE; + const uint8_t *pubkey = NULL; + size_t pubkey_len = 0; + int res = duox_certificate_anchor_public_key(anchor, &curveid, &pubkey, &pubkey_len); + if (res != PM3_SUCCESS) { + return res; + } + + return duox_verify_x509_signature_with_public_key(cert, curveid, pubkey, pubkey_len, verbose); +} + +int duox_verify_x509_certificate_with_anchors(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *anchors, size_t anchor_count, + bool verbose, duox_cert_info_t *out, + size_t *matched_index) { + if (data == NULL || data_len == 0 || anchors == NULL || anchor_count == 0 || + anchor_count > DUOX_MAX_CERTIFICATE_ANCHORS || out == NULL) { + return PM3_EINVARG; + } + + mbedtls_x509_crt cert; + mbedtls_x509_crt_init(&cert); + int res = duox_parse_x509_der_crt(data, data_len, verbose, &cert); + if (res != PM3_SUCCESS) { + mbedtls_x509_crt_free(&cert); + return res; + } + + duox_cert_info_t info = {0}; + res = duox_cert_info_from_x509_crt(&cert, &info); + *out = info; + if (res != PM3_SUCCESS) { + mbedtls_x509_crt_free(&cert); + return PM3_ECRYPTO; + } + + bool tried[DUOX_MAX_CERTIFICATE_ANCHORS] = {0}; + int verify_res = PM3_ECRYPTO; + if (info.issuer[0] != '\0') { + for (size_t i = 0; i < anchor_count; i++) { + const char *subject = duox_certificate_anchor_subject(&anchors[i]); + if (subject[0] == '\0' || !str_contains_case_insensitive(info.issuer, subject)) { + continue; + } + + tried[i] = true; + if (duox_verify_x509_signature_with_anchor(&cert, &anchors[i], verbose) == PM3_SUCCESS) { + if (matched_index != NULL) { + *matched_index = i; + } + verify_res = PM3_SUCCESS; + goto out; + } + } + } + + for (size_t i = 0; i < anchor_count; i++) { + if (tried[i]) { + continue; + } + + if (duox_verify_x509_signature_with_anchor(&cert, &anchors[i], verbose) == PM3_SUCCESS) { + if (matched_index != NULL) { + *matched_index = i; + } + verify_res = PM3_SUCCESS; + goto out; + } + } + +out: + mbedtls_x509_crt_free(&cert); + return verify_res; +} + +static bool duox_format_bcd_date(const uint8_t *data, size_t data_len, char *out, size_t out_len) { + if (data == NULL || data_len != 4 || out == NULL || out_len < 11) { + return false; + } + for (size_t i = 0; i < data_len; i++) { + if ((data[i] >> 4) > 9 || (data[i] & 0x0F) > 9) { + return false; + } + } + snprintf(out, out_len, "%02X%02X-%02X-%02X", data[0], data[1], data[2], data[3]); + return true; +} + +typedef struct { + uint32_t tag; + size_t tl_offset; + size_t value_offset; + size_t len; + size_t total_len; + const uint8_t *value; +} duox_gp_vde_tlv_t; + +static int duox_gp_vde_parse_tlv(const uint8_t *data, size_t data_len, size_t *offset, duox_gp_vde_tlv_t *tlv) { + if (data == NULL || offset == NULL || tlv == NULL || *offset >= data_len) { + return PM3_ENODATA; + } + + size_t idx = *offset; + uint32_t tag = data[idx++]; + if ((tag & 0x1F) == 0x1F) { + do { + if (idx >= data_len || tag > 0x00FFFFFFU) { + return PM3_ENODATA; + } + uint8_t b = data[idx++]; + tag = (tag << 8) | b; + if ((b & 0x80) == 0) { + break; + } + } while (true); + } + + size_t len = 0; + if (idx >= data_len || data[idx] == 0x80 || + ((data[idx] & 0x80) != 0 && (data[idx] & 0x7F) > sizeof(size_t))) { + return PM3_ENODATA; + } + if (asn1_get_tag_length(data, &len, &idx, data_len) != 0 || len > data_len - idx) { + return PM3_ENODATA; + } + + tlv->tag = tag; + tlv->tl_offset = *offset; + tlv->value_offset = idx; + tlv->len = len; + tlv->total_len = (idx - *offset) + len; + tlv->value = data + idx; + *offset = idx + len; + return PM3_SUCCESS; +} + +static int duox_verify_gp_vde_signature(const uint8_t *signed_data, size_t signed_data_len, + const uint8_t signature_rs[DUOX_VDE_SIG_LEN], + const uint8_t *ca_id, size_t ca_id_len, + const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count, + bool verbose, size_t *matched_index) { + if (signed_data == NULL || signed_data_len == 0 || signature_rs == NULL || + ca_anchors == NULL || ca_anchor_count == 0 || ca_anchor_count > DUOX_MAX_CERTIFICATE_ANCHORS) { + return PM3_EINVARG; + } + + char ca_id_hex[32] = {0}; + snprintf(ca_id_hex, sizeof(ca_id_hex), "%s", sprint_hex_inrow(ca_id, ca_id_len)); + + bool tried[DUOX_MAX_CERTIFICATE_ANCHORS] = {0}; + for (int pass = 0; pass < 2; pass++) { + for (size_t i = 0; i < ca_anchor_count; i++) { + if (tried[i]) { + continue; + } + if (pass == 0 && ca_id_hex[0] != '\0' && + strstr(duox_certificate_anchor_subject(&ca_anchors[i]), ca_id_hex) == NULL) { + continue; + } + + mbedtls_ecp_group_id curveid = MBEDTLS_ECP_DP_NONE; + const uint8_t *pubkey = NULL; + size_t pubkey_len = 0; + if (duox_certificate_anchor_public_key(&ca_anchors[i], &curveid, &pubkey, &pubkey_len) != PM3_SUCCESS || + curveid != MBEDTLS_ECP_DP_BP256R1 || pubkey_len != 65) { + continue; + } + + tried[i] = true; + int res = ecdsa_signature_r_s_verify(MBEDTLS_ECP_DP_BP256R1, + (uint8_t *)pubkey, + (uint8_t *)signed_data, + (int)signed_data_len, + (uint8_t *)signature_rs, + DUOX_VDE_SIG_LEN, + true); + if (res == PM3_SUCCESS) { + if (matched_index != NULL) { + *matched_index = i; + } + return PM3_SUCCESS; + } + if (verbose) { + PrintAndLogEx(INFO, "GP VDE signature did not verify with %s", duox_certificate_anchor_display_name(&ca_anchors[i])); + } + } + } + + return PM3_ECRYPTO; +} + +int duox_parse_gp_vde_certificate(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count, + bool verify_signature, + bool verbose, duox_cert_info_t *out, size_t *matched_index) { + if (data == NULL || data_len == 0 || out == NULL) { + return PM3_EINVARG; + } + if (verify_signature && (ca_anchors == NULL || ca_anchor_count == 0)) { + return PM3_EINVARG; + } + + size_t offset = 0; + duox_gp_vde_tlv_t outer = {0}; + int res = duox_gp_vde_parse_tlv(data, data_len, &offset, &outer); + if (res != PM3_SUCCESS || outer.tag != 0x7F21) { + return PM3_ENODATA; + } + + size_t cert_end = outer.value_offset + outer.len; + const uint8_t *ca_id = NULL; + size_t ca_id_len = 0; + const uint8_t *serial_uid = NULL; + size_t serial_uid_len = 0; + const uint8_t *subject_id = NULL; + size_t subject_id_len = 0; + const uint8_t *signature_rs = NULL; + const uint8_t *public_key = NULL; + size_t signed_start = SIZE_MAX; + size_t signed_end = 0; + bool curve_ok = false; + char valid_from[DUOX_CERT_TEXT_LEN] = {0}; + char valid_to[DUOX_CERT_TEXT_LEN] = {0}; + + offset = outer.value_offset; + while (offset < cert_end) { + duox_gp_vde_tlv_t tlv = {0}; + res = duox_gp_vde_parse_tlv(data, cert_end, &offset, &tlv); + if (res != PM3_SUCCESS) { + return res; + } + + switch (tlv.tag) { + case 0x93: + serial_uid = tlv.value; + serial_uid_len = tlv.len; + signed_start = tlv.tl_offset; + break; + case 0x42: + ca_id = tlv.value; + ca_id_len = tlv.len; + break; + case 0x5F20: + subject_id = tlv.value; + subject_id_len = tlv.len; + break; + case 0x5F25: + duox_format_bcd_date(tlv.value, tlv.len, valid_from, sizeof(valid_from)); + break; + case 0x5F24: + duox_format_bcd_date(tlv.value, tlv.len, valid_to, sizeof(valid_to)); + break; + case 0x7F49: { + size_t pki_end = tlv.value_offset + tlv.len; + size_t pki_offset = tlv.value_offset; + while (pki_offset < pki_end) { + duox_gp_vde_tlv_t ktlv = {0}; + res = duox_gp_vde_parse_tlv(data, pki_end, &pki_offset, &ktlv); + if (res != PM3_SUCCESS) { + return res; + } + if (ktlv.tag == 0xB0 && ktlv.len == 65 && ktlv.value[0] == 0x04) { + public_key = ktlv.value; + } else if (ktlv.tag == 0xF0 && ktlv.len == 1 && ktlv.value[0] == 0x03) { + curve_ok = true; + } + } + signed_end = tlv.tl_offset + tlv.total_len; + break; + } + case 0x5F37: + if (tlv.len == DUOX_VDE_SIG_LEN) { + signature_rs = tlv.value; + } + break; + default: + break; + } + } + + if (signed_start == SIZE_MAX || signed_end <= signed_start || public_key == NULL || !curve_ok || + signature_rs == NULL || ca_id == NULL || subject_id == NULL) { + return PM3_ENODATA; + } + + duox_cert_info_t info = {0}; + info.format = DUOX_CERTIFICATE_FORMAT_GP_VDE; + info.curveid = MBEDTLS_ECP_DP_BP256R1; + memcpy(info.pubkey, public_key, 65); + info.pubkey_len = 65; + + char ca_id_hex[32] = {0}; + char serial_hex[32] = {0}; + char subject_hex[32] = {0}; + snprintf(ca_id_hex, sizeof(ca_id_hex), "%s", sprint_hex_inrow(ca_id, ca_id_len)); + snprintf(serial_hex, sizeof(serial_hex), "%s", sprint_hex_inrow(serial_uid, serial_uid_len)); + snprintf(subject_hex, sizeof(subject_hex), "%s", sprint_hex_inrow(subject_id, subject_id_len)); + snprintf(info.issuer, sizeof(info.issuer), "CA ID %s", ca_id_hex); + snprintf(info.subject, sizeof(info.subject), "Subject ID %s", subject_hex); + snprintf(info.serial, sizeof(info.serial), "%s", serial_hex); + snprintf(info.valid_from, sizeof(info.valid_from), "%s", valid_from); + snprintf(info.valid_to, sizeof(info.valid_to), "%s", valid_to); + snprintf(info.certificate_profile_note, sizeof(info.certificate_profile_note), "VDE CA ID %s", ca_id_hex); + *out = info; + + if (!verify_signature) { + return PM3_SUCCESS; + } + + return duox_verify_gp_vde_signature(data + signed_start, signed_end - signed_start, + signature_rs, ca_id, ca_id_len, + ca_anchors, ca_anchor_count, + verbose, matched_index); +} + +static int duox_parse_or_verify_x509_payload(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count, + bool verify_signature, + bool verbose, duox_cert_info_t *out, size_t *matched_index) { + return verify_signature + ? duox_verify_x509_certificate_with_anchors(data, data_len, + ca_anchors, ca_anchor_count, + verbose, out, matched_index) + : duox_parse_x509_certificate(data, data_len, verbose, out); +} + +static int duox_verify_x509_certificate_variants(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count, + bool verify_signature, + bool verbose, duox_cert_info_t *out, size_t *matched_index) { + if (data == NULL || data_len == 0 || out == NULL) { + return PM3_EINVARG; + } + + int res = duox_parse_or_verify_x509_payload(data, data_len, + ca_anchors, ca_anchor_count, + verify_signature, + verbose, out, matched_index); + if (res != PM3_ENODATA) { + return res; + } + + size_t payload_len = 0; + if (data_len > 3) { + payload_len = (size_t)data[0] | ((size_t)data[1] << 8) | ((size_t)data[2] << 16); + if (payload_len > 0 && payload_len <= data_len - 3) { + if (verbose) { + PrintAndLogEx(INFO, "Trying 3-byte length-prefixed X.509 payload (%zu bytes)", payload_len); + } + res = duox_parse_or_verify_x509_payload(data + 3, payload_len, + ca_anchors, ca_anchor_count, + verify_signature, + verbose, out, matched_index); + if (res != PM3_ENODATA) { + return res; + } + } + } + + if (data_len > 2 && data[0] == 0x30) { + size_t asn1_len = 0; + size_t asn1_offset = 1; + if (asn1_get_tag_length(data, &asn1_len, &asn1_offset, data_len) == 0 && + asn1_len <= data_len - asn1_offset) { + payload_len = asn1_offset + asn1_len; + } + } + if (payload_len > 0 && payload_len < data_len) { + if (verbose) { + PrintAndLogEx(INFO, "Trying zero-padded X.509 payload (%zu bytes)", payload_len); + } + return duox_parse_or_verify_x509_payload(data, payload_len, + ca_anchors, ca_anchor_count, + verify_signature, + verbose, out, matched_index); + } + + return PM3_ENODATA; +} + +int duox_parse_or_verify_certificate_variants(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count, + bool verify_signature, + bool verbose, duox_cert_info_t *out, size_t *matched_index) { + int x509_res = duox_verify_x509_certificate_variants(data, data_len, + ca_anchors, ca_anchor_count, + verify_signature, + verbose, out, matched_index); + if (x509_res != PM3_ENODATA) { + return x509_res; + } + + int gp_res = duox_parse_gp_vde_certificate(data, data_len, + ca_anchors, ca_anchor_count, + verify_signature, + verbose, out, matched_index); + if (gp_res != PM3_ENODATA) { + return gp_res; + } + + return PM3_ENODATA; +} diff --git a/client/src/crypto/duoxcrypto.h b/client/src/crypto/duoxcrypto.h new file mode 100644 index 000000000..4b492c142 --- /dev/null +++ b/client/src/crypto/duoxcrypto.h @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 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 General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// MIFARE DUOX certificate crypto helpers +//----------------------------------------------------------------------------- + +#ifndef DUOXCRYPTO_H +#define DUOXCRYPTO_H + +#include +#include +#include +#include + +#define DUOX_DEFAULT_CA_DIR "duox_trust" +#define DUOX_EC_PUBKEY_MAX_LEN 133 +#define DUOX_MAX_CERTIFICATE_ANCHORS 16 +#define DUOX_CERTIFICATE_ANCHOR_NAME_LEN 96 +#define DUOX_CERTIFICATE_ANCHOR_MAX_PATHS 128 +#define DUOX_CERTIFICATE_ANCHOR_PATH_LEN 1024 +#define DUOX_CERT_TEXT_LEN 256 +#define DUOX_VDE_CHALLENGE_LEN 32 +#define DUOX_VDE_SIG_LEN 64 + +typedef enum { + DUOX_CERTIFICATE_FORMAT_UNKNOWN = 0, + DUOX_CERTIFICATE_FORMAT_X509, + DUOX_CERTIFICATE_FORMAT_GP_VDE, +} duox_certificate_format_t; + +typedef struct { + duox_certificate_format_t format; + mbedtls_ecp_group_id curveid; + uint8_t pubkey[DUOX_EC_PUBKEY_MAX_LEN]; + size_t pubkey_len; + char issuer[DUOX_CERT_TEXT_LEN]; + char subject[DUOX_CERT_TEXT_LEN]; + char serial[DUOX_CERT_TEXT_LEN]; + char valid_from[DUOX_CERT_TEXT_LEN]; + char valid_to[DUOX_CERT_TEXT_LEN]; + char certificate_profile_note[DUOX_CERT_TEXT_LEN]; +} duox_cert_info_t; + +typedef struct { + mbedtls_ecp_group_id curveid; + uint8_t pubkey[DUOX_EC_PUBKEY_MAX_LEN]; + size_t pubkey_len; +} duox_ec_public_key_t; + +typedef enum { + DUOX_CERTIFICATE_ANCHOR_MATERIAL_NONE = 0, + DUOX_CERTIFICATE_ANCHOR_MATERIAL_PUBLIC_KEY, + DUOX_CERTIFICATE_ANCHOR_MATERIAL_CERT, +} duox_certificate_anchor_material_type_t; + +typedef struct { + char name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN]; + char source[DUOX_CERTIFICATE_ANCHOR_PATH_LEN]; + duox_certificate_anchor_material_type_t type; + union { + duox_ec_public_key_t key; + duox_cert_info_t cert; + } material; +} duox_certificate_anchor_t; + +const char *duox_certificate_format_name(duox_certificate_format_t format); +const char *duox_cert_info_format_name(const duox_cert_info_t *cert); +int duox_certificate_anchor_public_key(const duox_certificate_anchor_t *anchor, + mbedtls_ecp_group_id *curveid, + const uint8_t **pubkey, size_t *pubkey_len); +const char *duox_certificate_anchor_subject(const duox_certificate_anchor_t *anchor); +const char *duox_certificate_anchor_display_name(const duox_certificate_anchor_t *anchor); +int duox_load_certificate_anchor_from_input(const char *input, const char *anchor_store_dir, + duox_certificate_anchor_t *anchor); +int duox_load_certificate_anchors_from_store(const char *anchor_store_dir, + duox_certificate_anchor_t *anchors, + size_t max_anchors, size_t *out_count); +int duox_parse_x509_certificate(const uint8_t *data, size_t data_len, + bool verbose, duox_cert_info_t *out); +int duox_verify_x509_certificate_with_anchors(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *anchors, size_t anchor_count, + bool verbose, duox_cert_info_t *out, + size_t *matched_index); +int duox_parse_gp_vde_certificate(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count, + bool verify_signature, + bool verbose, duox_cert_info_t *out, size_t *matched_index); +int duox_parse_or_verify_certificate_variants(const uint8_t *data, size_t data_len, + const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count, + bool verify_signature, + bool verbose, duox_cert_info_t *out, size_t *matched_index); + +#endif /* DUOXCRYPTO_H */ diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index c70adbdd9..67dcc1b49 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -527,6 +527,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf mfdes value" }, { 0, "hf mfdes clearrecfile" }, { 1, "hf mfdes makemfclicense" }, + { 0, "hf mfdes verifycert" }, { 0, "hf mfdes intauth" }, { 0, "hf mfdes vdesign" }, { 1, "hf mfdes test" }, diff --git a/client/src/util.c b/client/src/util.c index d15ec3d50..68e9ec401 100644 --- a/client/src/util.c +++ b/client/src/util.c @@ -1391,6 +1391,71 @@ void strn_upper(char *s, size_t n) { s[i] = toupper(s[i]); } } + +static int char_compare_case_insensitive(char a, char b) { + return tolower((unsigned char)a) - tolower((unsigned char)b); +} + +bool str_equal_case_insensitive(const char *a, const char *b) { + if (a == NULL || b == NULL) { + return false; + } + + while (*a != '\0' && *b != '\0') { + if (char_compare_case_insensitive(*a, *b) != 0) { + return false; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +bool str_startswith_case_insensitive(const char *s, const char *pre) { + if (s == NULL || pre == NULL) { + return false; + } + + while (*pre != '\0') { + if (*s == '\0' || char_compare_case_insensitive(*s, *pre) != 0) { + return false; + } + s++; + pre++; + } + + return true; +} + +bool str_contains_case_insensitive(const char *s, const char *needle) { + if (s == NULL || needle == NULL) { + return false; + } + + size_t needle_len = strlen(needle); + if (needle_len == 0) { + return true; + } + + size_t s_len = strlen(s); + if (needle_len > s_len) { + return false; + } + + for (size_t i = 0; i <= (s_len - needle_len); i++) { + size_t j = 0; + while (j < needle_len && char_compare_case_insensitive(s[i + j], needle[j]) == 0) { + j++; + } + if (j == needle_len) { + return true; + } + } + + return false; +} + // check for prefix in string bool str_startswith(const char *s, const char *pre) { return strncmp(pre, s, strlen(pre)) == 0; @@ -1499,6 +1564,28 @@ size_t str_nlen(const char *src, size_t maxlen) { return len; } +size_t str_copy(char *dst, size_t dst_size, const char *src) { + if (src == NULL) { + if (dst != NULL && dst_size > 0) { + dst[0] = '\0'; + } + return 0; + } + + size_t src_len = strlen(src); + if (dst == NULL || dst_size == 0) { + return src_len; + } + + size_t copy_len = src_len; + if (copy_len >= dst_size) { + copy_len = dst_size - 1; + } + memcpy(dst, src, copy_len); + dst[copy_len] = '\0'; + return src_len; +} + static bool str_regex_atom_matches(char atom, bool escaped, char c) { if (!escaped && atom == '.') { return true; diff --git a/client/src/util.h b/client/src/util.h index 036a67d5c..1ba9f8763 100644 --- a/client/src/util.h +++ b/client/src/util.h @@ -166,6 +166,9 @@ void str_lower(char *s); // converts string to lower case void str_upper(char *s); // converts string to UPPER case void strn_upper(char *s, size_t n); +bool str_equal_case_insensitive(const char *a, const char *b); +bool str_startswith_case_insensitive(const char *s, const char *pre); +bool str_contains_case_insensitive(const char *s, const char *needle); bool str_startswith(const char *s, const char *pre); // check for prefix in string bool str_endswith(const char *s, const char *suffix); // check for suffix in string void clean_ascii(unsigned char *buf, size_t len); @@ -181,6 +184,7 @@ void str_trim(char *s); char *str_dup(const char *src); char *str_ndup(const char *src, size_t len); size_t str_nlen(const char *src, size_t maxlen); +size_t str_copy(char *dst, size_t dst_size, const char *src); // Lightweight regex subset: // - supported metacharacters: '^' (start), '$' (end), '.' (any char), '*' (zero or more) // - escaping: '\\' to match the following char literally