diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 8759c00f3..6252393e8 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -337,6 +337,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdlfzx8211.c ${PM3_ROOT}/client/src/cmdmain.c ${PM3_ROOT}/client/src/cmdnfc.c + ${PM3_ROOT}/client/src/cmdvas.c ${PM3_ROOT}/client/src/cmdparser.c ${PM3_ROOT}/client/src/cmdpiv.c ${PM3_ROOT}/client/src/cmdscript.c diff --git a/client/deps/mbedtls.cmake b/client/deps/mbedtls.cmake index 40929e1ea..784f0f94d 100644 --- a/client/deps/mbedtls.cmake +++ b/client/deps/mbedtls.cmake @@ -9,6 +9,7 @@ add_library(pm3rrg_rdv4_mbedtls STATIC ../../common/mbedtls/entropy.c ../../common/mbedtls/error.c ../../common/mbedtls/ecp.c + ../../common/mbedtls/ecdh.c ../../common/mbedtls/ecp_curves.c ../../common/mbedtls/certs.c ../../common/mbedtls/camellia.c diff --git a/client/src/cmdnfc.c b/client/src/cmdnfc.c index eadfa7161..cb9979449 100644 --- a/client/src/cmdnfc.c +++ b/client/src/cmdnfc.c @@ -28,6 +28,7 @@ #include "cmdhfst25ta.h" #include "cmdhfthinfilm.h" #include "cmdhftopaz.h" +#include "cmdvas.h" #include "cmdnfc.h" #include "fileutils.h" #include "mifare/mifaredefault.h" @@ -421,6 +422,7 @@ static command_t CommandTable[] = { // {"type5", CmdNFCType5, AlwaysAvailable, "{ NFC Forum Tag Type 5... }"}, {"mf", CmdNFCMF, AlwaysAvailable, "{ NFC Type MIFARE Classic/Plus Tag... }"}, {"barcode", CmdNFCBarcode, AlwaysAvailable, "{ NFC Barcode Tag... }"}, + {"vas", CmdVAS, AlwaysAvailable, "{ Apple Value Added Service }"}, // {"--------", CmdHelp, AlwaysAvailable, "--------------------- " _CYAN_("NFC peer-to-peer") " ------------"}, // {"isodep", CmdISODEP, AlwaysAvailable, "{ ISO-DEP protocol... }"}, // {"llcp", CmdNFCLLCP, AlwaysAvailable, "{ Logical Link Control Protocol... }"}, diff --git a/client/src/cmdvas.c b/client/src/cmdvas.c new file mode 100644 index 000000000..a0e0b0522 --- /dev/null +++ b/client/src/cmdvas.c @@ -0,0 +1,431 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// An implementation of the Value Added Service protocol +//----------------------------------------------------------------------------- + +#include "cmdvas.h" +#include "cliparser.h" +#include "cmdparser.h" +#include "comms.h" +#include "ansi.h" +#include "cmdhf14a.h" +#include "emv/tlv.h" +#include "iso7816/apduinfo.h" +#include "ui.h" +#include "util.h" +#include "util_posix.h" +#include "iso7816/iso7816core.h" +#include "stddef.h" +#include "stdbool.h" +#include "mifare.h" +#include +#include +#include "crypto/libpcrypto.h" +#include "fileutils.h" +#include "mbedtls/ecp.h" +#include "mbedtls/bignum.h" +#include "mbedtls/ecdh.h" + +uint8_t ecpData[] = { 0x6a, 0x01, 0x00, 0x00, 0x04 }; +uint8_t aid[] = { 0x4f, 0x53, 0x45, 0x2e, 0x56, 0x41, 0x53, 0x2e, 0x30, 0x31 }; +uint8_t getVasUrlOnlyP2 = 0x00; +uint8_t getVasFullReqP2 = 0x01; + +static int ParseSelectVASResponse(uint8_t *response, size_t resLen, bool verbose) { + struct tlvdb *tlvRoot = tlvdb_parse_multi(response, resLen); + + struct tlvdb *versionTlv = tlvdb_find_full(tlvRoot, 0x9F21); + if (versionTlv == NULL) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + const struct tlv *version = tlvdb_get_tlv(versionTlv); + if (version->len != 2) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + if (verbose) { + PrintAndLogEx(INFO, "Mobile VAS application version: %d.%d", version->value[0], version->value[1]); + } + if (version->value[0] != 0x01 || version->value[1] != 0x00) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + + struct tlvdb *capabilitiesTlv = tlvdb_find_full(tlvRoot, 0x9F23); + if (capabilitiesTlv == NULL) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + const struct tlv *capabilities = tlvdb_get_tlv(capabilitiesTlv); + if (capabilities->len != 4 + || capabilities->value[0] != 0x00 + || capabilities->value[1] != 0x00 + || capabilities->value[2] != 0x00 + || (capabilities->value[3] & 8) == 0) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + + tlvdb_free(tlvRoot); + return PM3_SUCCESS; +} + +static int CreateGetVASDataCommand(uint8_t *pidHash, const char *url, size_t urlLen, uint8_t *out, int *outLen) { + if (pidHash == NULL && url == NULL) { + PrintAndLogEx(FAILED, "Must provide a Pass Type ID or a URL"); + return PM3_EINVARG; + } + + if (url != NULL && urlLen > 256) { + PrintAndLogEx(FAILED, "URL must be less than 256 characters"); + return PM3_EINVARG; + } + + uint8_t p2 = pidHash == NULL ? getVasUrlOnlyP2 : getVasFullReqP2; + + size_t reqTlvLen = 19 + (pidHash != NULL ? 35 : 0) + (url != NULL ? 3 + urlLen : 0); + uint8_t *reqTlv = calloc(reqTlvLen, sizeof(uint8_t)); + + uint8_t version[] = {0x9F, 0x22, 0x02, 0x01, 0x00}; + memcpy(reqTlv, version, sizeof(version)); + + uint8_t unknown[] = {0x9F, 0x28, 0x04, 0x00, 0x00, 0x00, 0x00}; + memcpy(reqTlv + sizeof(version), unknown, sizeof(unknown)); + + uint8_t terminalCapabilities[] = {0x9F, 0x26, 0x04, 0x00, 0x00, 0x00, 0x02}; + memcpy(reqTlv + sizeof(version) + sizeof(unknown), terminalCapabilities, sizeof(terminalCapabilities)); + + if (pidHash != NULL) { + size_t offset = sizeof(version) + sizeof(unknown) + sizeof(terminalCapabilities); + reqTlv[offset] = 0x9F; + reqTlv[offset + 1] = 0x25; + reqTlv[offset + 2] = 32; + memcpy(reqTlv + offset + 3, pidHash, 32); + } + + if (url != NULL) { + size_t offset = sizeof(version) + sizeof(unknown) + sizeof(terminalCapabilities) + (pidHash != NULL ? 35 : 0); + reqTlv[offset] = 0x9F; + reqTlv[offset + 1] = 0x29; + reqTlv[offset + 2] = urlLen; + memcpy(reqTlv + offset + 3, url, urlLen); + } + + out[0] = 0x80; + out[1] = 0xCA; + out[2] = 0x01; + out[3] = p2; + out[4] = reqTlvLen; + memcpy(out + 5, reqTlv, reqTlvLen); + out[5 + reqTlvLen] = 0x00; + + *outLen = 6 + reqTlvLen; + + free(reqTlv); + return PM3_SUCCESS; +} + +static int ParseGetVASDataResponse(uint8_t *res, size_t resLen, uint8_t *cryptogram, size_t *cryptogramLen) { + struct tlvdb *tlvRoot = tlvdb_parse_multi(res, resLen); + + struct tlvdb *cryptogramTlvdb = tlvdb_find_full(tlvRoot, 0x9F27); + if (cryptogramTlvdb == NULL) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + const struct tlv *cryptogramTlv = tlvdb_get_tlv(cryptogramTlvdb); + + memcpy(cryptogram, cryptogramTlv->value, cryptogramTlv->len); + *cryptogramLen = cryptogramTlv->len; + + tlvdb_free(tlvRoot); + return PM3_SUCCESS; +} + +static int LoadReaderPrivateKey(uint8_t *buf, size_t bufLen, mbedtls_ecp_keypair *privKey) { + struct tlvdb *derRoot = tlvdb_parse_multi(buf, bufLen); + + struct tlvdb *privkeyTlvdb = tlvdb_find_full(derRoot, 0x04); + if (privkeyTlvdb == NULL) { + tlvdb_free(derRoot); + return PM3_EINVARG; + } + const struct tlv *privkeyTlv = tlvdb_get_tlv(privkeyTlvdb); + + if (mbedtls_ecp_read_key(MBEDTLS_ECP_DP_SECP256R1, privKey, privkeyTlv->value, privkeyTlv->len)) { + tlvdb_free(derRoot); + PrintAndLogEx(FAILED, "Unable to parse private key file. Should be DER encoded ASN1"); + return PM3_EINVARG; + } + + struct tlvdb *pubkeyCoordsTlvdb = tlvdb_find_full(derRoot, 0x03); + if (pubkeyCoordsTlvdb == NULL) { + tlvdb_free(derRoot); + PrintAndLogEx(FAILED, "Private key file should include public key component"); + return PM3_EINVARG; + } + const struct tlv *pubkeyCoordsTlv = tlvdb_get_tlv(pubkeyCoordsTlvdb); + if (pubkeyCoordsTlv->len != 66 || pubkeyCoordsTlv->value[0] != 0x00 || pubkeyCoordsTlv->value[1] != 0x04) { + tlvdb_free(derRoot); + PrintAndLogEx(FAILED, "Invalid public key data"); + return PM3_EINVARG; + } + + tlvdb_free(derRoot); + + if (mbedtls_mpi_read_binary(&privKey->Q.X, pubkeyCoordsTlv->value + 2, 32) + || mbedtls_mpi_read_binary(&privKey->Q.Y, pubkeyCoordsTlv->value + 34, 32) + || mbedtls_mpi_lset(&privKey->Q.Z, 1)) { + PrintAndLogEx(FAILED, "Failed to read in public key coordinates"); + return PM3_EINVARG; + } + + if (mbedtls_ecp_check_pubkey(&privKey->grp, &privKey->Q)) { + PrintAndLogEx(FAILED, "VAS protocol requires an elliptic key on the P-256 curve"); + return PM3_EINVARG; + } + + return PM3_SUCCESS; +} + +static int GetPrivateKeyHint(mbedtls_ecp_keypair *privKey, uint8_t *keyHint) { + uint8_t xcoord[32] = {0}; + if (mbedtls_mpi_write_binary(&privKey->Q.X, xcoord, sizeof(xcoord))) { + return PM3_EINVARG; + } + + uint8_t hash[32] = {0}; + sha256hash(xcoord, 32, hash); + + memcpy(keyHint, hash, 4); + return PM3_SUCCESS; +} + +static int LoadMobileEphemeralKey(uint8_t *xcoordBuf, mbedtls_ecp_keypair *pubKey) { + if (mbedtls_mpi_read_binary(&pubKey->Q.X, xcoordBuf, 32)) { + return PM3_EINVARG; + } + return PM3_SUCCESS; +} + +static int DecryptVASCryptogram(uint8_t *cryptogram, size_t cryptogramLen, mbedtls_ecp_keypair *privKey, uint8_t *out, size_t *outLen, uint8_t *timestamp) { + uint8_t keyHint[4] = {0}; + if (GetPrivateKeyHint(privKey, keyHint) != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Unable to generate key hint"); + return PM3_EINVARG; + } + + if (memcmp(keyHint, cryptogram, 4) != 0) { + PrintAndLogEx(FAILED, "Private key does not match cryptogram"); + return PM3_EINVARG; + } + + mbedtls_ecp_keypair mobilePubKey; + mbedtls_ecp_keypair_init(&mobilePubKey); + mobilePubKey.grp = privKey->grp; + + if (LoadMobileEphemeralKey(cryptogram + 4, &mobilePubKey) != PM3_SUCCESS) { + mbedtls_ecp_keypair_free(&mobilePubKey); + PrintAndLogEx(FAILED, "Unable to parse mobile ephemeral key from cryptogram"); + return PM3_EINVARG; + } + + mbedtls_mpi sharedSecret; + mbedtls_mpi_init(&sharedSecret); + + if (mbedtls_ecdh_compute_shared(&privKey->grp, &sharedSecret, &mobilePubKey.Q, &privKey->d, NULL, NULL)) { + mbedtls_mpi_free(&sharedSecret); + mbedtls_ecp_keypair_free(&mobilePubKey); + PrintAndLogEx(FAILED, "Failed to generate ECDH shared secret"); + return PM3_EINVARG; + } + + mbedtls_mpi_free(&sharedSecret); + mbedtls_ecp_keypair_free(&mobilePubKey); + return PM3_SUCCESS; +} + +static int VASReader(uint8_t *pidHash, const char *url, size_t urlLen, uint8_t *cryptogram, size_t *cryptogramLen, bool verbose) { + clearCommandBuffer(); + + uint16_t flags = ISO14A_RAW | ISO14A_CONNECT | ISO14A_NO_SELECT | ISO14A_APPEND_CRC | ISO14A_NO_DISCONNECT; + SendCommandMIX(CMD_HF_ISO14443A_READER, flags, sizeof(ecpData), 0, ecpData, sizeof(ecpData)); + + msleep(160); + + if (SelectCard14443A_4(false, false, NULL) != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "No card in field"); + return PM3_ECARDEXCHANGE; + } + + uint16_t status = 0; + size_t resLen = 0; + uint8_t selectResponse[APDU_RES_LEN] = {0}; + Iso7816Select(CC_CONTACTLESS, true, true, aid, sizeof(aid), selectResponse, APDU_RES_LEN, &resLen, &status); + + if (status != 0x9000) { + PrintAndLogEx(FAILED, "Card doesn't support VAS"); + return PM3_ECARDEXCHANGE; + } + + if (ParseSelectVASResponse(selectResponse, resLen, verbose) != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Card doesn't support VAS"); + return PM3_ECARDEXCHANGE; + } + + uint8_t getVasApdu[PM3_CMD_DATA_SIZE]; + int getVasApduLen = 0; + + int s = CreateGetVASDataCommand(pidHash, url, urlLen, getVasApdu, &getVasApduLen); + if (s != PM3_SUCCESS) { + return s; + } + + uint8_t apduRes[APDU_RES_LEN] = {0}; + int apduResLen = 0; + + s = ExchangeAPDU14a(getVasApdu, getVasApduLen, false, false, apduRes, APDU_RES_LEN, &apduResLen); + if (s != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Failed to send APDU"); + return s; + } + + if (apduResLen == 2 && apduRes[0] == 0x62 && apduRes[1] == 0x87) { + PrintAndLogEx(WARNING, "Device returned error on GET VAS DATA. Either doesn't have pass with matching id, or requires user authentication."); + return PM3_ECARDEXCHANGE; + } + + if (apduResLen == 0 || apduRes[0] != 0x70) { + PrintAndLogEx(FAILED, "Invalid response from peer"); + } + + return ParseGetVASDataResponse(apduRes, apduResLen, cryptogram, cryptogramLen); +} + +static int CmdVASReader(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "nfc vas reader", + "Read and decrypt VAS message", + "nfc vas reader -p pass.com.example.ticket -k ./priv.key -> select pass and decrypt with priv.key\nnfc vas reader --url https://example.com -> URL Only mode"); + void *argtable[] = { + arg_param_begin, + arg_str0("p", NULL, "", "pass type id"), + arg_str0("k", NULL, "", "path to terminal private key"), + arg_str0(NULL, "url", "", "a URL to provide to the mobile device"), + arg_lit0("v", "verbose", "log additional information"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + struct arg_str *passTypeIdArg = arg_get_str(ctx, 1); + int passTypeIdLen = arg_get_str_len(ctx, 1); + uint8_t pidHash[32] = {0}; + sha256hash((uint8_t *) passTypeIdArg->sval[0], passTypeIdLen, pidHash); + + struct arg_str *keyPathArg = arg_get_str(ctx, 2); + int keyPathLen = arg_get_str_len(ctx, 2); + + if (keyPathLen == 0 && passTypeIdLen > 0) { + PrintAndLogEx(FAILED, "Must provide path to terminal private key if a pass type id is provided"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + uint8_t *keyData = NULL; + size_t keyDataLen = 0; + if (loadFile_safe(keyPathArg->sval[0], "", (void **)&keyData, &keyDataLen) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + mbedtls_ecp_keypair privKey; + mbedtls_ecp_keypair_init(&privKey); + + if (LoadReaderPrivateKey(keyData, keyDataLen, &privKey) != PM3_SUCCESS) { + CLIParserFree(ctx); + mbedtls_ecp_keypair_free(&privKey); + return PM3_EINVARG; + } + + struct arg_str *urlArg = arg_get_str(ctx, 3); + int urlLen = arg_get_str_len(ctx, 3); + const char *url = NULL; + if (urlLen > 0) { + url = urlArg->sval[0]; + } + + bool verbose = arg_get_lit(ctx, 4); + + uint8_t cryptogram[120] = {0}; + size_t cryptogramLen = 0; + + int s = VASReader(passTypeIdLen > 0 ? pidHash : NULL, url, urlLen, cryptogram, &cryptogramLen, verbose); + if (s != PM3_SUCCESS) { + CLIParserFree(ctx); + mbedtls_ecp_keypair_free(&privKey); + return s; + } + + if (verbose) { + PrintAndLogEx(INFO, "Got cryptogram response: %s", sprint_hex(cryptogram, cryptogramLen)); + } + + uint8_t message[64] = {0}; + size_t messageLen = 0; + uint8_t timestamp[4] = {0}; + + if (DecryptVASCryptogram(cryptogram, cryptogramLen, &privKey, message, &messageLen, timestamp) != PM3_SUCCESS) { + CLIParserFree(ctx); + mbedtls_ecp_keypair_free(&privKey); + return PM3_EINVARG; + } + + CLIParserFree(ctx); + mbedtls_ecp_keypair_free(&privKey); + return s; +} + +static int CmdVASDecrypt(const char *Cmd) { + return PM3_SUCCESS; +} + +static int CmdVASSim(const char *Cmd) { + return PM3_SUCCESS; +} + +static int CmdHelp(const char *Cmd); + +static command_t CommandTable[] = { + {"--------", CmdHelp, AlwaysAvailable, "----------- " _CYAN_("Value Added Service") " -----------"}, + {"reader", CmdVASReader, IfPm3Iso14443a, "Read and decrypt VAS message"}, + {"decrypt", CmdVASDecrypt, AlwaysAvailable, "Decrypt a VAS cryptogram"}, + {"sim", CmdVASSim, IfPm3Iso14443a, "Simulate a VAS mobile credential"}, + {"--------", CmdHelp, AlwaysAvailable, "----------------- " _CYAN_("General") " -----------------"}, + {"help", CmdHelp, AlwaysAvailable, "This help"}, + {NULL, NULL, NULL, NULL} +}; + +int CmdVAS(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CommandTable, Cmd); +}; + +static int CmdHelp(const char *Cmd) { + (void)Cmd; // Cmd is not used so far + CmdsHelp(CommandTable); + return PM3_SUCCESS; +}; diff --git a/client/src/cmdvas.h b/client/src/cmdvas.h new file mode 100644 index 000000000..59a53f50d --- /dev/null +++ b/client/src/cmdvas.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// An implementation of the Value Added Service protocol +//----------------------------------------------------------------------------- + +#ifndef CMDVAS_H__ +#define CMDVAS_H__ + +#include "common.h" + +int CmdVAS(const char *Cmd); + +#endif