How to Decode EMV Card Data — TLV Parsing & BER-TLV Walkthrough
EMV card responses — from SELECT to GPO to READ RECORD — all use BER-TLV encoding (Basic Encoding Rules — Tag, Length, Value). Every field (PAN, expiry, AFL, AIP, CVM list) is a TLV triple. This guide covers tag encoding (1-byte, 2-byte, constructed), length encoding (short vs long definite), and parsing real EMV data objects including AFL record navigation and CVM list decoding.
1. BER-TLV Tag Encoding
Every EMV tag is 1 or 2 bytes. The first byte encodes tag class and whether it's constructed:
Tag byte 1: [CC] [0/1] [TTTTT] Bits 7-6 (CC): 00=Universal, 01=Application, 10=Context-Specific, 11=Private Bit 5: 0=Primitive (value is data), 1=Constructed (value contains more TLVs) Bits 4-0: Tag number (0-30 = single byte, 31 = second byte follows) # Examples: 0x5A → Application (01), Primitive (0), tag 26 (PAN) 0x77 → Application (01), Constructed (1), tag 23 (Response Message Template) 0x9F36 → Context (10), Primitive (0), tag 0x1F→second byte 0x36 (ATC)
2. Length Encoding
# Short form: Byte 7=0, length = lower 7 bits (0-127)
0x0A → length = 10 bytes
0x7F → length = 127 bytes
# Long form: Byte 7=1, lower 7 bits = number of subsequent length bytes
0x81 0xA0 → 1 byte follows, length = 0xA0 (160)
0x82 0x01 0x00 → 2 bytes follow, length = 0x0100 (256)
def parse_length(data, offset):
b = data[offset]
if b & 0x80 == 0:
return b, offset + 1 # Short form
n_bytes = b & 0x7F
length = int.from_bytes(data[offset+1:offset+1+n_bytes], 'big')
return length, offset + 1 + n_bytes
3. TLV Parser
def parse_tlv(data, offset=0):
"""Parse BER-TLV encoded data into list of (tag, length, value, next_offset)."""
result = []
while offset < len(data):
if data[offset] == 0x00 or data[offset] == 0xFF:
break # Padding
# Parse tag
tag = data[offset]
offset += 1
if tag & 0x1F == 0x1F: # 2-byte tag
tag = (tag << 8) | data[offset]
offset += 1
# Parse length
length, offset = parse_length(data, offset)
# Extract value
value = data[offset:offset+length]
offset += length
is_constructed = (tag >> 8 if tag > 0xFF else tag) & 0x20
result.append({
'tag': tag,
'length': length,
'value': value,
'constructed': bool(is_constructed)
})
return result
4. Key EMV Tags and How to Decode Them
AFL — Application File Locator (Tag 0x94)
The AFL tells the terminal which records to read. Each entry is 4 bytes: SFI, first record, last record, and offline data authentication records.
# AFL = 08 01 01 00 10 03 04 01
# Entry 1: SFI=0x08, First=1, Last=1, ODA=0 → Read SFI 1, record 1
# Entry 2: SFI=0x10, First=3, Last=4, ODA=1 → Read SFI 2, records 3-4
def parse_afl(afl_bytes):
entries = []
for i in range(0, len(afl_bytes), 4):
sfi = afl_bytes[i] >> 3 # SFI is upper 5 bits
first = afl_bytes[i+1]
last = afl_bytes[i+2]
oda = afl_bytes[i+3]
entries.append({'sfi': sfi, 'first': first, 'last': last, 'oda_records': oda})
return entries
AIP — Application Interchange Profile (Tag 0x82)
# AIP = 2 bytes, each bit enables a kernel feature
def parse_aip(aip_bytes):
b1, b2 = aip_bytes[0], aip_bytes[1]
return {
'SDA_supported': bool(b1 & 0x40),
'DDA_supported': bool(b1 & 0x20),
'CVM_required': bool(b1 & 0x10),
'Terminal_risk_management': bool(b1 & 0x08),
'Issuer_auth_via_online': bool(b1 & 0x04),
'CDA_supported': bool(b2 & 0x01),
}
CVM List — Cardholder Verification Method (Tag 0x8E)
# CVM List: each entry = 2 bytes
# Byte 1: [CVM Code (6 bits)] [CVM Condition (2 bits)]
# Byte 2: [CVM Result (when condition fails)]
CVM_CODES = {
0x00: 'Fail CVM processing',
0x02: 'Enciphered PIN verified online',
0x03: 'Plaintext PIN verified by ICC',
0x1E: 'Signature (paper)',
0x1F: 'No CVM required',
0x41: 'Enciphered PIN verification by ICC', # Offline PIN
}
def parse_cvm_list(cvm_bytes):
# Skip first 2 bytes (amount + X/Y), then parse entries
entries = []
for i in range(4, len(cvm_bytes), 2): # Skip 4 bytes header
code = cvm_bytes[i] >> 2
condition = cvm_bytes[i] & 0x03
result = cvm_bytes[i+1]
entries.append({
'method': CVM_CODES.get(code, f'Unknown(0x{code:02X})'),
'condition': ['Always', 'If unattended cash', 'If not unattended cash', 'If terminal supports'][condition],
'fallback': CVM_CODES.get(result, f'Unknown(0x{result:02X})')
})
return entries
5. Reading EMV Records from a Card
# Standard EMV transaction flow:
# 1. SELECT PSE (1PAY.SYS.DDF01) or PPSE (2PAY.SYS.DDF01)
# 2. SELECT AID (e.g., A0000000031010 for Visa)
# 3. GET PROCESSING OPTIONS → returns AFL + AIP
# 4. For each AFL entry, READ RECORD
def emulate_emv_read(apdu_send):
# Step 1: SELECT PPSE
select_ppse = [0x00, 0xA4, 0x04, 0x00, 0x0E] + \
[ord(c) for c in '2PAY.SYS.DDF01']
_, sw1, sw2 = apdu_send(select_ppse)
# Step 2: SELECT AID (from FCI template)
select_app = [0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x10, 0x10]
fci, sw1, sw2 = apdu_send(select_app)
# Step 3: GET PROCESSING OPTIONS (GPO)
# PDOL data depends on terminal capabilities
gpo = [0x80, 0xA8, 0x00, 0x00, 0x02, 0x83, 0x00] # Empty PDOL
gpo_response, sw1, sw2 = apdu_send(gpo)
# Parse AFL from GPO response (tag 0x94 inside 0x77 or 0x80)
tlvs = parse_tlv(gpo_response)
return tlvs
6. Common EMV Parsing Pitfalls
| Issue | Cause | Fix |
|---|---|---|
| Tag byte read as value | Padding bytes (0x00/0xFF) before tag | Skip padding before reading each tag |
| DOL data misalignment | Wrong length for tagged fields in PDOL/CDOL | Use exact EMV tag reference lengths (e.g., 9F1A=2 bytes) |
| Constructed tag not recursed | Template (0x77, 0x6F, 0xE0) value contains nested TLVs | Check tag bit 5 — recurse into value if constructed |
Related Tools
EMV TLV Parser — Decode TLV hex to tags | CVM List Decoder — Parse cardholder verification | EMV Cryptogram Visualizer — ARQC/ARPC generation | EMV Tag Reference — Full data object list