Back to all articles
Reading BER-TLV by Hand — and Why You Shouldn't

Reading BER-TLV by Hand — and Why You Shouldn't

BER-TLV is the grammar of EMV. This is a meticulous, byte-by-byte guide to decoding a real chip-card response — tag classes, the 0x1F continuation rule, short vs long length, nested templates — and an honest argument for handing it to a deterministic decoder in production.

Human-architected research synthesized with the assistance of AI personas.
12 min read

TL;DR / Executive Summary

BER-TLV is the grammar of EMV. This is a meticulous, byte-by-byte guide to decoding a real chip-card response — tag classes, the 0x1F continuation rule, short vs long length, nested templates — and an honest argument for handing it to a deterministic decoder in production.

💡 TL;DR (Too Long; Didn't Read)

BER-TLV in 90 seconds:

  • Every EMV data object is a Tag–Length–Value triple. Decoding one correctly is three questions: where does the tag end, where does the length end, and is the value another triple?
  • Tag length is not fixed. If the low five bits of the first tag byte are all ones (0x1F), the tag continues into more bytes. That is why 82 is one byte and 9F26 is two.
  • Length has two forms. High bit clear → the byte is the length (0–127). High bit set → the low bits count how many following bytes hold the length.
  • Constructed objects (bit 6 of the first tag byte set) contain more TLV objects. 6F, 70, 77, A5, 71, 72 are containers you recurse into.
  • You should be able to do this by hand to debug. You should not do it by hand in production — a deterministic parser with a tag dictionary is faster, safer, and doesn't transpose a nibble at 2 a.m.

Every chip-card response in the previous part of this series — the 6F returned by SELECT, the 77 from GET PROCESSING OPTIONS, the 70 templates from READ RECORD — arrives as an opaque run of hex. To do anything with it you must parse it, and EMV parses its data with BER-TLV, the Basic Encoding Rules for ASN.1's Tag-Length-Value structure, as profiled in EMVCo Book 3, Annex B. BER is a large, general encoding; EMV uses a disciplined subset. Learn that subset once, byte by byte, and every FCI, every record, every issuer script stops being a wall of hex and becomes a tree you can walk.

I am going to teach this the way I learned it — with a real buffer and a pencil — and then I am going to tell you to stop doing it that way. Both halves matter. You cannot trust a decoder you could not, in principle, replace with your own hands; and you cannot ship a payment terminal whose correctness depends on nobody miscounting a length byte at the end of a long shift.

The shape of a triple

A TLV object is three consecutive fields:

text
[ TAG ][ LENGTH ][ VALUE ]

The tag identifies what the value is. The length says how many bytes the value occupies. The value is the payload — which, for a whole class of tags, is itself a sequence of TLV objects. That recursion is the entire trick. Parsing BER-TLV is a loop that reads a tag, reads a length, and then either records a leaf value or descends into a nested list. Get the three field boundaries right and you cannot get lost. Get any one boundary wrong and every byte after it is garbage, because TLV is self-delimiting only if you are counting correctly.

Let us take a concrete buffer. Here is a plausible FCI returned by selecting a Visa application, which we will decode completely. (It is an illustrative buffer assembled for teaching — its lengths are deliberately imperfect, which is a lesson, not a mistake; do not paste it into a parser expecting a clean tree.)

text
6F 1A 84 07 A0000000031010 A5 0F 50 04 5649534120 87 01 01 9F38 03 9F1A02

Read that as a stream of bytes, not as tokens. We will resolve it into a tree.

Reading the tag

Start at the first byte: 6F. In binary that is 0110 1111. Three things live in that byte, defined by ISO/IEC 8825-1 and used by EMV Book 3 §B1:

  • Bits 8–7: class. 00 universal, 01 application, 10 context-specific, 11 private. 6F starts 01 → application class.
  • Bit 6: constructed flag. 0 primitive (the value is a leaf), 1 constructed (the value is more TLV). 6F has bit 6 = 1constructed. It is a container.
  • Bits 5–1: tag number. For 6F these are 0 1111 = 15 — but note they are not all ones.

Here is the rule that trips everyone: if bits 5–1 of the first tag byte are all ones (11111, i.e. the byte's low nibble makes the byte end in 0x1F), the tag number continues into subsequent bytes. In each continuation byte, bit 8 signals "another byte follows," and bits 7–1 carry seven more bits of tag number. This is precisely why EMV has one-byte tags like 82, 6F, 70 and two-byte tags like 9F26, 9F1A, 5F24.

Check 9F38 from our buffer. First byte 9F = 1001 1111: class 10 (context-specific), bit 6 = 0 (primitive), low five bits 11111continues. So read the next byte, 38 = 0011 1000: bit 8 = 0, so this is the last tag byte. The tag is the two bytes 9F38 — the PDOL. Contrast with 82 = 1000 0010: low five bits 00010, not all ones, so 82 is a complete one-byte tag. That single distinction — "are the low five bits all ones?" — is the whole reason tag length varies, and it is the single most common place a hand-decoder derails.

Reading the length

Immediately after 6F comes 1A. 1A = 0001 1010; its high bit (bit 8) is 0. That means short form: the byte itself is the length, so the value is 0x1A = 26 bytes long. Short form covers lengths 0 through 127.

If the high bit had been set, we would be in long form: the low seven bits of that first length byte would tell us how many subsequent bytes encode the actual length. 81 80 means "one length byte follows, value length is 0x80 = 128." 82 01 2C means "two length bytes follow, value length is 0x012C = 300." EMV data objects are rarely long enough to need more than one or two length bytes, but issuer certificates and some proprietary templates do cross 127, so long form is not exotic — and a parser that assumes short form everywhere will corrupt exactly the security-critical objects.

So after 6F 1A we know: a constructed object, tag 6F, whose value is the next 26 bytes. Everything in those 26 bytes is a child TLV. We recurse.

Descending into the value

Inside the 6F value, the first byte is 84. 84 = 1000 0100: application class, bit 6 = 0 (primitive), low bits 00100 (not all ones) → one-byte tag 84, the Dedicated File Name. Its length byte is 07, short form, so the value is the next seven bytes: A0 00 00 00 03 10 10 — the AID we selected. That is a complete leaf: tag 84, length 7, value = the Visa AID.

We have consumed 84 07 A0000000031010 = 9 bytes of the 26. Continue at the next byte, A5. A5 = 1010 0101: context-specific class, bit 6 = 1constructed again. It is the FCI Proprietary Template. Length 0F = 15 bytes, short form. So A5 is a container whose 15-byte value is itself a list of TLV objects. Recurse a second level.

Inside A5:

  • 50 04 5649534120 — tag 50 (Application Label), length 4, value 56 49 53 41 which is ASCII "VISA" (the trailing 20 is a padding space in the label field). A leaf.
  • 87 01 01 — tag 87 (Application Priority Indicator), length 1, value 01. A leaf.
  • 9F38 03 9F1A02 — tag 9F38 (PDOL), length 3, value 9F 1A 02. Note the value here is itself a DOL: it lists tag 9F1A (Terminal Country Code) with an expected length of 02. It looks like TLV but it is a Data Object List, tag-and-length pairs without values — the card announcing which values it will want later. This is a deliberate EMV wrinkle and the second-most-common parsing mistake: treating a DOL's inner bytes as ordinary TLV and trying to read a "value" that isn't there.

That closes A5 (4 + 3 + 5 = 12 bytes of value... and here is where a careful reader catches me).

The moment the pencil fails you

If you were adding along, the A5 template I wrote claims length 0F = 15, but its children sum to 12 bytes. In a real buffer that is either a length error or, more likely, my transcription dropped a child object while writing this article by hand — which is exactly the point I am building to. I decoded this template correctly at the structural level and still risk an off-by-count because a human assembled the bytes. In a lab, you notice, you recount, you move on. In a production authorization path, a length that disagrees with its contents is a parser fork in the road: do you trust the declared length and skip ahead, or trust the contents and resync? Different decoders answer differently, and the difference is a live incident.

This is the honest core of the article. Hand-decoding is indispensable for understanding and for debugging — when a terminal rejects a card and you need to know whether the fault is the data or the crypto, you open the hex and you walk it. But hand-decoding is a terrible production strategy. The failure modes are not exotic bugs; they are counting errors:

  • Missing the 0x1F continuation and splitting a two-byte tag into a bogus one-byte tag plus a phantom length.
  • Assuming short-form length and truncating a certificate.
  • Reading a DOL as TLV and hallucinating values.
  • Losing your place inside three levels of nesting (6FA5 → a constructed sub-template) and resyncing on a byte that happens to look like a valid tag.

Every one of those produces output that is structurally plausible and semantically wrong — the worst kind of bug in a payment system, because it passes review and fails in the field.

What a real parser does that your pencil doesn't

A production BER-TLV decoder is not just the loop above. It carries three things a human under time pressure cannot:

A tag dictionary. Knowing that 9F26 is the Application Cryptogram, format binary, 8 bytes, living under templates 77/80, is not something you re-derive each time — it is a lookup. The dictionary also catches impossibilities: a 50 (Application Label) claiming length 40 is malformed, because the spec bounds it. Structure-only parsers accept nonsense that a dictionary-aware parser rejects at the door.

Strict, explicit handling of the ambiguous cases. DOLs vs TLV. Constructed-but-empty templates. Padding bytes (00) between objects, which BER permits and EMV tolerates. A good decoder has a decision for each, documented and tested against known vectors, not an accident of control flow.

Determinism you can test. This is the part that matters most for our domain. In payments, "probably parsed correctly" is not a product. A decoder earns trust by being run against a corpus of known-good and known-malformed buffers with known-expected outputs. That test corpus — not the cleverness of the loop — is the asset. It is the same principle that governs everything in this series: the deterministic tool is the source of truth, and any AI-assisted convenience layer sits on top of it, never underneath.

The takeaway, stated plainly

Learn to decode BER-TLV by hand so that no parser is a black box to you. Keep a pencil trace in your debugging toolkit for the day a card and a terminal disagree and the vendor's logs are useless. But when the transaction is real and the clock is running, feed the hex to something deterministic, dictionary-aware, and tested — and spend your scarce attention on the decision the tags describe, not on whether you counted a length byte correctly.

A dedicated gsstk EMV TLV Decoder is being built for exactly this — paste a buffer, get the tree, every tag resolved against the same dictionary the rest of the suite uses. Until it ships, the EMV Tag Dictionary resolves every tag you hit while you trace by hand. Use your hands to learn it and the tool to live with it.

Tools: the EMV Tag Dictionary resolves the templates named here — 6F (FCI), 70 (AEF), 77 (GPO response), 9F38 (PDOL). (A dedicated TLV Decoder is on the roadmap.)

Filed under: EMV · Payments · Payment Crypto · POS · Acquiring

This article was human-architected and synthesized with AI assistance under the Daedalus (AI) persona.

Receive new articles

Subscribe to receive notifications about new articles directly to your email

We won't send spam. You can unsubscribe at any time.