Software
Türkçe okuWhat Is TLV (Tag–Length–Value)? BER-TLV Encoding, Parser Design, and Python Implementation
We explain the TLV data structure at the byte level; we examine single- and multi-byte tag-and-length encoding, primitive and constructed objects, common mistakes, and a safe encoder/parser example in Python in the context of ePassports, smart cards, EMV, and NFC.
TLV is an encoding approach that breaks binary data down into small, self-describing chunks. Its name comes from three components: “Tag” specifies what the field is, “Length” indicates how many bytes of data it carries, and “Value” specifies the actual content. Although it may seem simple, a proper TLV implementation must handle multi-byte tags, length limits, nested objects, profile rules, and untrusted input all at once.
In this article, we will distinguish between the general concept of TLV and BER-TLV—which is commonly used in smart cards—and examine the topic end-to-end through byte-level encoding, secure parser design, common mistakes, and a working Python example.
1. What is TLV?
In its simplest form, a TLV record looks like this:
+---------+----------+-------------------+
| Tag | Length | Value |
+---------+----------+-------------------+
tür boyut boyut kadar bayt
For example, in a completely application-specific protocol, 01 03 41 42 43 the sequence can be interpreted as follows:
01: the field label,03: the value’s length is three bytes,41 42 43: the ASCII equivalentABC.
However, TLV is not a single universal format. Different profiles that use the “tag first, length next, value last” concept may define tag bits, byte order, length encoding, repeating fields, and padding behavior differently. Therefore, simply knowing that a data stream is “TLV” is not sufficient to write a parser; one must also know which TLV profile and data dictionary it conforms to.
2. The Difference Between General TLV and BER-TLV
BER-TLV is a structure based on the tag–length–value rules within the ASN.1 Basic Encoding Rules. It is commonly found in ISO/IEC 7816 smart card applications and the ICAO eMRTD ecosystem. In BER-TLV, the tag and length fields are not always a single byte; furthermore, an object can be either a primitive that carries data directly or a constructed type that carries other TLV objects.
ISO/IEC 7816-4 defines the command/response exchange between the card and the terminal, data objects, file and application structures, and secure messaging. ICAO Doc 9303 Part 10, on the other hand, describes the Logical Data Structure of electronic travel documents. The meaning of the tags used in these contexts derives not only from the BER rules but also from the data dictionary of the relevant standard.
3. How is a tag read?
The first byte of a BER-TLV tag carries three pieces of information:
Bit: 8 7 | 6 | 5 4 3 2 1
sınıf C tag numarası
- Bits 8–7: Specify the class (Universal, Application, Context-specific, or Private).
- 6th bit:
1If the object is a constructed object,0it is a primitive. - The lower five bits:
11111, that is,0x1Fotherwise, the tag is completed in the first byte. - The lower five bits
0x1F, the tag number continues in the following bytes. The most significant bit of the continuation bytes indicates whether another tag byte follows.
Therefore, 5A when the tag is a single byte, 9F33 or DF01 multi-byte tags. If the parser treats only the first byte as the tag, it will mistakenly read the next byte as the length, causing the entire data stream to be misaligned.
Primitive and constructed objects
The `value` field of a primitive object contains raw data. The `value` field of a constructed object, on the other hand, consists of TLV records:
E1 1B
DF01 01 50
DF02 09 583132333435363738
DF03 08 3230333031323331
Here E1 is a constructed container. Inside it, DF01, DF02 and DF03 are primitive fields. These tags are private demo tags selected for the following application; they are not fields assigned by ICAO or EMV.
4. How is “Length” written?
There are two basic formats for the BER-TLV length:
Short format
If the value length is between 0 and 127 bytes, a single byte is used:
Uzunluk 5 → 05
Uzunluk 127 → 7F
Long format
If the length is 128 or greater, the most significant bit of the first length byte 1 . The lower seven bits indicate how many additional bytes carry the length. The length value is written in big-endian format in these bytes:
Uzunluk 128 → 81 80
Uzunluk 255 → 81 FF
Uzunluk 256 → 82 01 00
Uzunluk 500 → 82 01 F4
81 7F Although it can technically represent the value 127, this is an unnecessarily long encoding; the short form 7F should be used. ICAO Doc 9303 Part 10 also specifies that the BER-TLV length and value fields may be variable and should be encoded in the shortest possible form for performance.
In general BER, 80 it may imply “indefinite length.” However, many card and document profiles do not use or prohibit this. The parser should accept indefinite length only if the profile it is applying explicitly permits it; otherwise, it should safely reject it.
5. How is the “Value” field handled?
The `length` field specifies the number of bytes, not the number of characters. This distinction is particularly important for UTF‑8 text:
"ABC" → 3 karakter, 3 bayt
"İD" → 2 karakter, UTF-8 ile 3 bayt
The correct order is as follows: first, the value is converted to the byte representation required by the profile; then, the length of the resulting byte sequence is calculated. Writing the number of characters in the text to the `length` field causes a shift starting with the first non-ASCII character.
The meaning of `Value` is determined by the tag dictionary. The same byte sequence can represent text under one tag, and a date, counter, BCD, signature, or other TLV objects under another tag. TLV is a container structure; it is not a data type schema on its own.
6. Step-by-Step Sample Package
In a demo application, let’s define the following custom fields that carry the document type, document number, and expiration date:
DF01Document TypeP1 byte
DF02Demo document numberX123456789 bytes
DF03Expiration date203012318 bytes
BER-TLV equivalents of the fields:
DF01 01 50
DF02 09 58 31 32 33 34 35 36 37 38
DF03 08 32 30 33 30 31 32 33 31
The three internal fields total 27 bytes; their hexadecimal equivalent 1B . A complete packet with the E1 container:
E1 1B
DF 01 01 50
DF 02 09 58 31 32 33 34 35 36 37 38
DF 03 08 32 30 33 30 31 32 33 31
Space-separated transfer format:
E11BDF010150DF0209583132333435363738DF03083230333031323331
7. Secure encoder and parser with Python
The following example supports multi-byte tags, short/long lengths, and `constructed` objects. It also includes checks for missing data, out-of-profile indefinite lengths, unnecessary long-form values, excessively large values, and excessively nested structures.
from dataclasses import dataclass, field
class TLVError(ValueError):
pass
@dataclass
class TLVNode:
tag: bytes
value: bytes
children: list["TLVNode"] = field(default_factory=list)
@property
def constructed(self) -> bool:
return bool(self.tag[0] & 0x20)
def read_tag(data: bytes, offset: int) -> tuple[bytes, int]:
if offset >= len(data):
raise TLVError("Tag beklenirken veri bitti")
start = offset
first = data[offset]
offset += 1
# Alt 5 bit 0x1F ise high-tag-number biçimi kullanılır.
if (first & 0x1F) == 0x1F:
continuation_count = 0
while True:
if offset >= len(data):
raise TLVError("Çok baytlı tag tamamlanmadı")
current = data[offset]
offset += 1
continuation_count += 1
# Uygulama limiti: kontrolsüz tag büyümesini engeller.
if continuation_count > 3:
raise TLVError("Tag uygulama limitinden uzun")
# İlk devam baytında sıfır tag grubu minimal değildir.
if continuation_count == 1 and (current & 0x7F) == 0:
raise TLVError("Minimal olmayan tag kodlaması")
if (current & 0x80) == 0:
break
return data[start:offset], offset
def read_length(data: bytes, offset: int) -> tuple[int, int]:
if offset >= len(data):
raise TLVError("Length beklenirken veri bitti")
first = data[offset]
offset += 1
if first < 0x80:
return first, offset
length_octets = first & 0x7F
if length_octets == 0:
raise TLVError("Indefinite length bu profilde desteklenmiyor")
if length_octets > 4:
raise TLVError("Length alanı uygulama limitinden uzun")
if offset + length_octets > len(data):
raise TLVError("Length alanı tamamlanmadı")
if data[offset] == 0:
raise TLVError("Length başında gereksiz 00 var")
length = int.from_bytes(
data[offset:offset + length_octets],
byteorder="big"
)
offset += length_octets
if length < 0x80:
raise TLVError("Kısa yazılabilecek length uzun-form ile kodlanmış")
return length, offset
def encode_length(length: int) -> bytes:
if length < 0:
raise TLVError("Length negatif olamaz")
if length < 0x80:
return bytes([length])
encoded = length.to_bytes((length.bit_length() + 7) // 8, "big")
if len(encoded) > 4:
raise TLVError("Length uygulama limitinden uzun")
return bytes([0x80 | len(encoded)]) + encoded
def validate_tag(tag: bytes) -> None:
parsed, end = read_tag(tag, 0)
if end != len(tag) or parsed != tag:
raise TLVError("Geçersiz tag")
def encode_tlv(tag_hex: str, value: bytes) -> bytes:
tag = bytes.fromhex(tag_hex)
validate_tag(tag)
return tag + encode_length(len(value)) + value
def parse_tlvs(
data: bytes,
*,
depth: int = 0,
max_depth: int = 8,
max_value_length: int = 1_048_576
) -> list[TLVNode]:
if depth > max_depth:
raise TLVError("Maksimum iç içe TLV derinliği aşıldı")
nodes: list[TLVNode] = []
offset = 0
while offset < len(data):
tag, offset = read_tag(data, offset)
length, offset = read_length(data, offset)
if length > max_value_length:
raise TLVError("Value uygulama limitinden büyük")
end = offset + length
if end > len(data):
raise TLVError("Length, kalan veriden büyük")
value = data[offset:end]
offset = end
children = []
if tag[0] & 0x20:
children = parse_tlvs(
value,
depth=depth + 1,
max_depth=max_depth,
max_value_length=max_value_length
)
nodes.append(TLVNode(tag=tag, value=value, children=children))
return nodes
def print_tree(nodes: list[TLVNode], indent: int = 0) -> None:
for node in nodes:
prefix = " " * indent
kind = "constructed" if node.constructed else "primitive"
print(
f"{prefix}{node.tag.hex().upper()} "
f"length={len(node.value)} {kind}"
)
if node.children:
print_tree(node.children, indent + 1)
else:
print(f"{prefix} value={node.value.hex().upper()}")
document_type = encode_tlv("DF01", b"P")
document_number = encode_tlv("DF02", b"X12345678")
expiry_date = encode_tlv("DF03", b"20301231")
inner = document_type + document_number + expiry_date
packet = encode_tlv("E1", inner)
print(packet.hex().upper())
print_tree(parse_tlvs(packet))
Output generated by the program:
E11BDF010150DF0209583132333435363738DF03083230333031323331
E1 length=27 constructed
DF01 length=1 primitive
value=50
DF02 length=9 primitive
value=583132333435363738
DF03 length=8 primitive
value=3230333031323331
This code is a basic tutorial parser. In production, a profile-based list of allowed tags, mandatory field checks, repetition and ordering rules, sensitive data masking, stream-based reading, test vectors, and fuzz tests should be added.
8. Important Considerations During Implementation
Fix the profile and tag dictionary
The parser should not be approached with a “accept every TLV” mindset; instead, it should start with the version of the applicable standard and the data dictionary. Whether a tag is required, optional, repeatable, or constructed must be verified against the schema.
Verify the bounds first, then read
Do not access beyond the buffer limits for the tag, length, or value. After reading the length, offset + length the value must be checked against the remaining data; memory should not be allocated for an unverified length.
Limit resource consumption
Untrusted data—such as enormous lengths, excessively long tags, or hundreds of nested constructed objects—can attempt to consume CPU, memory, or stack resources. The maximum packet size, value size, tag bytes, and nesting depth must be clearly defined.
Validate canonical/minimal encoding
Allowing the same length to be represented by multiple byte sequences can create inconsistencies across signatures, hashes, cache keys, and different parsers. If the profile requires minimal encoding, the encoder must produce it, and the decoder must reject non-minimal formats.
Define the policy for unknown tags
In systems requiring forward compatibility, an unknown tag can be safely skipped for the duration of the length or preserved in its raw form. In a schema where security is a concern, however, an unknown critical field may be rejected. This decision should be specific to the application profile; it should not be determined arbitrarily within the parser.
Do not mistake APDU status bytes for TLVs
The status bytes at the end of a smart card APDU response SW1 SW2 status bytes at the end of a smart card APDU response are, in most scenarios, separate from the response data field. The APDU frame must first be parsed and the status checked; only the data section should be passed to the TLV parser.
Do not treat padding as universal
00 or FF it is incorrect to unconditionally skip its bytes. The relevant profile defines where and how padding can be used. Behavior permitted in one protocol may constitute corrupted data in another BER-TLV stream.
Mask the value in logs
Writing the entire TLV packet—including the document number, biometric data, card data, or personal fields—to the error log can lead to data leaks. The tag and length may be sufficient for operational diagnostics; sensitive value fields should be masked or not recorded at all.
9. Common Mistakes
- Assuming the tag is a single byte:
DF01interpreting tags like this as two separate fields. - Assuming the "length" is a single byte:
81 80directly interpreting 0x81 in the array as 129 bytes. - Writing the character count: Using the text length instead of the actual byte length converted to UTF-8.
- Mixing hex and decimal:
10Overlooking that a hex value is equivalent to 16 in decimal. - Ignoring the constructed bit: Leaving a value containing nested TLVs as a plain byte array.
- Determining every constructed tag by rote: Failing to check the constructed bit in the tag’s first byte and the profile schema.
- Failing to perform length boundary checks: Overflowing the buffer due to an incomplete or malicious packet.
- Blindly accepting indefinite and non-minimal formats: Opening the door to differing interpretations among parsers.
- Rejecting repeated fields without a dictionary: Failing to account for the possibility that the same tag may repeat in some profiles.
- Assuming order: Interpreting fields solely based on their position if the profile does not require a specific order.
- Parsing the entire APDU: Mistaking the final status words for TLVs.
- Logging sensitive values: Leaving personal or payment data in production logs.
10. Current use cases
ePassport and electronic ID
BER-TLV data objects play a significant role in the eMRTD Logical Data Structure defined by ICAO Doc 9303. File selection, security objects, data groups, and chip communication are addressed collectively. However, not every eMRTD file is a “plain TLV list”; biometric templates, ASN.1 structures, and protocol-specific containers must be parsed according to their respective standards.
ISO/IEC 7816 smart cards
BER-TLV is a common building block in areas such as card application selection, data object transfer, file operations, and secure messaging. While APDU defines the transport frame, BER-TLV organizes the objects within the data section of a command or response.
EMV Contact and Contactless Payments
EMV chip and contactless specifications define the interoperability between the card and the acceptance terminal; numerous BER-TLV objects are used in the application data. Here, tag meanings must be read from the payment profile’s data dictionary, and processing rules must be distinguished from a general BER parser.
NFC Forum tags
The NFC Forum Type 2 Tag structure contains TLV structures that define NDEF messages and memory regions. However, the NFC Type 2 TLV and the ISO 7816 BER-TLV are not the same format. Although both are called TLV, the tag and length rules must be applied according to the relevant NFC Forum specification.
ASN.1 BER/DER-Based Security Structures
Certificates and many cryptographic data structures are modeled using ASN.1 and encoded according to rules such as DER. Fundamentally, there are tag, length, and value components; however, DER’s canonical rules are stricter than a simple, application-specific TLV format. General-purpose, mature ASN.1 libraries should be preferred for this type of data.
11. Test Strategy
Testing with a single successful packet is not sufficient. At a minimum, the following boundaries must be covered:
- Zero-length values, and the 127, 128, 255, and 256-byte boundaries,
- Single- and multi-byte tags,
- Multiple constructed levels,
- Incomplete tag, length, and value,
- `length` values greater than the remaining data,
- Unnecessary long-form and length
00lengths, - Unallowed indefinite length,
- Unknown, duplicate, and out-of-order tags,
- Values just below or above the maximum size and nesting limits,
decode(encode(x)) == xround-trip tests,- Fuzz tests with random and mutated inputs.
12. Conclusion
TLV’s strength lies in its ability to carry data fields in an extensible and parsable format. However, the key to a reliable implementation is not merely reading the three fields in sequence: the correct profile, multibyte tag and length support, controlled parsing of constructed structures, byte-level length calculation, resource limits, and schema validation must all be applied together.
Especially in secure document and smart card systems, the TLV parser is more than just a helper function. Operating at the protocol boundary, this component transforms an untrusted byte sequence into data objects that the application can process. Therefore, the more rigorously, measurably, and testably the parser is designed, the stronger the system’s interoperability and security will be.
Official sources
How would you rate this article?
Your feedback helps improve future articles.