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

Подписи

Необязательный массив `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_sign25-байтовый префикс, сцепленный с 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) — но никогда оба сразу. Элемент, несущий оба, отвергается; верификатору никогда не приходится устранять неоднозначность во время проверки. Поэтому разрешение — это различение на уровне проводного формата, а не порядок приоритетов:

ПутьУсловиеКлюч подписанта
132-байтовый защищённый kid, нет cose_keyЗначение 32-байтового kid, используется напрямую.
2присутствует cose_key, нет 32-байтового kidКлюч Ed25519 под меткой -2 COSE_Key.

kid, переданный только в незащищённом заголовке, не является санкционированным путём разрешения: он лежит вне подписанного конверта, поэтому ретранслятор мог бы переписать его, не сломав подпись. При разрешении ключа верификатор ДОЛЖЕН игнорировать значения kid из незащищённого заголовка. Если ни один из допустимых путей не даёт 32-байтового ключа Ed25519, элемент отмечается как неразрешённый и не привносит никакого утверждения об авторстве.

Проверка

Публичный верификатор проверяет каждый sigs[i] независимо, в таком порядке:

  1. Декодирование. Разберите байтовую строку sigs[i].cose_sign1 как COSE_Sign1. Поле полезной нагрузки ДОЛЖНО быть null (отсоединённым); любая ненулевая или непустая полезная нагрузка некорректна.
  2. Алгоритм. Прочитайте alg из защищённого заголовка. Если он вне набора, поддерживаемого верификатором, элемент не поддерживается (см. ниже), а не является ошибкой записи.
  3. Разрешение ключа. Примените описанное выше различение пути 1 и пути 2, чтобы получить 32-байтовый открытый ключ Ed25519. Если ни один путь его не даёт, элемент считается неразрешённым.
  4. Восстановление и проверка. Восстановите to_sign и Sig_structure = ["Signature1", protected, h'', to_sign], закодируйте в канонический CBOR и проверьте подпись строгим Ed25519. (Сначала подставьте Blake2b-224(to_sign) вместо to_sign, если незащищённый заголовок несёт "hashed": true.)
  5. Привязка к кошельку (только путь 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 каждый — единственная байтовая строка) и передача тела целиком.
  • Проверка — коды исходов для элементов, правила вердикта на уровне записи и полный конвейер валидации.