Lesson 7. The card program: challenge-response

In pictures

There are no new parts in this lesson — the card's whole program, 207 commands. The main thing in it is challenge-response: the card proves that it is genuine without showing its secret.


7.1. The idea: a secret that never leaves

The number and the PIN can be spied on. So the terminal sends a random number — a challenge. The card mixes it with its key and answers. The bank knows the key and computes the same thing: if it matches, the card is genuine.

  • the key never leaves the card;
  • an overheard answer is useless: next time the challenge will be different.
Hchallenge byte 1Lchallenge byte 2Kkey byteADDXORHLrotate the pair left by 1 bitthe top bit of H goes into the bottom of L× 4 rounds, each with its own key byte K0…K3
One round of the card cipher: L adds a key byte, H mixes with L through XOR, and the pair H L rotates as one 16-bit number. Four rounds — and the challenge A5 3C turns into 68 F7. The cipher is a toy, the principle is real: without the key the answer cannot be computed.
Our cipher is a teaching one. The key is 32 bits, the challenge and the answer are 2 bytes each: there are only 65,536 answers, and the key can be found by brute force in seconds. Real cards have a key of 128 bits or more, the cipher is 3DES or AES, the challenge is 4 bytes, the answer 8 bytes; the long computations are done by a separate crypto coprocessor in the chip. An 8-bit processor does not limit the key length: the key is processed a byte at a time, in a loop — the way our program walks through the 4 key bytes. Our principle is the real one: the key never leaves the card.

7.2. The idea: a long answer in two commands

The answer is 2 bytes, but the tester lamps show one. So A answers with the first byte and R with the second. Real cards do the same: the terminal takes a long answer with a separate command, GET RESPONSE.

7.3. The program

The same project as in lesson 6. No new parts — only the program: the card's whole program, 207 commands.

Step 1. The program in the ROM

⚙ on ROM → button ⬆ → file 07-card.rom.json. Reset RST — the card sends the greeting.

What it consists of:

PartLabelsWhat it does
power-onstartOK = 0 (PIN not entered), let go of the line, greeting $3B
parsing the commandloopreceive a byte, compare (CMP) with N, P, A, R — jump to its part
numbercmdNanswer with byte $10 — the first row of the matrix
PINcmdP, wrong, blockedreceive 2 bytes, compare with $11–$12; try counter TRY: 3 — blocked
challenge-responsecmdA, denyonly if OK: 4 rounds of the cipher with key $13–$16, answer H
rest of the answercmdRanswer L (like GET RESPONSE)
receiving a byterx, rxbit, rxgwait for the start bit, 8 bits of 16 clocks, wait out the stop
sending a bytetx, txbit, dlystart, 8 bits of 16 clocks, stop, a guard pause

The cipher is 4 rounds over the two challenge bytes H, L: L = L + K (a key byte), H = H XOR L, then the pair H L is rotated left by 1 bit, as one 16-bit number. Challenge A5 3C, the key of card 1 5A C3 17 8E:

RoundKey KL + KH XOR Lrotate: Hrotate: L
15A9633672C
2C3EF8811DF
317F6E7CFED
48E7BB468F7

The answer is 68 F7. A slightly different challenge — a completely different answer; a different key — too.

