Это ознакомительный перевод. Нормативной является английская версия — при расхождениях верна она. Открыть английскую версию

Ключи

Модель ключей Label 309 — один 32-байтовый сид, три пары ключей разных алгоритмов, выведенные из него с помощью домен-разделённого HKDF-SHA-256, ключи шифрования ключей по слотам, которые запечатанное PoE выводит поверх них, и то, как кодируются открытые ключи получателей и их секреты.

Label 309 требует три вида асимметричных ключей: ключ Ed25519, который подписывает записи, ключ X25519, который принимает классические запечатанные данные, и гибридный ключ X-Wing (mlkem768x25519), который принимает запечатанные данные с постквантовой защитой. Стандарт не рассматривает их как три независимых секрета, которые нужно хранить и пересылать по отдельности. Он определяет ровно один секрет — 32-байтовый сид — и детерминированное правило, разворачивающее его во все три пары ключей.

На этой странице описано само выведение: сид, три домен-разделённых разворачивания HKDF, дающих закрытый ключ для каждого алгоритма, причины, по которым домены держат раздельно, ключи шифрования ключей по слотам, которые запечатанное PoE выводит поверх них, и то, как кодируются для передачи получаемые открытые ключи получателей и их секреты. Всё, что реализация делает с сидом помимо этого — где он хранится, как разблокируется, держит ли один человек сразу несколько, — находится за рамками стандарта. Label 309 заботит лишь одно: при одних и тех же 32 байтах любая совместимая реализация выводит одни и те же ключи.

Сид

Набор ключей Label 309 берёт начало из единственного значения:

СвойствоЗначение
Длина32 байта (256 бит)
ИсточникКриптостойкий генератор случайных чисел или любое 32-байтовое значение, которым владеет пользователь
РольВходной ключевой материал для трёх разворачиваний HKDF ниже

Сид — это чистый источник энтропии, а не ключ в смысле какого-либо одного алгоритма. У него нет привязки к кривой, нет длины, продиктованной примитивом, нет обрядов кодирования. Ключи, которые реализация в итоге использует, согласуются на каждом выведении; сид переживает любые алгоритмические выборы, сделанные поверх него. Производитель МОЖЕТ сгенерировать сид заново через системный CSPRNG или импортировать существующее 32-байтовое значение; в любом случае оно ДОЛЖНО декодироваться ровно в 32 байта. На уровне выведения не отбраковывается ни один низкоэнтропийный шаблон — сид из одних нулей является допустимым входом, и именно это делает нулевой сид пригодным в качестве воспроизводимой фикстуры для проверки совместимости.

Сид — это и есть вся идентичность целиком

Каждый факт об открытом ключе стороны, который выражает Label 309, — ключ, ручающийся за запись, ключи, принимающие запечатанные данные, — это детерминированная функция от этих 32 байт. Воспроизведите сид, и вы воспроизведёте все три пары ключей, байт в байт.

Кодирование сида для резервной копии

Поскольку 32-байтовый сид и есть идентичность, именно его пользователь сохраняет в резерв, экспортирует и импортирует, — а голый 32-байтовый блоб легко молча усечь или повредить. Label 309 определяет для него строковую кодировку с контрольной суммой, принимаемую наравне с сырым шестнадцатеричным представлением везде, где сид берётся на вход.

Строковая форма — это Bech32 (BIP-173, классический, со снятым ограничением длины в 90 символов) под человекочитаемым префиксом l309-seed- — завершающий дефис является частью HRP, так что разделитель Bech32 даёт видимый префикс l309-seed-1…. Кодирование возвращает форму отображения в ВЕРХНЕМ регистре L309-SEED-1…: секреты должны бросаться в глаза, а отображение в верхнем регистре визуально отличается от строк получателей age1… в нижнем регистре. Форма целиком в нижнем регистре — это равноправная кодировка тех же байтов.

