Files
proxmark3/tools/pm3_online_tests.sh
T
iceman1001andClaude Opus 5 (1M context) 3b078f2c57 pm3_online_tests: make desfire_value gate the communication mode
The six plain/mac assertion pairs differed only by -m, and the file they ran
against was created with no --amode and no --rawrights - so plain mode, free
access. Free access is served in plain whatever the file says, and the client
derives the mode from the file settings rather than from -m, so both halves of
every pair sent the same bytes:

  --op credit -m mac    ->  90 0C 00 00 05 02 0A 00 00 00 00
  --op credit -m plain  ->  90 0C 00 00 05 02 0A 00 00 00 00

They passed on a client that could not do MAC mode at all, which is how
e1598cd62 shipped: it had removed CREDIT/DEBIT/LIMITED_CREDIT from
EV1D40TransmitMAC, and this suite stayed green.

Build a fixture that makes the client answer a different question per file
instead, and drop the -m flags, since deriving the mode is the thing under
test:

  data  00  mac      EEEE   free access, MACed file
  data  01  mac      0000   key protected
  data  02  encrypt  EEEE   free access, enciphered file
  data  03  encrypt  0000   key protected
  value 10  mac      00E0   credit plain, debit MAC, same file
  value 11  mac      0000   key protected
  value 12  plain    EEEE   the original case
  value 13  encrypt  0000   plus FreeValue, so GetValue is plain

File 10 is the one worth having: credit is granted by read & write only, which
is free there, while debit is also granted by the write right, which is key 0.
A single communication mode for the whole file cannot satisfy both.

Also dump the application. It has no ISO file ids, so the client's
GetISOFileIDs probe is refused and the PICC ends the session on it; a dump
that does not notice reads the first file's plain content as a response CMAC
and reports it as no data.

Checked against a client built at 08e389c0b, before the three fixes: the free
access data writes fail -20, the MACed value credit fails -20, and the dump
loses file 00. The 00E0 and FreeValue cases pass there too - they guard the
derivation against future regressions rather than reproducing an old bug.

Co-Authored-By: Claude Opus 5 (1M context)
2026-09-13 00:04:38 +02:00

629 lines
31 KiB
Bash
Executable File

