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
Test this yourself: Our EMV TLV Parser decodes any EMV hex dump — paste raw TLV bytes and get human-readable tag names, lengths, and parsed values instantly. Also try the CVM List Decoder for cardholder verification methods.

6. Common EMV Parsing Pitfalls

IssueCauseFix
Tag byte read as valuePadding bytes (0x00/0xFF) before tagSkip padding before reading each tag
DOL data misalignmentWrong length for tagged fields in PDOL/CDOLUse exact EMV tag reference lengths (e.g., 9F1A=2 bytes)
Constructed tag not recursedTemplate (0x77, 0x6F, 0xE0) value contains nested TLVsCheck 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