seed (32 bytes)  0000…0000  ->  L309-SEED-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQLFUN82

Парсер принимает два представления и ветвится по форме:

  • Строку Bech32 в одном регистре (смешанный регистр отклоняется согласно BIP-173), с проверенной контрольной суммой и декодированной полезной нагрузкой ровно в 32 байта.
  • Сырое шестнадцатеричное представление — 64 шестнадцатеричные цифры, без учёта регистра, с допуском префикса 0x и окружающих или внутренних пробелов.

Каждый отклонённый ввод отображается на отдельный код ошибки API конструирования, так что вызывающая сторона может отличить опечатку от неверного типа ключа:

ВводКод ошибки
Строка Bech32, чья контрольная сумма не сошлась (переворот символа, усечение)SEED_STRING_BAD_CHECKSUM
Строка Bech32, смешивающая верхний и нижний регистрSEED_STRING_MIXED_CASE
Действительная строка Bech32 под другим HRP (например, получатель age1…)SEED_STRING_WRONG_HRP
Строка Bech32 или шестнадцатеричная строка, декодирующаяся в ≠ 32 байтаSEED_STRING_WRONG_LENGTH
Всё, что не является ни распознанной строкой Bech32, ни шестнадцатеричной (в т. ч. пустое)SEED_STRING_UNRECOGNIZED

Эти коды описывают кодек строки сида — удобство обращения с ключами вокруг выведения; они отличны от реестра кодов ошибок на проводе, которые выдаёт структурный валидатор (Проверка). Кодировка несёт голые 32 байта и ничего более — ни версии, ни параметров выведения, — поскольку смысл сида фиксируется тремя строками info ниже, а не тем, как он был передан.

Выведение трёх пар ключей

Закрытый ключ каждого алгоритма — это независимое разворачивание HKDF-SHA-256 одного и того же сида согласно RFC 5869. Три разворачивания используют общий входной ключевой материал и общую (отсутствующую) соль и различаются лишь одним параметром — строкой info, которая называет алгоритм:

АлгоритмСтрока infoРезультат
Ed25519cardano-poe-ed25519-v132-байтовый секретный сид Ed25519
X25519cardano-poe-x25519-v132-байтовый секретный сид X25519
mlkem768x25519cardano-poe-mlkem768x25519-v132-байтовый сид ключа декапсуляции X-Wing

Выведение в псевдокоде:

ed25519_priv        = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-ed25519-v1",        length = 32)
x25519_priv         = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-x25519-v1",         length = 32)
mlkem768x25519_priv = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-mlkem768x25519-v1", length = 32)

Три правила обеспечивают совместимость этих результатов между реализациями:

  1. Соль пуста. Соль HKDF ДОЛЖНА быть байтовой строкой нулевой длины. Согласно RFC 5869 §2.2 отсутствующая соль трактуется как HashLen нулевых байт — 32 нулевых байта для SHA-256, — поэтому каждая совместимая библиотека приходит к одному и тому же шагу извлечения.
  2. Результат равен 32 байтам. Каждое разворачивание запрашивает ровно 32 байта (один блок HKDF для SHA-256).
  3. Строки info — это точный ASCII. Каждое значение info ДОЛЖНО быть закодировано ровно теми байтами, что показаны выше: без окружающих пробелов, без нулевого терминатора, без метки порядка байт, без завершающего перевода строки. Длины трёх строк составляют 22, 21 и 29 байт соответственно.