; The payment card program. One for all cards; the personal part is in the permanent memory $10–$17.; Every command is exactly one clock, so the bit length on the IO line is counted in commands: P = 16 clocks.;; Talking to the terminal:;   reset            → $3B (a greeting, like the first ATR byte of real cards);   'N'              → account number;   'P' PIN1 PIN2    → $90 correct · $62/$61 wrong (tries left) · $60 card blocked;   'A' C1 C2        → R1 = first byte of cipher(challenge, key) — only after a correct PIN; otherwise $69;   'R'              → R2 = second byte of the answer (like GET RESPONSE of real cards: an answer longer than a byte —;                      the terminal takes the rest with a separate command; each byte is visible on the adapter lamps);   anything else    → $6D (no such command) ; --- RAM (8 bytes) ---.equ B    $00      ; byte: received / sent.equ N    $01      ; bit counter.equ D    $02      ; delay counter.equ H    $03      ; first argument byte / high byte of the cipher.equ L    $04      ; second argument byte / low byte of the cipher.equ OK   $05      ; 1 = PIN is correct in this session; --- permanent memory (on transistors) ---.equ ACC  $10      ; account number.equ PINH $11      ; PIN, digits 1–2.equ PINL $12      ; PIN, digits 3–4.equ K0   $13      ; key, 4 bytes.equ K1   $14.equ K2   $15.equ K3   $16; --- try register and port ---.equ TRY  $18      ; wrong tries in a row (3 = blocked).equ IN   $1C      ; bit 0 = the IO line.equ OUT  $1D      ; bit 0 = 1 → pull the line to 0 start:  LD #0        ST OK            ; after a reset the PIN is not entered again        ST OUT           ; let go of the line        LD #$3B        ST B        CALL tx          ; greeting loop:   CALL rx          ; command        LD B        CMP #'N'        JZ cmdN        CMP #'P'        JZ cmdP        CMP #'A'        JZ cmdA        CMP #'R'        JZ cmdR        LD #$6D        JMP reply cmdN:   LD ACC        JMP reply cmdR:   LD L             ; the second byte of the last answer        JMP reply ; --- PIN check ---cmdP:   CALL rx        LD B        ST H        CALL rx        LD B        ST L        LD #0        ST OK            ; a new try — the earlier correct PIN does not count        LD TRY        CMP #3        JZ blocked        LD H        CMP PINH        JNZ wrong        LD L        CMP PINL        JNZ wrong        LD #0        ST TRY           ; correct: the try counter goes to zero        LD #1        ST OK        LD #$90        JMP replywrong:  LD TRY        ADD #1        ST TRY        LD #3        SUB TRY          ; how many tries are left        OR #$60        JMP replyblocked: LD #$60        JMP reply ; --- challenge-response ---cmdA:   CALL rx        LD B        ST H        CALL rx        LD B        ST L        LD OK        JZ deny        ; 4 rounds: L += K, H ^= L, rotate (H, L) left by 1 bit        LD L        ADD K0        ST L        LD H        XOR L        ST H        LD L        ADD L        ST L        LD H        ADC H        ST H        LD L        ADC #0        ST L        LD L        ADD K1        ST L        LD H        XOR L        ST H        LD L        ADD L        ST L        LD H        ADC H        ST H        LD L        ADC #0        ST L        LD L        ADD K2        ST L        LD H        XOR L        ST H        LD L        ADD L        ST L        LD H        ADC H        ST H        LD L        ADC #0        ST L        LD L        ADD K3        ST L        LD H        XOR L        ST H        LD L        ADD L        ST L        LD H        ADC H        ST H        LD L        ADC #0        ST L        LD H             ; the first byte; the second one by command 'R'reply:  ST B        CALL tx        JMP loopdeny:   LD #$69        JMP reply ; --- receiving a byte: wait for the start bit, then 8 bits top first, one every 16 clocks ---rx:     LD IN        AND #1        JNZ rx           ; line is 1 — wait        LD #8        ST N        .rept 10         ; get to the middle of the first bit        NOP        .endrrxbit:  NOP        NOP        NOP        NOP        NOP        LD B        ADD B            ; shift B left        ST B        LD IN            ; ← here we read the bit        AND #1        ADD B        ST B        LD N        SUB #1        ST N        JNZ rxbit        ; the loop is exactly 16 clocks        LD #6        ST Drxg:    LD D             ; guard: wait out the terminal's stop bit (2 + 6 × 4 = 26 clocks)        SUB #1        ST D        JNZ rxg        RET ; --- sending byte B: start 0, 8 bits top first, stop 1 ---tx:     LD #1        ST OUT           ; start bit        .rept 7        NOP        .endr        LD #8        ST Ntxbit:  LD B        ADD B            ; top bit → C        ST B        LD #0        ADC #0           ; A = C        XOR #1           ; bit 1 → do not pull, 0 → pull        ST OUT           ; ← here the bit goes out on the line        NOP        NOP        NOP        NOP        NOP        LD N        SUB #1        ST N        JNZ txbit        ; the loop is exactly 16 clocks        NOP        NOP        NOP        NOP        NOP        LD #0        ST OUT           ; stop bit        LD #8        ST Ddly:    LD D             ; guard interval: 2 + 8 × 4 = 34 clocks        SUB #1        ST D        JNZ dly        RET

7.4. Checking

Go down the rows from top to bottom: one row is one action. After SEND wait until the "exchange" lamp goes off (lamp).

№What to doLamps (● lit)What it is
1Clock — MAX
2RST on and off○○●●●○●● 59greeting
3switches T7…T0 = 0100 0001, press SEND○○○○○○○○ go offthe byte went, the card waits for the next one
4switches T7…T0 = 1010 0101, press SEND○○○○○○○○ go offthe byte went, the card waits for the next one
5switches T7…T0 = 0011 1100, press SEND○●●○●○○● 105A A5 3C without a PIN — refused ($69)
6switches T7…T0 = 0101 0000, press SEND○○○○○○○○ go offthe byte went, the card waits for the next one
7switches T7…T0 = 0001 0010, press SEND○○○○○○○○ go offthe byte went, the card waits for the next one
8switches T7…T0 = 0011 0100, press SEND●○○●○○○○ 144P 12 34 — PIN correct
9switches T7…T0 = 0100 0001, press SEND○○○○○○○○ go offthe byte went, the card waits for the next one
10switches T7…T0 = 1010 0101, press SEND○○○○○○○○ go offthe byte went, the card waits for the next one
11switches T7…T0 = 0011 1100, press SEND○●●○●○○○ 104A A5 3C — first byte of the answer ($68)
12switches T7…T0 = 0101 0010, press SEND●●●●○●●● 247R — second byte of the answer ($F7)
  • A and R are two commands because the adapter lamps show one byte. Real cards do the same: the terminal takes a long answer with a separate command (GET RESPONSE).
  • After a reset (RST) "PIN entered" is forgotten: A answers $69 again.
  • The program is long (207 commands), the answer to A comes a little later — the card computes 4 rounds.

Stuck? Something doesn't work →