#!/usr/bin/env bash
# Online tests that require actual PM3 device connection
# This is used to make sure that the language for the functions is english instead of the system default language.
LANG=C.UTF-8
PM3PATH="$(dirname "$0")/.."
cd "$PM3PATH" || exit 1
TESTALL=false
TESTDESFIREVALUE=false
TESTHIDWIEGAND=false
TESTMFHIDENCODE=false
TESTICLASSREADER=false
TESTSMARTCARD=false
TESTICLASSEMU=false
TESTHITAG2=false
NEED_MF_HID_ENCODE_WIPE=false
TESTMANUAL=false
# https://medium.com/@Drew_Stokes/bash-argument-parsing-54f3b81a6a8f
PARAMS=""
while (( "$#" )); do
case "$1" in
-h|--help)
echo """
Usage: $0 [--pm3bin /path/to/pm3] [--pm3port /dev/tty...] [desfire_value|hid_wiegand|mf_hid_encode|iclass_emu|iclass_reader|hitag2]
--pm3bin ...: Specify path to pm3 binary to test
--pm3port ...: Specify serial port for client/proxmark3
--manual ...: Pause after successful online LF HID clone/read checks for external reader verification
desfire_value: Test DESFire value operations with card
hid_wiegand: Test LF HID T55xx clone and PM3 readback flows
mf_hid_encode: Test MIFARE Classic HID encoding flows
iclass_emu: Test iCLASS emulator memory load/write/read flows
iclass_reader: Load iCLASS HID credentials into emulator memory for external reader verification
smartcard: Test the RDV4 SIM module and an ISO 7816 contact card
hitag2: Test Hitag 2 against a genuine card and a genuine reader
You must specify a test target - no default 'all' for online tests
"""
exit 0
;;
--pm3bin)
if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
PM3BIN=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
--pm3port)
if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
PM3PORT=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
--manual)
TESTMANUAL=true
shift
;;
desfire_value)
TESTALL=false
TESTDESFIREVALUE=true
shift
;;
hid_wiegand)
TESTALL=false
TESTHIDWIEGAND=true
shift
;;
mf_hid_encode)
TESTALL=false
TESTMFHIDENCODE=true
shift
;;
iclass_reader)
TESTALL=false
TESTICLASSREADER=true
shift
;;
smartcard)
TESTALL=false
TESTSMARTCARD=true
shift
;;
iclass_emu)
TESTALL=false
TESTICLASSEMU=true
shift
;;
hitag2)
TESTALL=false
TESTHITAG2=true
shift
;;
-*|--*=) # unsupported flags
echo "Error: Unsupported flag $1" >&2
exit 1
;;
*) # preserve positional arguments
PARAMS="$PARAMS $1"
shift
;;
esac
done
# set positional arguments in their proper place
eval set -- "$PARAMS"
HITAG2_DUMP="${HITAG2_DUMP:-traces/lf-hitag-CE129911-dump.bin}"
C_RED='\033[0;31m'
C_GREEN='\033[0;32m'
C_YELLOW='\033[0;33m'
C_BLUE='\033[0;34m'
C_NC='\033[0m' # No Color
C_OK='\xe2\x9c\x94\xef\xb8\x8f'
C_FAIL='\xe2\x9d\x8c'
# Check if file exists
function CheckFileExist() {
printf "%-40s" "$1 "
if [ -f "$2" ]; then
echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK}"
return 0
fi
if ls "$2" 1> /dev/null 2>&1; then
echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK}"
return 0
fi
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL}"
return 1
}
# Execute command and check result
function CheckExecute() {
printf "%-40s" "$1 "
start=$(date +%s)
TIMEINFO=""
RES=$(eval "$2")
end=$(date +%s)
delta=$(expr $end - $start)
if [ $delta -gt 2 ]; then
TIMEINFO=" ($delta s)"
fi
if echo "$RES" | grep -E -q "$3"; then
echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK} $TIMEINFO"
return 0
fi
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO"
echo "Execution trace:"
echo "$RES"
return 1
}
function CheckLfHidCloneReadback() {
printf "%-40s" "$1 "
start=$(date +%s)
TIMEINFO=""
RES=$($PM3BIN -c "lf hid clone $2; lf hid reader" 2>&1)
end=$(date +%s)
delta=$(expr $end - $start)
if [ $delta -gt 2 ]; then
TIMEINFO=" ($delta s)"
fi
if echo "$RES" | grep -E -q "$3"; then
echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK} $TIMEINFO"
if $TESTMANUAL; then
echo " Manual check: $4"
WaitForEnter "PRESENT THE T55xx TAG TO ANOTHER READER AND CONFIRM: $4"
fi
return 0
fi
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO"
echo "Execution trace:"
echo "$RES"
return 1
}
function HexToBin() {
local hex="${1^^}"
local bin=""
local i ch
for ((i=0; i<${#hex}; i++)); do
ch="${hex:i:1}"
case "$ch" in
0) bin+="0000" ;;
1) bin+="0001" ;;
2) bin+="0010" ;;
3) bin+="0011" ;;
4) bin+="0100" ;;
5) bin+="0101" ;;
6) bin+="0110" ;;
7) bin+="0111" ;;
8) bin+="1000" ;;
9) bin+="1001" ;;
A) bin+="1010" ;;
B) bin+="1011" ;;
C) bin+="1100" ;;
D) bin+="1101" ;;
E) bin+="1110" ;;
F) bin+="1111" ;;
*) return 1 ;;
esac
done
printf "%s" "$bin"
}
function RestoreMfHidEncodeSector0() {
$PM3BIN -c "hf mf wrbl --blk 3 -b -k 89ECA97F8C2A -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true
$PM3BIN -c "hf mf wrbl --blk 3 -k FFFFFFFFFFFF -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true
$PM3BIN -c "hf mf wrbl --blk 3 -k A0A1A2A3A4A5 -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true
$PM3BIN -c "hf mf wrbl --blk 2 -k FFFFFFFFFFFF -d 00000000000000000000000000000000; \
hf mf wrbl --blk 1 -k FFFFFFFFFFFF -d 00000000000000000000000000000000" >/dev/null 2>&1 || return 1
}
function RestoreMfHidEncodeSector1() {
$PM3BIN -c "hf mf wrbl --blk 7 -b -k 204752454154 -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true
$PM3BIN -c "hf mf wrbl --blk 7 -k FFFFFFFFFFFF -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true
$PM3BIN -c "hf mf wrbl --blk 7 -k 484944204953 -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true
$PM3BIN -c "hf mf wrbl --blk 6 -k FFFFFFFFFFFF -d 00000000000000000000000000000000; \
hf mf wrbl --blk 5 -k FFFFFFFFFFFF -d 00000000000000000000000000000000; \
hf mf wrbl --blk 4 -k FFFFFFFFFFFF -d 00000000000000000000000000000000" >/dev/null 2>&1 || return 1
}
function RestoreMfHidEncodeCard() {
RestoreMfHidEncodeSector0 || return 1
RestoreMfHidEncodeSector1 || return 1
local verify
verify=$($PM3BIN -c 'hf mf rdbl --blk 1 -k FFFFFFFFFFFF; hf mf rdbl --blk 2 -k FFFFFFFFFFFF; hf mf rdbl --blk 4 -k FFFFFFFFFFFF; hf mf rdbl --blk 5 -k FFFFFFFFFFFF; hf mf rdbl --blk 6 -k FFFFFFFFFFFF' 2>&1) || return 1
echo "$verify" | grep -E -q " 1 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \
&& echo "$verify" | grep -E -q " 2 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \
&& echo "$verify" | grep -E -q " 4 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \
&& echo "$verify" | grep -E -q " 5 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \
&& echo "$verify" | grep -E -q " 6 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
}
function CleanupMfHidEncodeCard() {
if [ "$NEED_MF_HID_ENCODE_WIPE" != true ]; then
return 0
fi
echo ""
printf "%-40s" "hf mf encodehid cleanup "
if RestoreMfHidEncodeCard; then
echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK}"
else
echo -e "[ ${C_YELLOW}WARN${C_NC} ]"
echo "Cleanup could not restore sectors 0 and 1 to the default usable state."
fi
}
function CheckMfHidEncodeRoundTrip() {
printf "%-40s" "$1 "
start=$(date +%s)
TIMEINFO=""
if ! RestoreMfHidEncodeCard >/dev/null 2>&1; then
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL}"
echo "Execution trace:"
echo "Failed to restore sectors 0 and 1 to the default usable state before running the test."
return 1
fi
RES=$($PM3BIN -c "hf mf encodehid $2; hf mf rdbl --blk 5 -k 484944204953" 2>&1)
end=$(date +%s)
delta=$(expr $end - $start)
if [ $delta -gt 2 ]; then
TIMEINFO=" ($delta s)"
fi
BLOCKHEX=$(printf "%s\n" "$RES" | LC_ALL=C grep -aoE '02( [0-9A-F]{2}){15}' | tail -n1 | tr -d ' ')
if [ -z "$BLOCKHEX" ]; then
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO"
echo "Execution trace:"
echo "$RES"
return 1
fi
if [[ "$BLOCKHEX" != 02* ]]; then
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO"
echo "Expected block 5 to start with the 0x02 HID marker."
echo "Actual block 5 data: $BLOCKHEX"
echo "Execution trace:"
echo "$RES"
return 1
fi
RAWPAYLOAD=${BLOCKHEX#02}
PAYLOADBIN=$(HexToBin "$RAWPAYLOAD") || {
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO"
echo "Execution trace:"
echo "$RES"
return 1
}
while [[ "$PAYLOADBIN" == 0* ]]; do
PAYLOADBIN=${PAYLOADBIN#0}
done
if [[ "$PAYLOADBIN" != 1* ]]; then
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO"
echo "Expected a sentinel-prefixed Wiegand payload in block 5."
echo "Actual payload bits: $PAYLOADBIN"
echo "Execution trace:"
echo "$RES"
return 1
fi
RECOVERED_BIN=${PAYLOADBIN#1}
if [ "$RECOVERED_BIN" != "$3" ]; then
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO"
echo "Expected Wiegand bits: $3"
echo "Actual Wiegand bits: $RECOVERED_BIN"
echo "Execution trace:"
echo "$RES"
return 1
fi
DECODE_RES=$($PM3BIN -c "wiegand decode --bin $RECOVERED_BIN" 2>&1)
if echo "$DECODE_RES" | grep -E -q "$4"; then
echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK} $TIMEINFO"
return 0
fi
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO"
echo "Decode trace:"
echo "$DECODE_RES"
return 1
}
function CheckMfHidEncodeCleanup() {
printf "%-40s" "$1 "
RES=$($PM3BIN -c 'hf mf rdbl --blk 1 -k FFFFFFFFFFFF; hf mf rdbl --blk 2 -k FFFFFFFFFFFF; hf mf rdbl --blk 4 -k FFFFFFFFFFFF; hf mf rdbl --blk 5 -k FFFFFFFFFFFF; hf mf rdbl --blk 6 -k FFFFFFFFFFFF' 2>&1)
if echo "$RES" | grep -E -q " 1 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \
&& echo "$RES" | grep -E -q " 2 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \
&& echo "$RES" | grep -E -q " 4 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \
&& echo "$RES" | grep -E -q " 5 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \
&& echo "$RES" | grep -E -q " 6 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"; then
echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK}"
return 0
fi
echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL}"
echo "Execution trace:"
echo "$RES"
return 1
}
function WaitForEnter() {
echo ""
echo "$1"
# Only prompt when there is really a terminal to prompt on.
#
# `[ -r /dev/tty ]` is not that test: the node can exist and be readable by
# mode bits while the process has no controlling terminal, and the redirect
# then fails with "No such device or address" and the run carries on with the
# rig unconfirmed. Opening it is the only reliable check.
if [ -n "${PM3_ONLINE_NOPROMPT:-}" ]; then
echo "PM3_ONLINE_NOPROMPT set - assuming the rig is ready."
return
fi
if (exec < /dev/tty) 2>/dev/null; then
echo "Press Enter when ready, or Ctrl-C to abort."
stty sane < /dev/tty 2>/dev/null || true
IFS= read -r < /dev/tty
elif [ -t 0 ]; then
echo "Press Enter when ready, or Ctrl-C to abort."
IFS= read -r
else
echo "No terminal to prompt on - assuming the rig is ready."
fi
}
trap CleanupMfHidEncodeCard EXIT
echo -e "${C_BLUE}Iceman Proxmark3 online test tool${C_NC}"
echo ""
echo "work directory: $(pwd)"
if command -v git >/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo -n "git branch: "
git describe --all
echo -n "git sha: "
git rev-parse HEAD
echo ""
fi
# Check that user specified a test
if [ "$TESTDESFIREVALUE" = false ] && [ "$TESTHIDWIEGAND" = false ] && [ "$TESTMFHIDENCODE" = false ] && [ "$TESTICLASSEMU" = false ] && [ "$TESTICLASSREADER" = false ] && [ "$TESTSMARTCARD" = false ] && [ "$TESTHITAG2" = false ]; then
echo "Error: You must specify a test target. Use -h for help."
exit 1
fi
while true; do
# DESFire value tests
if $TESTDESFIREVALUE; then
echo -e "\n${C_BLUE}Testing DESFire card value operations${C_NC} ${PM3BIN:=./pm3}"
echo " PLACE A FACTORY DESFIRE CARD ON THE READER NOW"
if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi
echo " Formatting card to clean state..."
if ! CheckExecute "format card" "$PM3BIN -c 'hf mfdes formatpicc'" "done"; then break; fi
# The PICC serves a file in plain whenever access is granted by the free
# access right (0x0E) instead of by a key we hold, whatever the file
# communication mode says. The client has to work that out per operation,
# so every file below is a different answer to it. No -m flags on purpose:
# the point is that the client derives the mode, not that we ask for one.
echo " Building the fixture..."
if ! CheckExecute "card auth test" "$PM3BIN -c 'hf mfdes auth -n 0 -t 2tdea -k 00000000000000000000000000000000 --kdf none'" "authenticated.*succes"; then break; fi
if ! CheckExecute "card app creation" "$PM3BIN -c 'hf mfdes createapp --aid 123456 --ks1 0F --dstalgo aes --numkeys 2'" "successfully created"; then break; fi
# data files: free vs key protected, in each communication mode
for F in "00 mac EEEE" "01 mac 0000" "02 encrypt EEEE" "03 encrypt 0000"; do
set -- $F
if ! CheckExecute "data file $1 $2 $3 created" "$PM3BIN -c 'hf mfdes createfile --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid $1 --amode $2 --size 000008 --rawrights $3'" "created successfully"; then break 2; fi
done
# value files. 10 is the discriminating one: credit is granted by
# read&write only, which is free here, while debit is also granted by the
# write right, which is key 0 - so credit goes plain and debit goes MAC on
# the same file in the same session. 13 has the FreeValue option, which
# makes GetValue plain on an otherwise fully enciphered file.
if ! CheckExecute "value file 10 mac 00E0 created" "$PM3BIN -c 'hf mfdes createvaluefile --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid 10 --amode mac --rawrights 00E0 --lower 00000000 --upper 000003E8 --value 00000064'" "created successfully"; then break; fi
if ! CheckExecute "value file 11 mac 0000 created" "$PM3BIN -c 'hf mfdes createvaluefile --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid 11 --amode mac --rawrights 0000 --lower 00000000 --upper 000003E8 --value 00000064'" "created successfully"; then break; fi
if ! CheckExecute "value file 12 plain EEEE created" "$PM3BIN -c 'hf mfdes createvaluefile --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid 12 --amode plain --rawrights EEEE --lower 00000000 --upper 000003E8 --value 00000064'" "created successfully"; then break; fi
if ! CheckExecute "value file 13 enc FreeValue created" "$PM3BIN -c 'hf mfdes createvaluefile --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid 13 --amode encrypt --rawrights 0000 --lcredit 3 --lower 00000000 --upper 000003E8 --value 00000064'" "created successfully"; then break; fi
echo " Running data file tests..."
for F in 00 01 02 03; do
if ! CheckExecute "data file $F write" "$PM3BIN -c 'hf mfdes write --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid $F -d A0A1A2A3A4A5A6$F'" "Write data file $F success"; then break 2; fi
if ! CheckExecute "data file $F read back" "$PM3BIN -c 'hf mfdes read --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid $F'" "A0 A1 A2 A3 A4 A5 A6 $F"; then break 2; fi
done
# the app has no ISO file ids, so the client's GetISOFileIDs probe is
# refused and the PICC ends the session on it. dump has to notice, or it
# reads the first file's plain content as a response CMAC and loses it.
if ! CheckExecute "dump keeps the session" "$PM3BIN -c 'hf mfdes dump --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000'" "A0 A1 A2 A3 A4 A5 A6 00"; then break; fi
echo " Running value operation tests..."
for F in 10 11 12; do
if ! CheckExecute "value file $F get" "$PM3BIN -c 'hf mfdes value --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid $F --op get'" "Value: 100 \\(0x00000064\\)"; then break 2; fi
if ! CheckExecute "value file $F credit" "$PM3BIN -c 'hf mfdes value --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid $F --op credit -d 00000032'" "Value changed"; then break 2; fi
if ! CheckExecute "value file $F debit" "$PM3BIN -c 'hf mfdes value --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid $F --op debit -d 00000014'" "Value changed"; then break 2; fi
if ! CheckExecute "value file $F final" "$PM3BIN -c 'hf mfdes value --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid $F --op get'" "Value: 130 \\(0x00000082\\)"; then break 2; fi
done
if ! CheckExecute "value file 13 FreeValue get" "$PM3BIN -c 'hf mfdes value --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid 13 --op get'" "Value: 100 \\(0x00000064\\)"; then break; fi
if ! CheckExecute "value file 13 credit" "$PM3BIN -c 'hf mfdes value --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid 13 --op credit -d 00000032'" "Value changed"; then break; fi
if ! CheckExecute "value file 13 final" "$PM3BIN -c 'hf mfdes value --aid 123456 -n 0 -t aes -k 00000000000000000000000000000000 --fid 13 --op get'" "Value: 150 \\(0x00000096\\)"; then break; fi
if ! CheckExecute "card cleanup" "$PM3BIN -c 'hf mfdes selectapp --aid 000000; hf mfdes auth -n 0 -t 2tdea -k 00000000000000000000000000000000 --kdf none; hf mfdes deleteapp --aid 123456'" "application.*deleted"; then break; fi
echo " card value operation tests completed successfully!"
fi
if $TESTHIDWIEGAND; then
echo -e "\n${C_BLUE}Testing LF HID T55xx clone flows${C_NC} ${PM3BIN:=./pm3}"
if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi
if ! CheckExecute "lf hid clone raw oversize" "$PM3BIN -c 'lf hid clone -r 01400076000c86' 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi
if ! CheckExecute "lf hid clone bin oversize" "PAT=\$(printf '01%.0s' {1..48}); $PM3BIN -c \"lf hid clone --bin \$PAT\" 2>&1" "Packed HID encoding supports up to 84 Wiegand bits"; then break; fi
if ! CheckExecute "lf hid clone new oversize" "$PM3BIN -c 'lf hid clone --new 0000A4550148AB' 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi
WaitForEnter "PLACE A REWRITABLE T55xx TAG ON THE PM3 NOW"
if ! CheckLfHidCloneReadback "lf hid clone H10301 26-bit" "-w H10301 --fc 118 --cn 1603" "H10301.*FC: 118.*CN: 1603" "H10301 26-bit, FC 118, CN 1603"; then break; fi
if ! CheckLfHidCloneReadback "lf hid clone C1k35s 35-bit" "-w C1k35s --fc 118 --cn 1603" "C1k35s.*FC: 118.*CN: 1603" "C1k35s 35-bit, FC 118, CN 1603"; then break; fi
if ! CheckLfHidCloneReadback "lf hid clone H10304 37-bit" "-w H10304 --fc 118 --cn 1603" "H10304.*FC: 118.*CN: 1603" "H10304 37-bit, FC 118, CN 1603"; then break; fi
fi
if $TESTMFHIDENCODE; then
echo -e "\n${C_BLUE}Testing MIFARE Classic HID encoding${C_NC} ${PM3BIN:=./pm3}"
if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi
WaitForEnter "PLACE A BLANK MIFARE CLASSIC 1K CARD ON THE PM3 NOW"
NEED_MF_HID_ENCODE_WIPE=true
if ! CheckMfHidEncodeRoundTrip "hf mf encodehid bin roundtrip" "--bin 10001111100000001010100011" "10001111100000001010100011" "H10301.*FC: 31.*CN: 337"; then break; fi
if ! CheckMfHidEncodeRoundTrip "hf mf encodehid raw roundtrip" "--raw 063E02A3" "10001111100000001010100011" "H10301.*FC: 31.*CN: 337"; then break; fi
if ! CheckMfHidEncodeRoundTrip "hf mf encodehid new roundtrip" "--new 068F80A8C0" "10001111100000001010100011" "H10301.*FC: 31.*CN: 337"; then break; fi
if ! CheckMfHidEncodeRoundTrip "hf mf encodehid format roundtrip" "-w H10301 --fc 31 --cn 337" "10001111100000001010100011" "H10301.*FC: 31.*CN: 337"; then break; fi
if ! RestoreMfHidEncodeCard; then break; fi
if ! CheckMfHidEncodeCleanup "hf mf encodehid cleanup verify"; then break; fi
fi
if $TESTICLASSEMU; then
echo -e "\n${C_BLUE}Testing iCLASS emulator memory${C_NC} ${PM3BIN:=./pm3}"
if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi
PM3CMD="$PM3BIN"
if [ -n "${PM3PORT:-}" ]; then
PM3CMD="$PM3CMD -p $PM3PORT"
fi
if ! CheckExecute "hf iclass esetblk preserves emu" "$PM3CMD -c 'hf iclass eload -f traces/iclass/hf-iclass-dump.json; hw fpgaoff; hf iclass esetblk --blk 7 -d A55AC33C9669F00F; hf iclass eview -s 64' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "0/0x00.*6D C2 5B 15 FE FF 12 E0.*7/0x07.*A5 5A C3 3C 96 69 F0 0F"; then break; fi
if ! CheckExecute "hf iclass sim preserves emu" "$PM3CMD -c 'hf iclass eload -f traces/iclass/hf-iclass-dump.json; hf iclass sim -t 3; hf iclass eview -s 64' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "6D C2 5B 15 FE FF 12 E0.*03 03 03 03 00 03 E0 17"; then break; fi
if ! CheckExecute "hf iclass sim break preserves emu" "$PM3CMD -c 'hf iclass eload -f traces/iclass/hf-iclass-dump.json; hf iclass sim -t 3; hw break; hf iclass eview -s 64' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "6D C2 5B 15 FE FF 12 E0.*03 03 03 03 00 03 E0 17"; then break; fi
if ! CheckExecute "hf iclass tagsim bin exits cleanly" "printf '\n' | $PM3CMD -c 'hf iclass tagsim --bin 10001111100000001010100011 --enc none; hf iclass eview -s 80' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "Uploaded .* bytes to emulator memory.*0/0x00.*6/0x06.*03 03 03 03 00 03 E0 14.*7/0x07.*00 00 00 00 06 3E 02 A3"; then break; fi
if ! CheckExecute "hf iclass tagsim bin back-to-back updates" "timeout 25 $PM3CMD -c 'hf iclass tagsim --bin 10001111100000001010100011 --enc none; hf iclass tagsim --bin 01010101010101010101010101010101 --enc none; hf iclass eview -s 80' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "7/0x07.*00 00 00 01 55 55 55 55"; then break; fi
if ! CheckExecute "hf iclass tagsim live update exits cleanly" "printf '\033[C' | $PM3CMD -c 'hf iclass tagsim -w H10301 --fc 31 --cn 337 --enc none; hf iclass eview -s 80' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "CN: 338.*0/0x00.*BD 0F 60 10 F7 FF 12 E0"; then break; fi
if ! CheckExecute "hf 15 uid sim resets emu state" "timeout 15 $PM3CMD -c 'hf 15 sim -u E011223344556677 -t 100; hf 15 eview' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "UID.*E0 11 22 33 44 55 66 77.*4 bytes / blocks x 64 blocks"; then break; fi
fi
# Hitag 2, against a genuine card and a genuine reader.
#
# The card is a password mode Hitag 2 (configuration byte 0x06). Its UID and
# password are read from the card itself rather than hardcoded, so the tests
# work with whatever fob is on the bench; only the ones that need a known
# password use HITAG2_KEY, which defaults to the Paxton fob this was developed
# against and can be overridden from the environment.
if $TESTHITAG2; then
echo -e "\n${C_BLUE}Testing Hitag 2 with a genuine card and reader${C_NC} ${PM3BIN:=./pm3}"
if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi
PM3CMD="$PM3BIN"
if [ -n "${PM3PORT:-}" ]; then
echo "Using PM3 port: $PM3PORT"
PM3CMD="$PM3CMD -p $PM3PORT"
fi
HITAG2_KEY="${HITAG2_KEY:-BDF5E846}"
WaitForEnter "PLACE THE GENUINE HITAG 2 CARD ON THE PM3 LF ANTENNA"
# Reading the card: identification, then a full password mode dump. The
# config byte has to come back as a Hitag 2 in password mode, and every one
# of the eight pages has to be present - a partial dump is the failure this
# is here to catch, since one lost frame used to end the read early.
if ! CheckExecute "lf hitag info genuine card" "$PM3CMD -c 'lf hitag info' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "UID.*Password mode.*Hitag 2"; then break; fi
if ! CheckExecute "lf hitag read genuine card" "$PM3CMD -c 'lf hitag read -2 --pwd -k $HITAG2_KEY' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "0/0x00.*1/0x01.*2/0x02.*3/0x03.*4/0x04.*5/0x05.*6/0x06.*7/0x07"; then break; fi
# Writing: put a known value in page 4 and read it back. Page 4 is the
# first user page, so this does not touch the UID, the key or the config.
if ! CheckExecute "lf hitag wrbl genuine card" "$PM3CMD -c 'lf hitag wrbl -2 -k $HITAG2_KEY -p 4 -d 11223344' 2>&1" "Write \( ok \)"; then break; fi
if ! CheckExecute "lf hitag write readback" "$PM3CMD -c 'lf hitag read -2 --pwd -k $HITAG2_KEY' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "4/0x04 \| 11 22 33 44"; then break; fi
# Sniffing a genuine exchange. Both directions have to appear: the reader's
# START_AUTH and password, and the tag's answers. A capture with reader
# frames but no tag frames is the defect this catches - the tag side decoder
# used to fabricate rows with no card present and miss real ones.
WaitForEnter "PUT THE PM3 BETWEEN THE GENUINE CARD AND THE GENUINE READER, THEN PRESS ENTER. PRESS ENTER AGAIN IN THE CLIENT TO STOP THE SNIFF"
if ! CheckExecute "lf hitag sniff genuine pair" "$PM3CMD -c 'lf hitag sniff; lf hitag list' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "Rdr.*START AUTH.*Tag.*32:"; then break; fi
# Simulating to a genuine reader. Only the operator can confirm the reader
# actually accepted it, so this asks - the client cannot see the beep.
WaitForEnter "REMOVE THE CARD, PRESENT THE PM3 TO THE GENUINE READER. THE SIM RUNS FOR 20s - LISTEN FOR THE READER TO BEEP, THEN PRESS ENTER"
if ! CheckExecute "lf hitag sim to reader" "timeout 30 $PM3CMD -c 'lf hitag eload -f $HITAG2_DUMP -2; lf hitag sim -2' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "Starting Hitag 2 simulation"; then break; fi
WaitForEnter "DID THE READER BEEP / ACCEPT THE SIMULATED CARD? PRESS ENTER IF YES, CTRL-C IF NO"
fi
if $TESTICLASSREADER; then
echo -e "\n${C_BLUE}Testing iCLASS reader verification${C_NC} ${PM3BIN:=./pm3}"
if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi
PM3CMD="$PM3BIN"
if [ -n "${PM3PORT:-}" ]; then
echo "Using PM3 port: $PM3PORT"
PM3CMD="$PM3CMD -p $PM3PORT"
fi
WaitForEnter "PRESS ENTER TO START ICLASS PLAIN SIM, PRESENT THE PM3 TO ANOTHER READER, CONFIRM: iCLASS H10301 FC 31 CN 337, THEN PRESS THE PM3 BUTTON TO STOP SIM"
if ! CheckExecute "hf iclass emu reader plain" "$PM3CMD -c 'hf iclass tagsim -w H10301 --fc 31 --cn 337 --enc none' 2>&1" "Uploaded .* bytes to emulator memory"; then break; fi
WaitForEnter "PRESS ENTER TO START ICLASS DES SIM, PRESENT THE PM3 TO ANOTHER READER, CONFIRM: iCLASS H10301 FC 31 CN 337, THEN PRESS THE PM3 BUTTON TO STOP SIM"
if ! CheckExecute "hf iclass emu reader des" "$PM3CMD -c 'hf iclass tagsim -w H10301 --fc 31 --cn 337 --enc des' 2>&1" "Uploaded .* bytes to emulator memory"; then break; fi
WaitForEnter "PRESS ENTER TO START ICLASS 2K3DES SIM, PRESENT THE PM3 TO ANOTHER READER, CONFIRM: iCLASS H10301 FC 31 CN 337, THEN PRESS THE PM3 BUTTON TO STOP SIM"
if ! CheckExecute "hf iclass emu reader 2k3des" "$PM3CMD -c 'hf iclass tagsim -w H10301 --fc 31 --cn 337 --enc 2k3des' 2>&1" "Uploaded .* bytes to emulator memory"; then break; fi
if ! CheckExecute "hf iclass sim preserves emu" "$PM3CMD -c 'hf iclass eview -s 80' 2>&1 | LC_ALL=C tr -cd '\11\12\15\40-\176' | tr '\n' ' '" "0/0x00.*BD 0C 60 10 F7 FF 12 E0.*6/0x06.*03 03 03 03 00 03 E0 17.*7/0x07.*10 A1 45 91 9E D1 6F 50"; then break; fi
fi
# SIM module / ISO 7816 contact card tests.
#
# Card agnostic: asserts that answers come back and are stable, never what
# they contain. Several checks repeat an exchange and require every answer
# to match - a bus driven too fast returns the occasional corrupted frame
# rather than failing outright, which a single shot would not catch.
if $TESTSMARTCARD; then
echo -e "\n${C_BLUE}Testing SIM module and contact smartcard${C_NC} ${PM3BIN:=./pm3}"
if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi
PM3CMD="$PM3BIN"
if [ -n "${PM3PORT:-}" ]; then
echo "Using PM3 port: $PM3PORT"
PM3CMD="$PM3CMD -p $PM3PORT"
fi
WaitForEnter "INSERT AN ISO 7816 CONTACT CARD INTO THE RDV4 SIM SLOT"
# --- the module itself answers over I2C ---
if ! CheckExecute "sim module present" "$PM3CMD -c 'hw status' 2>&1" "Smart card module"; then break; fi
if ! CheckExecute "sim module version ok" "$PM3CMD -c 'hw status' 2>&1" "version\.+ v[0-9]+\.[0-9]+ \( .*ok"; then break; fi
# --- the card answers, and answers the same way every time ---
if ! CheckExecute "smart info returns an ATR" "$PM3CMD -c 'smart info' 2>&1" "ISO7816-3 ATR\.+ 3[BF]"; then break; fi
if ! CheckExecute "ATR stable over 5 reads" "for i in 1 2 3 4 5; do $PM3CMD -c 'smart info' 2>&1 | grep -oE 'ISO7816-3 ATR\.+ [0-9A-F ]+'; done | sort -u | wc -l" "^ *1$"; then break; fi
# any status word will do - the point is that the exchange completed
if ! CheckExecute "T=0 apdu gets a status word" "$PM3CMD -c 'smart raw --t0 -a -s -d 00a4040007a0000000041010' 2>&1" "\[[+-]\] [0-9A-Fa-f]{4} \|"; then break; fi
if ! CheckExecute "T=0 answer stable over 3" "for i in 1 2 3; do $PM3CMD -c 'smart raw --t0 -a -s -d 00a4040007a0000000041010' 2>&1 | grep -oE '^\[[+-]\] [0-9A-Fa-f]{4}'; done | sort -u | wc -l" "^ *1$"; then break; fi
# loose guard against the 1200 ms per-read wait coming back
echo -n " timing three exchanges... "
SMART_T0=$(date +%s)
$PM3CMD -c 'smart info; smart info; smart info' >/dev/null 2>&1
SMART_DT=$(( $(date +%s) - SMART_T0 ))
echo "${SMART_DT}s"
if ! CheckExecute "three exchanges under 5s" "echo $SMART_DT" "^[0-5]$"; then break; fi
# --- T=1, only if this card offers it ---
if $PM3CMD -c 'smart info' 2>&1 | grep -q "Protocol T1"; then
echo " card offers T=1"
if ! CheckExecute "module reports T=1 support" "$PM3CMD -c 'hw status' 2>&1" "T=1, PPS\.+ \( .*supported"; then break; fi
if ! CheckExecute "T=1 apdu gets a status word" "$PM3CMD -c 'smart raw --t1 -d 00a4040007a000000004101000' 2>&1" "\[[+-]\] [0-9A-Fa-f]{4} \|"; then break; fi
if ! CheckExecute "T=1 answer stable over 3" "for i in 1 2 3; do $PM3CMD -c 'smart raw --t1 -d 00a4040007a000000004101000' 2>&1 | grep -oE '^\[[+-]\] [0-9A-Fa-f]{4}'; done | sort -u | wc -l" "^ *1$"; then break; fi
else
echo " card is T=0 only, skipping the T=1 checks"
fi
fi
echo -e "\n------------------------------------------------------------"
echo -e "Tests [ ${C_GREEN}OK${C_NC} ] ${C_OK}\n"
exit 0
done
echo -e "\n------------------------------------------------------------"
echo -e "\nTests [ ${C_RED}FAIL${C_NC} ] ${C_FAIL}\n"
exit 1