Эти 32 выходных байта — секретный сид алгоритма, а не его развёрнутый скаляр на кривой. RFC 8032 §5.1.5 проводит это различие для Ed25519: секретный сид занимает 32 байта, а библиотека подписи разворачивает его внутри (через SHA-512 с последующим клампингом) в собственно скаляр и префикс подписи. То же верно и для X25519, где клампинг применяется внутри примитива согласно RFC 7748 §5. Реализация ДОЛЖНА передавать примитиву необработанный 32-байтовый вывод HKDF и предоставлять библиотеке выполнять разворачивание и клампинг — она не делает предварительного клампинга и не разворачивает ключ заранее. Для X-Wing 32-байтовый вывод — это сид ключа декапсуляции X-Wing, из которого вся пара ключей, включая 1216-байтовый открытый ключ, детерминированно восстанавливается процедурой генерации ключей X-Wing. В любом случае канонической формой для хранения и передачи служит компактный 32-байтовый сид, а не развёрнутый ключ.

Почему три домена, а не один

Параметр info в HKDF — это его тег домен-разделения: он привязывает развёрнутый вывод к конкретному прикладному контексту, и RFC 5869 §3.1 настоятельно рекомендует задавать такой тег, когда контекст известен. Label 309 задаёт отдельный тег на каждый алгоритм, а не переиспользует одно разворачивание для всех трёх, хотя все три закрытых ключа и оказываются шириной в 32 байта. Причина — изоляция:

  • Сбои остаются локализованными. Если бы две пары ключей делили одни и те же байты, слабость, свойственная одному алгоритму, — изъян в выведении nonce, побочный канал при умножении на скаляр, — могла бы раскрыть ключ совершенно другого алгоритма. Домен-разделение гарантирует, что три закрытых ключа являются независимыми функциями сида, поэтому компрометация одного из них не сообщает атакующему ничего об остальных.
  • Миграция остаётся аддитивной. Каждая строка info оканчивается на -v1. Переход на другую кривую или другой гибрид в будущей редакции выводит из того же сида свежий -v2-ключ под новым тегом, не сталкиваясь с уже развёрнутыми v1-ключами. Это повторяет ту самую гибкость выбора алгоритмов, на которую опирается и сам проводной формат.

Третий тег, cardano-poe-mlkem768x25519-v1, даёт постквантовому гибриду собственный домен, хотя сид его ключа декапсуляции той же 32-байтовой ширины, что и классический секрет X25519. Поэтому изъян в ML-KEM-768, в X25519 или в комбайнере X-Wing не может перекинуться на классический ключ шифрования или ключ подписи.

Этот тег ключа идентичности, cardano-poe-mlkem768x25519-v1, вдобавок не содержит сегмента -kek-: он отличается от метки выведения KEK по записи cardano-poe-kek-mlkem768x25519-v1 ниже, так что разворачивание сид → ключ идентичности и обёртывание ключа слота в запечатанном PoE никогда не делят строку info.

Ключи шифрования ключей по слотам

Три выведенные из сида пары ключей выше — это долгоживущие ключи идентичности. Запечатанное PoE добавляет второй, по записи слой HKDF-SHA-256: для каждого слота получателя отправитель выводит свежий 32-байтовый ключ шифрования ключей (KEK), которым обёртывается ключ шифрования содержимого записи. Выведение KEK — часть модели ключей, поэтому оно описано здесь; то, как обёрнутый ключ затем едет в конверте, изложено на странице Запечатанное PoE.

Оба KEM выводят KEK через HKDF-SHA-256 со своей, специфичной для KEM, строкой info, под помеченной хешем солью, привязывающей три значения: собственный KEM-материал слота (так что KEK уникален для слота), открытый ключ получателя pub_R (так что инкапсуляцию, изготовленную для одного получателя, нельзя ретранслировать против другого) и уникальный для конверта enc.nonce (так что KEK заякорен к одному конверту). Общий секрет — это собственный вывод KEM: ECDH (классический) или декапсуляция X-Wing (гибридный) — одно и то же 32-байтовое значение, инкапсулирует ли отправитель или декапсулирует получатель, — а salt и info одинаковы с обеих сторон:

