TLV (Tag–Length–Value) Nedir? BER‑TLV Kodlama, Parser Tasarımı ve Python Uygulaması
TLV veri yapısını byte seviyesinde açıklıyor; tek ve çok baytlı tag ile length kodlamasını, primitive/constructed nesneleri, sık yapılan hataları ve Python ile güvenli encoder/parser örneğini ePassport, akıllı kart, EMV ve NFC bağlamında inceliyoruz.
TLV, ikili veriyi kendi kendini tarif eden küçük parçalara ayıran bir kodlama yaklaşımıdır. Adı üç bileşenden gelir: Tag alanın ne olduğunu, Length kaç bayt veri taşıdığını, Value ise gerçek içeriği belirtir. Basit görünmesine rağmen doğru bir TLV uygulaması; çok baytlı tag’leri, uzunluk sınırlarını, iç içe nesneleri, profil kurallarını ve güvenilmeyen girdiyi birlikte ele almalıdır.
Bu yazıda genel TLV fikri ile akıllı kartlarda sık kullanılan BER‑TLV birbirinden ayrılacak; byte seviyesinde kodlama, güvenli parser tasarımı, sık yapılan hatalar ve çalışan bir Python örneği üzerinden uçtan uca incelenecektir.
1. TLV nedir?
En yalın haliyle bir TLV kaydı şöyledir:
+---------+----------+-------------------+
| Tag | Length | Value |
+---------+----------+-------------------+
tür boyut boyut kadar bayt
Örneğin tamamen uygulamaya özel bir protokolde 01 03 41 42 43 dizisi şu şekilde yorumlanabilir:
01: alanın etiketi,03: değerin uzunluğu üç bayt,41 42 43: ASCII karşılığıABColan değer.
Ancak TLV tek bir evrensel format değildir. “Tag önce, uzunluk sonra, değer en sonda” fikrini kullanan farklı profiller; tag bitlerini, byte sırasını, uzunluk kodlamasını, tekrar eden alanları ve padding davranışını farklı tanımlayabilir. Bu nedenle bir veri akışının “TLV” olduğunu bilmek parser yazmak için yeterli değildir; hangi TLV profiline ve veri sözlüğüne uyduğu da bilinmelidir.
2. Genel TLV ile BER‑TLV arasındaki fark
BER‑TLV, ASN.1 Basic Encoding Rules içindeki tag–length–value kurallarını temel alan yapıdır. ISO/IEC 7816 akıllı kart uygulamalarında ve ICAO eMRTD ekosisteminde çok sık görülür. BER‑TLV’de tag ve length alanları her zaman tek bayt değildir; ayrıca bir nesne doğrudan veri taşıyan primitive veya başka TLV nesnelerini taşıyan constructed türünde olabilir.
ISO/IEC 7816‑4; kart ile terminal arasındaki komut/yanıt alışverişini, veri nesnelerini, dosya ve uygulama yapılarını ve güvenli mesajlaşmayı tanımlar. ICAO Doc 9303 Part 10 ise elektronik seyahat belgelerinin Logical Data Structure yapısını tarif eder. Bu bağlamlarda kullanılan tag’lerin anlamı yalnızca BER kuralından değil, ilgili standardın veri sözlüğünden gelir.
3. Tag nasıl okunur?
BER‑TLV tag’inin ilk baytı üç bilgi taşır:
Bit: 8 7 | 6 | 5 4 3 2 1
sınıf C tag numarası
- 8–7. bitler: Universal, Application, Context-specific veya Private sınıfını belirtir.
- 6. bit:
1ise nesne constructed,0ise primitive’dir. - Alt beş bit:
11111, yani0x1Fdeğilse tag ilk baytta tamamlanır. - Alt beş bit
0x1Fise tag numarası sonraki baytlarda devam eder. Devam baytlarının en yüksek biti, başka bir tag baytı gelip gelmediğini gösterir.
Bu nedenle 5A tek baytlı bir tag iken 9F33 veya DF01 çok baytlı tag’lerdir. Parser’ın yalnızca ilk baytı tag kabul etmesi, sonraki baytı yanlışlıkla length olarak okumasına ve bütün veri akışının kaymasına yol açar.
Primitive ve constructed nesne
Primitive bir nesnenin value alanı ham içeriktir. Constructed bir nesnenin value alanı ise yeniden TLV kayıtlarından oluşur:
E1 1B
DF01 01 50
DF02 09 583132333435363738
DF03 08 3230333031323331
Burada E1 constructed bir kapsayıcıdır. İçindeki DF01, DF02 ve DF03 ise primitive alanlardır. Bu tag’ler aşağıdaki uygulama için seçilmiş private demo tag’leridir; ICAO veya EMV tarafından atanmış alanlar değildir.
4. Length nasıl yazılır?
BER‑TLV uzunluğunda iki temel biçim vardır:
Kısa biçim
Değer uzunluğu 0–127 bayt arasındaysa tek bayt kullanılır:
Uzunluk 5 → 05
Uzunluk 127 → 7F
Uzun biçim
Uzunluk 128 veya daha büyükse ilk length baytının en yüksek biti 1 olur. Alt yedi bit, uzunluğu taşıyan kaç ek bayt bulunduğunu söyler. Uzunluk değeri bu baytlarda big-endian yazılır:
Uzunluk 128 → 81 80
Uzunluk 255 → 81 FF
Uzunluk 256 → 82 01 00
Uzunluk 500 → 82 01 F4
81 7F teknik olarak 127 değerini ifade edebilse de gereksiz uzun kodlamadır; kısa biçim olan 7F kullanılmalıdır. ICAO Doc 9303 Part 10 da BER‑TLV length ve value alanlarının değişken olabileceğini, performans için mümkün olan en kısa şekilde kodlanmasını belirtir.
Genel BER’de 80 “indefinite length” anlamına gelebilir. Fakat birçok kart ve belge profili bunu kullanmaz veya yasaklar. Parser, yalnızca uyguladığı profil açıkça izin veriyorsa indefinite length kabul etmelidir; aksi halde güvenli biçimde reddetmelidir.
5. Value alanı nasıl ele alınır?
Length, karakter sayısını değil bayt sayısını belirtir. Bu ayrım özellikle UTF‑8 metinde önemlidir:
"ABC" → 3 karakter, 3 bayt
"İD" → 2 karakter, UTF-8 ile 3 bayt
Doğru sıra şöyledir: önce değer, profilin istediği byte gösterimine çevrilir; sonra oluşan byte dizisinin uzunluğu hesaplanır. Metnin karakter sayısını length alanına yazmak, ASCII dışındaki ilk karakterde kaymaya neden olur.
Value’nun anlamını tag sözlüğü belirler. Aynı byte dizisi bir tag altında metin, başka bir tag altında tarih, sayaç, BCD, imza veya başka TLV nesneleri olabilir. TLV taşıyıcı yapıdır; tek başına veri tipi şeması değildir.
6. Adım adım örnek paket
Bir demo uygulamasında belge türü, belge numarası ve son geçerlilik tarihini taşıyan şu özel alanları tanımlayalım:
DF01Belge türüP1 bayt
DF02Demo belge numarasıX123456789 bayt
DF03Son geçerlilik tarihi203012318 bayt
Alanların BER‑TLV karşılıkları:
DF01 01 50
DF02 09 58 31 32 33 34 35 36 37 38
DF03 08 32 30 33 30 31 32 33 31
Üç iç alan toplam 27 bayttır; onaltılık karşılığı 1B olur. Constructed E1 kapsayıcısıyla tam paket:
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
Boşluksuz aktarım biçimi:
E11BDF010150DF0209583132333435363738DF03083230333031323331
7. Python ile güvenli encoder ve parser
Aşağıdaki örnek; çok baytlı tag’i, kısa/uzun length’i ve constructed nesneleri destekler. Ayrıca eksik veri, profil dışı indefinite length, gereksiz uzun-form, aşırı büyük value ve aşırı iç içe yapı kontrolleri içerir.
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))
Programın ürettiği çıktı:
E11BDF010150DF0209583132333435363738DF03083230333031323331
E1 length=27 constructed
DF01 length=1 primitive
value=50
DF02 length=9 primitive
value=583132333435363738
DF03 length=8 primitive
value=3230333031323331
Bu kod, öğretici bir temel parser’dır. Üretimde buna profil bazlı izin verilen tag listesi, zorunlu alan kontrolü, tekrar ve sıralama kuralları, hassas veri maskeleme, akış tabanlı okuma, test vektörleri ve fuzz testleri eklenmelidir.
8. Uygularken dikkat edilmesi gereken hususlar
Profili ve tag sözlüğünü sabitleyin
Parser’a “her TLV’yi kabul et” yaklaşımıyla değil, uygulanan standardın sürümü ve veri sözlüğüyle başlanmalıdır. Bir tag’in zorunlu, isteğe bağlı, tekrar edebilir veya constructed olması şemadan doğrulanmalıdır.
Önce sınırı doğrulayın, sonra okuyun
Tag, length veya value için buffer dışına erişilmemelidir. Length okunduktan sonra offset + length değeri kalan veriye karşı kontrol edilmeli; doğrulanmamış length kadar bellek ayrılmamalıdır.
Kaynak tüketimini sınırlayın
Güvenilmeyen veri; devasa length, çok uzun tag veya yüzlerce kat constructed nesne ile CPU, bellek ya da stack tüketmeye çalışabilir. Maksimum paket boyutu, value boyutu, tag baytı ve nesting derinliği açıkça belirlenmelidir.
Canonical/minimal kodlamayı doğrulayın
Aynı uzunluğun birden fazla byte dizisiyle ifade edilmesine izin vermek; imza, hash, önbellek anahtarı ve farklı parser’lar arasında tutarsızlık yaratabilir. Profil minimal kodlama istiyorsa encoder bunu üretmeli, decoder de minimal olmayan biçimleri reddetmelidir.
Bilinmeyen tag politikasını belirleyin
İleri uyumluluk gereken sistemlerde bilinmeyen tag, length kadar güvenli biçimde atlanabilir veya ham haliyle korunabilir. Güvenlik kararını etkileyen bir şemada ise bilinmeyen kritik alan reddedilebilir. Karar, uygulama profiline ait olmalıdır; parser içinde rastgele verilmemelidir.
APDU durum baytlarını TLV sanmayın
Akıllı kart APDU yanıtının sonundaki SW1 SW2 durum baytları, çoğu senaryoda yanıt veri alanından ayrıdır. Önce APDU çerçevesi ayrıştırılmalı ve durum kontrol edilmeli, yalnızca veri bölümü TLV parser’a verilmelidir.
Padding’i evrensel kabul etmeyin
00 veya FF baytlarını koşulsuz biçimde atlamak doğru değildir. Padding’in nerede ve nasıl kullanılabileceğini ilgili profil tanımlar. Bir protokolde izin verilen davranış, başka bir BER‑TLV akışında bozuk veri anlamına gelebilir.
Loglarda value’yu maskeleyin
Belge numarası, biyometri, kart verisi veya kişisel alan içeren tüm TLV paketini hata loguna yazmak veri sızıntısına dönüşebilir. Tag ve length operasyonel teşhis için yeterli olabilir; hassas value alanları maskelenmeli veya hiç kaydedilmemelidir.
9. Sık yapılan hatalar
- Tag’i tek bayt sanmak:
DF01gibi tag’leri iki ayrı alan olarak yorumlamak. - Length’i tek bayt sanmak:
81 80dizisinde 0x81’i doğrudan 129 bayt kabul etmek. - Karakter sayısını yazmak: UTF‑8’e çevrilmiş gerçek byte uzunluğu yerine metin uzunluğunu kullanmak.
- Hex ve decimal’i karıştırmak:
10hex değerinin decimal 16 olduğunu gözden kaçırmak. - Constructed bitini yok saymak: İç içe TLV taşıyan value’yu düz bir byte dizisi olarak bırakmak.
- Her constructed tag’i ezbere belirlemek: Tag’in ilk baytındaki constructed bitini ve profil şemasını kontrol etmemek.
- Length sınır kontrolü yapmamak: Eksik veya kötü niyetli pakette buffer dışına taşmak.
- Indefinite ve minimal olmayan biçimleri körlemesine kabul etmek: Parser’lar arasında farklı yorumlara kapı açmak.
- Tekrarlı alanları sözlüksüz reddetmek: Bazı profillerde aynı tag’in tekrar edebileceğini hesaba katmamak.
- Sıralamayı varsaymak: Profil sıralama zorunluluğu getirmiyorsa alanları yalnızca konumuna göre anlamlandırmak.
- APDU’nun tamamını parse etmek: Sondaki status word’leri TLV sanmak.
- Hassas değeri loglamak: Üretim loglarında kişisel veya ödeme verisi bırakmak.
10. Güncel kullanım alanları
ePassport ve elektronik kimlik
ICAO Doc 9303’ün eMRTD Logical Data Structure yapısında BER‑TLV veri nesneleri önemli bir yer tutar. Dosya seçimi, güvenlik nesneleri, veri grupları ve çip iletişimi birlikte ele alınır. Ancak her eMRTD dosyası “düz bir TLV listesi” değildir; biyometrik şablonlar, ASN.1 yapıları ve protokole özel kapsayıcılar kendi standardına göre ayrıştırılmalıdır.
ISO/IEC 7816 akıllı kartları
Kart uygulaması seçimi, veri nesnelerinin taşınması, dosya işlemleri ve güvenli mesajlaşma gibi alanlarda BER‑TLV yaygın bir yapı taşıdır. APDU, taşıma çerçevesini; BER‑TLV ise komut veya yanıtın veri bölümündeki nesneleri düzenleyebilir.
EMV temaslı ve temassız ödeme
EMV chip ve contactless spesifikasyonları, kart ile kabul terminalinin birlikte çalışmasını tanımlar; uygulama verilerinde çok sayıda BER‑TLV nesnesi kullanılır. Burada tag anlamları ödeme profilinin veri sözlüğünden okunmalı, genel bir BER parser ile işlem kuralları birbirinden ayrılmalıdır.
NFC Forum tag’leri
NFC Forum Type 2 Tag yapısında NDEF mesajlarını ve bellek bölgelerini tanımlayan TLV yapıları bulunur. Fakat NFC Type 2 TLV ile ISO 7816 BER‑TLV aynı format değildir. İkisinin de adı TLV olsa da tag ve length kuralları ilgili NFC Forum spesifikasyonuna göre uygulanmalıdır.
ASN.1 BER/DER tabanlı güvenlik yapıları
Sertifikalar ve birçok kriptografik veri yapısı ASN.1 ile modellenip DER gibi kurallarla kodlanır. Temelde tag, length ve value bileşenleri vardır; ancak DER’in canonical kuralları, uygulamaya özel basit bir TLV formatından daha katıdır. Bu tür veri için genel amaçlı, olgun ASN.1 kütüphaneleri tercih edilmelidir.
11. Test stratejisi
Yalnızca başarılı bir paketle yapılan test yeterli değildir. En az şu sınırlar kapsanmalıdır:
- Sıfır uzunluklu value, 127, 128, 255 ve 256 bayt sınırları,
- Tek ve çok baytlı tag’ler,
- Birden fazla constructed seviyesi,
- Yarım bırakılmış tag, length ve value,
- Kalan veriden büyük length,
- Gereksiz uzun-form ve başında
00bulunan length, - İzin verilmeyen indefinite length,
- Bilinmeyen, tekrarlı ve sırası değişmiş tag’ler,
- Maksimum boyut ve nesting limitinin hemen altı/üstü,
decode(encode(x)) == xround-trip testleri,- Rastgele ve mutasyona uğratılmış girdilerle fuzz testleri.
12. Sonuç
TLV’nin gücü, veri alanlarını genişletilebilir ve ayrıştırılabilir bir biçimde taşımasından gelir. Güvenilir bir uygulamanın anahtarı ise yalnızca üç alanı sırayla okumak değildir: doğru profil, çok baytlı tag ve length desteği, constructed yapıların kontrollü çözülmesi, byte seviyesinde uzunluk hesabı, kaynak limitleri ve şema doğrulaması birlikte uygulanmalıdır.
Özellikle güvenli belge ve akıllı kart sistemlerinde TLV parser bir yardımcı fonksiyondan fazlasıdır. Protokol sınırında çalışan bu bileşen, güvenilmeyen byte dizisini uygulamanın karar verebileceği veri nesnelerine dönüştürür. Bu yüzden parser ne kadar katı, ölçülebilir ve test edilebilir tasarlanırsa sistemin birlikte çalışabilirliği ve güvenliği de o kadar güçlü olur.
Resmî kaynaklar
Bu makaleyi nasıl değerlendirirsin?
Geri bildirimin sonraki içerikleri geliştirmeme yardımcı olur.