Подписи
Необязательный массив `sigs` на уровне записи — отсоединённая COSE_Sign1 поверх всего тела записи, её подписываемая нагрузка с доменным разделением, два способа передать ключ подписанта и строгая проверка Ed25519.
Запись Label 309 МОЖЕТ нести одну или несколько подписей авторства в
необязательном массиве верхнего уровня sigs. Каждый элемент — это отсоединённая
COSE_Sign1 (RFC 9052) поверх тела
записи, удостоверяющая, что некоторый ключ ручается за эту запись. Авторство
всегда необязательно: стандарт никогда не требует подписи, и запись без поля
sigs — это полноценное, полностью проверяемое подтверждение существования.
Подпись лишь добавляется поверх остального, ничего не заменяя: к утверждению о времени она добавляет «и за это ручается такой-то ключ», но не вытесняет его. Основное утверждение — хеш содержимого; подпись же сообщает, кто за этим утверждением стоит. Важно, что подпись, которую верификатор проверить не может (из-за неподдерживаемого алгоритма или неразрешимого ключа), никогда не отменяет утверждение о содержимом и времени. Подписи отказывают мягко; существование — нет.
На этой странице определено, что покрывает подпись, какие именно байты
подписываются, какими двумя способами передаётся открытый ключ подписанта и какую
строгую проверку выполняет публичный верификатор. Сам ключ Ed25519 определён на
странице Ключи; поле sigs в его проводном виде — где cose_sign1 и
cose_key каждый являются единственной байтовой строкой CBOR — определено на странице
Запись.
Что покрывает подпись
Один элемент sigs[i] удостоверяет всё тело записи целиком, единообразно.
Гранулярности подписи на уровне отдельного элемента, отдельного URI или отдельного
поля не существует: одна подпись фиксирует каждый элемент, каждый URI хранилища,
каждый конверт шифрования, указатель supersedes, если он есть, и каждый ключ
расширения, который несёт запись. Ретранслятор не сможет постфактум что-либо
добавить, удалить или переписать в этих данных, не сломав подпись.
Подписываемое тело — это карта записи с удалённым полем sigs, то есть
remove_keys(record_map, ["sigs"]), обозначаемая здесь как record_body. Массив
sigs исключается из того, что подписывает каждый элемент, потому что подпись не
может покрывать саму себя и потому что каждый подписант фиксирует только само
утверждение, а не состав соподписантов. Конкретно, каждый элемент подписывает
{v, items?, merkle?, supersedes?, crit?, <extensions?>} — одни и те же байты
record_body для всех элементов, — но ни один элемент не подписывает остальные
элементы в sigs. Тем самым подписант удостоверяет, что подписанное им тело — то
же самое тело, с которым связан каждый другой элемент; однако ни один подписант не
удостоверяет, кто именно соподписал запись.
Подпись охватывает тело записи, а не транзакцию
Проверенная подпись доказывает, что некоторый ключ выработал подпись поверх тела записи. Она не доказывает, что тот же ключ отправил несущую транзакцию, оплатил её комиссию или выбрал её время блока. Идентичное тело записи МОЖЕТ быть повторно опубликовано любой стороной в более поздней транзакции — это намеренная переносимость записи. Отображайте проверенную подпись как «подписано ключом <key>», но никогда как «<key> отправил это» или «опубликовано ключом <key> в <time>».
Подписываемая нагрузка
Каждый элемент несёт отсоединённую COSE_Sign1, поэтому поле полезной нагрузки COSE пусто, а байты, которые в действительности подписываются, верификатор восстанавливает из ончейн-записи. Подписант вычисляет:
record_body = remove_keys(record_map, ["sigs"])
record_body_bytes = canonical_cbor(record_body)
SIG_DOMAIN_RECORD = utf8("cardano-poe-record-sig-v1") ; 25 bytes
to_sign = SIG_DOMAIN_RECORD || record_body_bytes ; concatenation
Sig_structure = [ "Signature1", protected, h'', to_sign ]
signature = Sign(canonical_cbor(Sig_structure), signer_key)record_body сериализуется как канонический CBOR согласно
RFC 8949 §4.2.1 — той же
детерминированной кодировкой, которую использует вся запись. Именно детерминизм
делает подпись совместимой: две реализации, кодирующие одно и то же логическое
тело, дают байт-в-байт идентичные record_body_bytes, поэтому подпись, созданная
одной из них, проверяется в другой.
Префикс доменного разделения
to_sign — это 25-байтовая строка UTF-8 cardano-poe-record-sig-v1,
дописанная в начало record_body_bytes. Префикс привязывает подпись к её роли в
Label 309 и предотвращает межпротокольный повтор. Будущая схема метаданных
Cardano, у которой случайно совпала бы форма CBOR с телом записи (те же ключи, те
же типы), не смогла бы повторно использовать против неё подпись Label 309: её
to_sign нёс бы другой префикс или вовсе никакого, так что последовательность
подписанных байтов отличалась бы и подпись не прошла бы. Реализации ДОЛЖНЫ
размещать эту буквальную последовательность байтов точно в начале to_sign;
подписывать лишь голый канонический CBOR без префикса — несоответствие стандарту.
Почему external_aad пуст
Label 309 помещает доменный разделитель внутрь to_sign, а не в COSE
external_aad. Слот external_aad (Sig_structure[2]) всегда равен пустой
байтовой строке h''. Это намеренное отступление от привычного для COSE приёма,
когда доменную строку кладут в external_aad, и причина — совместимость с
кошельками:
CIP-30
signData — стандартный путь подписи кошельком в Cardano — предписывает не
использовать external_aad и не даёт dApp никакой возможности его задать.
Непустой external_aad сделал бы недействительной любую подпись, выработанную
кошельком. Размещение префикса в полезной нагрузке сохраняет то же свойство защиты
от повтора и при этом оставляет байты, выработанные кошельком, и байты,
восстановленные верификатором, тождественными вплоть до байта.
Структура Sig_structure
Sig_structure — это четырёхэлементный подписной массив COSE_Sign1 из
RFC 9052 §4.4:
| Слот | Значение | Примечания |
|---|---|---|
[0] | "Signature1" | Фиксированный контекстный идентификатор COSE, выдаётся как полная текстовая строка CBOR (11 байт), но не как голый UTF-8. |
[1] | protected | Байты защищённого заголовка подписанта в обёртке bstr и каноническом CBOR, используются дословно — верификатор их не переканонизирует. |
[2] | external_aad | Всегда h'' (bstr нулевой длины). |
[3] | to_sign | 25-байтовый префикс, сцепленный с record_body_bytes. |
Опубликованная COSE_Sign1 несёт своё поле полезной нагрузки (COSE_Sign1[2]) как
CBOR null (0xF6) — это отсоединённая форма. Присоединённая полезная
нагрузка, в том числе байтовая строка нулевой длины, отвергается. Именно
отсоединение полезной нагрузки привязывает подписанные байты к телу записи,
которое верификатор восстанавливает независимо; присоединённая форма позволила бы
создателю подписать заимствованные байты, никак не связанные с ончейн-утверждениями.
Хешированный режим аппаратного кошелька
CIP-30 / CIP-8
определяют необязательный флаг незащищённого заголовка "hashed": true, который может выставить
ограниченный в ресурсах аппаратный соподписант. Когда он присутствует и равен true,
Sig_structure[3] — это 28-байтовый дайджест Blake2b-224(to_sign), а не сам to_sign;
остальные три слота не меняются. Верификатор ДОЛЖЕН проверить незащищённый заголовок и
выполнить эту подстановку перед строгой проверкой Ed25519. Программным создателям и SDK НЕ
РЕКОМЕНДУЕТСЯ выставлять этот флаг — он не экономит ни байта в проводном виде и усложняет
кодовые пути верификатора.
Алгоритм подписи
Единственный алгоритм подписи в v1 — это EdDSA поверх Ed25519
(RFC 8032), обозначаемый COSE
alg = -8 (RFC 9053 §2.2),
который находится в защищённом заголовке COSE_Sign1. Обязательный минимум для
верификатора v1 — это {-8}; он МОЖЕТ дополнительно принимать -19 (Ed25519,
полностью специфицированный) и проверять оба кодовых значения одним и тем же
примитивом Ed25519. Реестр расширяем — будущие редакции добавляют постквантовые
подписи аддитивно, никогда не ломая совместимость.
Разрешение ключа подписанта
Публичный верификатор должен разрешить открытый ключ подписанта без обращения к какому-либо сервису, поэтому каждая подпись несёт свой ключ либо однозначную ссылку на него внутри самой подписи, ончейн. В v1 есть ровно две формы передачи, и они взаимно исключают друг друга в пределах одного элемента: элемент, использующий обе сразу, — это структурная ошибка.
Путь 1 — подпись по идентичности (kid внутри подписи)
32-байтовый сырой открытый ключ Ed25519 помещается в COSE-заголовок с меткой 4
(kid, RFC 9052 §3.1)
внутри защищённого заголовка COSE_Sign1. Элемент не несёт поля cose_key. По
соглашению Label 309 kid в защищённом заголовке длиной ровно 32 байта и есть
открытый ключ, а не непрозрачный указатель на него, который пришлось бы разыскивать
где-то на стороне. Длина в 32 байта служит однозначным признаком: открытые ключи
Ed25519 всегда занимают 32 байта. Размещение ключа в защищённом, а не в
незащищённом заголовке привязывает его к подписи; злоумышленник, переписавший его,
сломал бы проверку.
Это соглашение — намеренное, документированное отклонение от трактовки kid как
непрозрачного идентификатора в RFC 9052; именно оно делает путь по идентичности
независимым от сервисов, без какого-либо каталога ключей. Модель ключей определена
на странице Ключи.
Путь 2 — подпись кошельком (встроенный cose_key)
Подпись CIP-30 signData возвращает открытый ключ подписанта как отдельный блок
cbor<COSE_Key>, а не внутри COSE_Sign1. Создатель, встраивающий такую подпись в
запись, ДОЛЖЕН поместить этот COSE_Key в тот же элемент sigs[i] под ключом
cose_key — как единственную байтовую строку CBOR. Верификатор декодирует его как
COSE_Key и читает открытый ключ Ed25519 из метки -2. COSE_Key ДОЛЖЕН описывать только открытую
половину — kty = OKP (1), crv = Ed25519 (6), 32-байтовый x под меткой -2 —
и НЕ ДОЛЖЕН нести материал закрытого ключа (метку -4 и тому подобное);
публикация закрытого скаляра в постоянном реестре — это необратимая утечка ключа.
Взаимное исключение
Эти два пути исключают друг друга на уровне проводного формата. Элемент несёт
либо 32-байтовый kid в защищённом заголовке и никакого cose_key
(путь 1), либо поле cose_key и никакого 32-байтового kid в защищённом
заголовке (путь 2) — но никогда оба сразу. Элемент, несущий оба, отвергается;
верификатору никогда не приходится устранять неоднозначность во время проверки.
Поэтому разрешение — это различение на уровне проводного формата, а не порядок
приоритетов:
| Путь | Условие | Ключ подписанта |
|---|---|---|
| 1 | 32-байтовый защищённый kid, нет cose_key | Значение 32-байтового kid, используется напрямую. |
| 2 | присутствует cose_key, нет 32-байтового kid | Ключ Ed25519 под меткой -2 COSE_Key. |
kid, переданный только в незащищённом заголовке, не является
санкционированным путём разрешения: он лежит вне подписанного конверта, поэтому
ретранслятор мог бы переписать его, не сломав подпись. При разрешении ключа
верификатор ДОЛЖЕН игнорировать значения kid из незащищённого заголовка.
Если ни один из допустимых путей не даёт 32-байтового ключа Ed25519, элемент
отмечается как неразрешённый и не привносит никакого утверждения об авторстве.
Проверка
Публичный верификатор проверяет каждый sigs[i] независимо, в таком порядке:
- Декодирование. Разберите байтовую строку
sigs[i].cose_sign1как COSE_Sign1. Поле полезной нагрузки ДОЛЖНО бытьnull(отсоединённым); любая ненулевая или непустая полезная нагрузка некорректна. - Алгоритм. Прочитайте
algиз защищённого заголовка. Если он вне набора, поддерживаемого верификатором, элемент не поддерживается (см. ниже), а не является ошибкой записи. - Разрешение ключа. Примените описанное выше различение пути 1 и пути 2, чтобы получить 32-байтовый открытый ключ Ed25519. Если ни один путь его не даёт, элемент считается неразрешённым.
- Восстановление и проверка. Восстановите
to_signиSig_structure = ["Signature1", protected, h'', to_sign], закодируйте в канонический CBOR и проверьте подпись строгим Ed25519. (Сначала подставьтеBlake2b-224(to_sign)вместоto_sign, если незащищённый заголовок несёт"hashed": true.) - Привязка к кошельку (только путь 2). Пересчитайте стейк-адрес из
разрешённого ключа и сравните его байт-в-байт с
addressиз защищённого заголовка; несовпадение проваливает привязку, даже если сама подпись Ed25519 прошла. Именно эта проверка, специфичная для пути 2, позволяет интерфейсу отобразить запись как привязанную к кошельку; для элементов пути 1 она пропускается.
Строгий Ed25519
Проверка следует строгим правилам RFC 8032 §5.1.7: для любой заданной тройки из ключа, сообщения и подписи существует ровно один допустимый ответ.
- Неканонические кодировки
Rили скаляра подписиS(в частности, любоеS ≥ ℓ, порядок группы) ДОЛЖНЫ отвергаться. - Открытые ключи и значения
Rмалого порядка, из малой подгруппы или с торсионной компонентой ДОЛЖНЫ отвергаться. - Уравнение проверки с кофактором (форма в духе ZIP-215, удобная для пакетной проверки) НЕ ДОЛЖНО подставляться вместо строгого уравнения.
Именно строгость делает вердикт воспроизводимым в разных реализациях: верификатор с кофактором принял бы подписи, которые строгий отвергает, так что два соответствующих стандарту верификатора разошлись бы во мнениях. Реализации обязаны выбрать библиотеку — или режим библиотеки, — выполняющую строгую проверку без кофактора.
Семантика вердикта
Подписи лишь добавляются поверх остального, поэтому непроверяемая подпись
отмечается на самом элементе, а не превращается в провал на уровне записи. Каждый
sigs[i] разрешается в один из этих типизированных исходов для элемента; полный
каталог ошибок и правила вердикта на уровне записи описаны на странице
Проверка:
| Исход | Значение |
|---|---|
| проверена | Строгий Ed25519 (а для пути 2 — и привязка адреса) прошёл. |
| подпись не поддерживается | alg из защищённого заголовка вне набора верификатора. Информация, никогда не ошибка. |
| ключ подписанта не разрешён | Ни один из допустимых путей не даёт 32-байтового открытого ключа Ed25519. |
| подпись недействительна | Строгий Ed25519 вернул false поверх восстановленного Sig_structure. |
| несовпадение адреса кошелька | Путь 2: подпись прошла, но пересчитанный стейк-адрес ≠ заявленному. |
Неподдерживаемая подпись никогда не отменяет подтверждение
Нераспознанный или неподдерживаемый алгоритм подписи даёт типизированный исход
signature-unsupported с уровнем серьёзности «информация». Утверждение о содержимом и времени —
ончейн-обязательство hashes — структурно корректно вне зависимости от того, какие алгоритмы
подписи реализует верификатор. Запись, несущая только подписи на алгоритмах будущего, по-прежнему
предстаёт как действительное подтверждение существования, и каждый такой элемент помечается как
неподдерживаемый. Подписи добавляются поверх остального; существование от них не зависит.
Смежные страницы
- Ключи — подписной ключ Ed25519, его выведение и 32-байтовый
открытый ключ, передаваемый в
kidпо пути 1. - Запись — поле
sigsверхнего уровня, закрытая картаsig-entry(cose_sign1/cose_keyкаждый — единственная байтовая строка) и передача тела целиком. - Проверка — коды исходов для элементов, правила вердикта на уровне записи и полный конвейер валидации.
Ключи
Модель ключей Label 309 — один 32-байтовый сид, три пары ключей разных алгоритмов, выведенные из него с помощью домен-разделённого HKDF-SHA-256, ключи шифрования ключей по слотам, которые запечатанное PoE выводит поверх них, и то, как кодируются открытые ключи получателей и их секреты.
Sealed PoE
Конверт шифрования Label 309 — как отправитель запечатывает содержимое для одного или нескольких ключей получателей, тогда как в блокчейн попадают только хеш открытого текста и обёрнутые слоты ключей, но никогда — сам открытый текст и никогда — идентичности получателей.