; x25519 (classical) — salt is a labelled SHA-256 over the ephemeral and recipient keys
kek_salt = SHA-256("cardano-poe-x25519-kek-salt-v1" || enc.nonce || pub_epk || pub_R)  ; 32 bytes
KEK      = HKDF-SHA-256(ikm  = shared,                  ; the X25519 ECDH shared secret
                        salt = kek_salt,
                        info = "cardano-poe-kek-v1",
                        L    = 32)

; mlkem768x25519 (hybrid) — same labelled-salt shape under the hybrid's own label
kek_salt = SHA-256("cardano-poe-xwing-kek-salt-v1" || enc.nonce || kem_ct || pub_R)    ; 32 bytes
KEK      = HKDF-SHA-256(ikm  = shared,                  ; the X-Wing shared secret
                        salt = kek_salt,
                        info = "cardano-poe-kek-mlkem768x25519-v1",
                        L    = 32)

Две соли имеют одинаковую форму — SHA-256(label || enc.nonce || <KEM-материал слота> || pub_R) — и различаются лишь меткой для своего KEM и тем, какой KEM-материал несут: 32-байтовый эфемерный pub_epk на классическом пути, 1120-байтовый шифртекст X-Wing kem_ct на гибридном пути. Обе свёртываются через дайджест SHA-256 фиксированной длины, поскольку гибридные входы слишком велики для сырой соли, а одна единообразная форма держит оба пути согласованными. Привязка вычисляется вне KEM, над собственными проводными байтами слота, поэтому она держит X-Wing как KEM-чёрный ящик и не опирается ни на какое свойство внутреннего хеширования комбайнера. Различная по KEM метка info дополнительно гарантирует, что KEK, выведенный под одним KEM, никогда не совпадёт с KEK, выведенным под другим, при одном и том же 32-байтовом общем секрете.

В обеих солях pub_R — это каноническая проводная кодировка ключа получателя: ровно 32-байтовый открытый ключ X25519 для x25519, ровно закреплённая 1216-байтовая байтовая строка открытого ключа X-Wing для mlkem768x25519. Отправитель и получатель ДОЛЖНЫ использовать именно эту кодировку и НЕ ДОЛЖНЫ подставлять никакой неканонический или перекодированный эквивалент: иначе обе стороны подадут в HKDF разные соли и выведут разные KEK, и слот никогда не откроется.

Каждый KEK и его префикс соли — это внутренние строительные блоки enc.scheme: 1: они не несут проводного идентификатора и не выбираются. Две метки префикса соли и две метки info здесь — это четыре из одиннадцати литералов-меток запечатанной конструкции, каталогизированных на странице Реестры алгоритмов; верификатор ДОЛЖЕН использовать каждую байт в байт.

Кодировки открытых ключей получателей

Отправителю запечатанного PoE нужен открытый ключ получателя в переносимой строковой форме, а получатель сохраняет в резерв свой секрет в соответствующей форме. Label 309 переиспользует Bech32-кодировки получателей из экосистемы age — по одному человекочитаемому префиксу (HRP) на каждый зарегистрированный механизм инкапсуляции ключей.

В Bech32 символ 1 служит разделителем между HRP и частью данных, поэтому человеко-видимый префикс строки — это её HRP плюс этот 1. Таким образом, HRP и видимый префикс различны, и таблица держит их в отдельных столбцах:

KEM (enc.kem)Открытый ключHRP открытого ключаВидимый префикс открытого ключаHRP секретаВидимый префикс секрета
x2551932-байтовый открытый ключ X25519ageage1… (62 символа)AGE-SECRET-KEY-AGE-SECRET-KEY-1…
mlkem768x255191216-байтовый открытый ключ X-Wingage1pqcage1pqc1… (1960 символов)AGE-SECRET-KEY-PQ-AGE-SECRET-KEY-PQ-1…

Классическая строка получателя x25519 имеет HRP age и стандартную для age v1 форму age1…. Гибридный открытый ключ конкатенирует ключ инкапсуляции ML-KEM-768 (1184 байта) с открытым ключом X25519 (32 байта); при длине 1216 байт его строка получателя age1pqc1… насчитывает 1960 символов.

