Ключи
Модель ключей 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 | Результат |
|---|---|---|
| Ed25519 | cardano-poe-ed25519-v1 | 32-байтовый секретный сид Ed25519 |
| X25519 | cardano-poe-x25519-v1 | 32-байтовый секретный сид X25519 |
mlkem768x25519 | cardano-poe-mlkem768x25519-v1 | 32-байтовый сид ключа декапсуляции 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)Три правила обеспечивают совместимость этих результатов между реализациями:
- Соль пуста. Соль HKDF ДОЛЖНА быть байтовой строкой нулевой длины.
Согласно RFC 5869 §2.2
отсутствующая соль трактуется как
HashLenнулевых байт — 32 нулевых байта для SHA-256, — поэтому каждая совместимая библиотека приходит к одному и тому же шагу извлечения. - Результат равен 32 байтам. Каждое разворачивание запрашивает ровно 32 байта (один блок HKDF для SHA-256).
- Строки
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 секрета | Видимый префикс секрета |
|---|---|---|---|---|---|
x25519 | 32-байтовый открытый ключ X25519 | age | age1… (62 символа) | AGE-SECRET-KEY- | AGE-SECRET-KEY-1… |
mlkem768x25519 | 1216-байтовый открытый ключ X-Wing | age1pqc | age1pqc1… (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, упомянутые здесь.
Реестры алгоритмов
Реестры именованных идентификаторов для хешей, AEAD, KEM, KDF и подписей — и правило гибкости выбора алгоритмов, благодаря которому миграция на постквантовые алгоритмы является дополнением, а не ломающим изменением.
Подписи
Необязательный массив `sigs` на уровне записи — отсоединённая COSE_Sign1 поверх всего тела записи, её подписываемая нагрузка с доменным разделением, два способа передать ключ подписанта и строгая проверка Ed25519.