TLV-структура: Tag, Length, Value

TLV (Tag-Length-Value) — базовый способ кодирования структурированных данных, применяемый в ASN.1 и DER-форматах, на которых основаны X.509 сертификаты, ключи, подписи и многие криптографические структуры в JavaScript-библиотеке Jsrsasign.

TLV представляет собой последовательность из трёх компонентов:

  • Tag (тег) — идентификатор типа данных
  • Length (длина) — размер полезной нагрузки
  • Value (значение) — сами данные

Эта модель лежит в основе бинарного представления сложных криптографических объектов, где данные должны быть строго структурированы и однозначно интерпретируемы.


Tag определяет тип закодированного значения. В ASN.1 каждый элемент имеет свой идентификатор, который указывает, как интерпретировать байты Value.

Tag состоит из:

  • класса (class)
  • признака примитивности/конструкции (primitive/constructed)
  • номера типа (tag number)

На уровне DER это обычно один или несколько байтов.

Примеры часто встречающихся тегов:

  • 0x02 — INTEGER
  • 0x03 — BIT STRING
  • 0x04 — OCTET STRING
  • 0x05 — NULL
  • 0x06 — OBJECT IDENTIFIER
  • 0x30 — SEQUENCE (структура, содержащая другие TLV)

В Jsrsasign работа с тегами скрыта за высокоуровневыми конструкторами ASN.1, например:

var seq = new KJUR.asn1.DERSequence({
  array: [
    new KJUR.asn1.DERInteger({int: 3}),
    new KJUR.asn1.DERUTF8String({str: "test"})
  ]
});

Здесь DERSequence автоматически использует тег 0x30, а вложенные элементы получают свои собственные теги.


Length (длина)

Length определяет размер поля Value в байтах. В DER используется два основных варианта кодирования длины:

Короткая форма

Если длина меньше 128:

0x00 - 0x7F

Один байт напрямую указывает длину.

Длинная форма

Если длина ≥ 128:

  • первый байт: 0x80 + количество байт длины
  • последующие байты: сама длина

Пример:

0x82 0x01 0x2C

Это означает длину 300 (0x012C).


В Jsrsasign разработчик обычно не управляет длиной напрямую. Она вычисляется автоматически при сериализации:

var int = new KJUR.asn1.DERInteger({int: 12345});
console.log(int.getEncodedHex());

Библиотека сама определяет размер Value и формирует корректное поле Length.


Value (значение)

Value содержит фактические данные, интерпретируемые согласно Tag.

Это может быть:

  • числовое значение (INTEGER)
  • строка (UTF8String, PrintableString)
  • бинарные данные (OCTET STRING)
  • вложенная структура (SEQUENCE)
  • битовые маски (BIT STRING)

Пример структуры SEQUENCE в ASN.1:

SEQUENCE {
  INTEGER 10,
  OCTET STRING "abc"
}

В Jsrsasign:

var seq = new KJUR.asn1.DERSequence({
  array: [
    new KJUR.asn1.DERInteger({int: 10}),
    new KJUR.asn1.DEROctetString({hex: "616263"})
  ]
});

TLV в контексте Jsrsasign

Jsrsasign реализует ASN.1 через набор классов KJUR.asn1.*, где каждый объект соответствует TLV-узлу.

Базовая архитектура

Каждый ASN.1 элемент:

  • имеет тег (Tag)
  • автоматически вычисляет длину (Length)
  • сериализует данные (Value)

Пример: INTEGER TLV

var i = new KJUR.asn1.DERInteger({int: 255});
console.log(i.getEncodedHex());

Результат TLV:

  • Tag: 02
  • Length: 01
  • Value: FF

Итог:

02 01 FF

Вложенные TLV-структуры

SEQUENCE позволяет строить иерархические структуры TLV внутри TLV.

Пример:

var seq = new KJUR.asn1.DERSequence({
  array: [
    new KJUR.asn1.DERInteger({int: 1}),
    new KJUR.asn1.DERInteger({int: 2}),
    new KJUR.asn1.DERInteger({int: 3})
  ]
});

Схема:

30 <len>
   02 01 01
   02 01 02
   02 01 03

Здесь:

  • внешний 30 — SEQUENCE
  • внутри три TLV INTEGER

Кодирование TLV вручную через Jsrsasign

Jsrsasign позволяет собирать TLV-структуры без использования высокоуровневых обёрток через ASN1Object:

var asn1 = new KJUR.asn1.ASN1Object();

asn1.hTLV = "020101"; // INTEGER 1
console.log(asn1.getEncodedHex());

Здесь hTLV уже содержит готовую TLV-структуру.


Разбор TLV через ASN.1 парсеры Jsrsasign

Для декодирования используется ASN1HEX:

var hex = "020101";

var tag = KJUR.asn1.ASN1HEX.getVbyList(hex, 0, ["02"]);
console.log(tag);

Методы:

  • getTLV() — возвращает TLV блок
  • getV() — возвращает Value
  • getVbyList() — извлекает значение по пути ASN.1 дерева

Пример: разбор SEQUENCE TLV

30 06
   02 01 01
   02 01 02

Разбор:

var hex = "3006020101020102";

var int1 = KJUR.asn1.ASN1HEX.getVbyList(hex, 0, [0,0]);
var int2 = KJUR.asn1.ASN1HEX.getVbyList(hex, 0, [0,1]);

console.log(int1); // 01
console.log(int2); // 02

TLV в криптографии: сертификаты X.509

X.509 полностью построен на TLV-структурах:

  • SEQUENCE сертификата
  • SEQUENCE алгоритма
  • BIT STRING подписи
  • вложенные SEQUENCE полей субъекта и издателя

Jsrsasign предоставляет:

var x = new X509();
x.readCertHex(certHex);

Внутри происходит разбор TLV дерева.


BIT STRING и особенности TLV

BIT STRING имеет дополнительный байт в Value:

  • первый байт Value — количество неиспользованных бит

Пример:

03 03 00 A0 B1
  • 03 — BIT STRING
  • 03 — длина
  • 00 — padding bits

OCTET STRING и бинарные данные

Обычно используется для хранения:

  • ключевых параметров
  • хешей
  • подписей

Jsrsasign:

new KJUR.asn1.DEROctetString({hex: "deadbeef"});

TLV:

04 04 DE AD BE EF

Практическое значение TLV в Jsrsasign

TLV обеспечивает:

  • строгую типизацию данных
  • независимость платформ
  • возможность вложенных структур
  • корректное кодирование криптографических объектов

Jsrsasign абстрагирует ручную работу с TLV, но вся библиотека фактически является генератором и парсером TLV-последовательностей.


Ошибки при работе с TLV

Типичные проблемы:

  • неверная длина (Length mismatch)
  • некорректный тег
  • нарушение DER-правил (например, лишние нули в INTEGER)
  • неправильное вложение SEQUENCE

Jsrsasign обычно предотвращает такие ошибки, но при ручной работе через ASN1Object они возможны.


Итоговая структура TLV в Jsrsasign

Любой ASN.1 объект в библиотеке можно разложить на:

Tag     → тип данных
Length  → размер Value
Value   → данные или вложенные TLV

И вся криптографическая инфраструктура Jsrsasign работает как система генерации и интерпретации таких троек, формируя из них сложные структуры сертификатов, ключей и подписей.