Секрет, который реализация сохраняет в резерв и импортирует, — это на обоих путях 32-байтовый сид: секретный сид X25519 под AGE-SECRET-KEY- и сид ключа декапсуляции X-Wing (третий вывод HKDF выше, info = "cardano-poe-mlkem768x25519-v1") под AGE-SECRET-KEY-PQ-. 1216-байтовый гибридный открытый ключ выводится из этого сида; канонический секрет для хранения — компактный сид, а не развёрнутый ключ.

BIP-173 ограничивает строку Bech32 90 символами, но это ограничение существует ради платёжных адресов, которые набирают вручную, и здесь оно не действует. Реализация ДОЛЖНА кодировать и декодировать строку age1pqc1…, не применяя лимит в 90 символов, но по-прежнему применяя контрольную сумму и правила набора символов Bech32. Отдельный HRP age1pqc не даёт гибридному получателю столкнуться с каким-либо классическим получателем age — и это намеренно не age1pq, более короткий префикс, который вышестоящая нативная кодировка ML-KEM-768 + X25519 уже заняла за тем же примитивом, так что две кодировки получателей никогда не сталкиваются на проводе. Классическая кодировка остаётся в пределах обычных длин и обрабатывается без изменений.

Эти строки служат лишь удобством для обнаружения получателей. Открытый ключ получателя никогда не появляется в конверте шифрования записи Label 309: элемент enc.slots[] несёт ключевой материал отдельного слота и значение wrap, а идентификатор KEM указывается один раз в enc.kem. То, как строятся конверт и слоты, описано на странице Запечатанное PoE.

Открытый ключ Ed25519 как kid подписи

Ключ Ed25519 не играет роли получателя; это идентификатор ключа, по которому верификатор находит ключ, чтобы проверить подпись. Когда производитель подписывает запись, необработанный 32-байтовый открытый ключ Ed25519 выступает значением kid (метка 4) в защищённом заголовке COSE_Sign1 согласно RFC 9052. Верификатор считывает это 32-байтовое значение прямо из ончейн-подписи и проверяет тело записи относительно него — открытый ключ путешествует вместе с подписью, поэтому для проверки авторства не нужен никакой отдельный поиск. Полная конструкция подписи, подписываемая полезная нагрузка и правила проверки описаны на странице Подписи.

Обмен ключами вне полосы

Label 309 определяет, как открытые ключи получателей кодируются, но не то, как они обнаруживаются. Стандарт не предписывает ни каталога, ни реестра, ни ончейн-формата объявления ключей получателей. Сторона, желающая принимать запечатанные данные, публикует свою строку age1… или age1pqc1… по любому каналу, которому обе стороны уже доверяют, — передача из рук в руки, запись, подписанная собственным ключом Ed25519, запись по стабильному веб-адресу или контент-адресуемому расположению, — и отправитель отвечает за происхождение любого ключа, для которого он шифрует.

Это намеренная граница. То же свойство, которое позволяет проверить запись, не доверяя серверу, означает, что и обмен ключами не должен протащить обратно доверенного посредника. Имя, помещённое рядом с ключом, — это аттестация того, кто его поместил, но никогда не криптографическое заявление: две стороны под одним и тем же именем всё равно дадут ключи с разными байтами, а верификатор сравнивает именно байты. Сопоставление человекочитаемых имён ключам — то, что приложение, построенное на Label 309, МОЖЕТ предложить, но это возможность приложения, лежащая за пределами протокола.

Связанные страницы

  • Подписи — как ключ Ed25519 подписывает запись и как проверяется kid.
  • Запечатанное PoE — как открытые ключи X25519 и X-Wing адресуют зашифрованные данные конкретным получателям.
  • Реестры алгоритмов — именованные идентификаторы подписей, KEM, AEAD и KDF, упомянутые здесь.