署名
任意指定のレコードレベル `sigs` 配列。レコード本体全体を対象とする分離型 COSE_Sign1、ドメイン分離された署名対象ペイロード、2 通りの署名鍵の運搬方法、そして厳格な Ed25519 検証について解説します。
Label 309 レコードは、任意指定の最上位 sigs 配列に 1 つ以上の著作者署名を持つことが
できます(MAY)。各エントリは、レコード本体を対象とする分離型
COSE_Sign1(RFC 9052)であり、ある鍵がそのレコードの
正当性を裏付けていることを証明します。著作者性は**常に任意(optional)**です。この標準が署名を要求することは
決してなく、sigs フィールドを持たないレコードであっても、それ自体で完全に検証可能な存在証明
(Proof of Existence, PoE)として成立します。
署名は付加的なものです。タイムスタンプの主張に上乗せして「さらにこの鍵がそれを裏付けている」という事実を 答えるだけであり、タイムスタンプの主張の代わりになるものではありません。一次的な主張はコンテンツのハッシュ値であり、 署名はその主張をだれが支持しているかについてのメタデータにすぎません。重要なのは、検証者が確認できない署名、たとえば サポート外のアルゴリズムや解決できない鍵による署名であっても、コンテンツやタイムスタンプの主張を無効にすることは 決してないという点です。署名はやわらかに失敗します。存在の主張はそうではありません。
このページでは、署名が何を対象とするか、署名される正確なバイト列、署名者の公開鍵を運搬する 2 通りの方法、
そして公開検証者が行う厳格な検証について定義します。Ed25519 鍵そのものは 鍵 で定義します。
ワイヤ上の sigs フィールド、すなわち cose_sign1 と cose_key がそれぞれ単一の CBOR バイト文字列となる仕組みは レコード で定義します。
署名が対象とするもの
1 つの sigs[i] エントリは、レコード本体全体を一律に証明します。アイテムごと、URI ごと、フィールドごとといった
署名の粒度は存在しません。1 つの署名が、すべてのアイテム、すべてのストレージ URI、すべての暗号化エンベロープ、
存在する場合は supersedes ポインタ、そしてレコードが持つすべての拡張キーにコミットします。リレーは事後に
これらのいずれかを追加、削除、書き換えすることが、署名を壊さずにはできません。
署名対象の本体は、sigs フィールドを取り除いたレコードマップ、すなわち
remove_keys(record_map, ["sigs"]) であり、ここではこれを record_body と表記します。署名は自分自身を
対象にできないため、また各署名者は主張だけにコミットし、共同署名者の顔ぶれにはコミットしないため、各エントリの
署名対象から sigs 配列は除外されます。具体的には、すべてのエントリが {v, items?, merkle?, supersedes?, crit?, <extensions?>} に署名します。これは、すべてのエントリにとって同一の record_body バイト列です。
ただし、どのエントリも sigs 内の他のエントリには署名しません。したがって署名者は、自分が署名した本体が、他のすべての
エントリが束ねられている本体と同一であることを証明するのであって、どの 署名者が共同署名したかを証明するのでは
ありません。
署名のスコープはレコード本体であり、トランザクションではありません
検証済みの署名は、ある鍵がレコード本体を対象とする署名を生成したことを証明します。同じ鍵が、それを運搬する トランザクションを送信したこと、その手数料を支払ったこと、そのブロック時刻を選んだことを証明するものでは ありません。同一のレコード本体は、後続のトランザクションでだれによっても再発行されることができます(MAY)。 これは意図的なレコードの可搬性です。検証済みの署名は「<key> によって署名済み」と表示してください。決して 「<key> がこれを送信した」や「<time> に <key> が発行した」とは表示しないでください。
署名対象ペイロード
各エントリは分離型 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 は RFC 8949 §4.2.1 に従って
正準 CBOR としてシリアライズされます。これはレコード全体が用いるのと同じ決定論的エンコーディングです。署名が相互運用
できるのは、この決定論のおかげです。同じ論理的本体をエンコードする 2 つの実装はバイト単位で同一の
record_body_bytes を生成するため、一方が生成した署名は他方でも検証できます。
ドメイン分離プレフィックス
to_sign は、25 バイトの UTF-8 文字列 cardano-poe-record-sig-v1 を record_body_bytes の先頭に
付加したものです。このプレフィックスは署名をその Label 309 上の役割に束縛し、プロトコルをまたいだリプレイを防ぎます。
仮に本体と同じ CBOR 構造(同じキー、同じ型)をたまたま共有する将来の Cardano メタデータスキーマがあったとしても、
それに対して Label 309 の署名を再利用することはできません。そのスキーマの to_sign は異なるプレフィックスを持つか、
プレフィックスを持たないため、署名されるバイト列が異なり、署名は失敗します。実装は、このリテラルなバイト列を
to_sign の先頭バイトとしてそのとおりに埋め込まなければなりません(MUST)。プレフィックスを付けずに、むき出しの
正準 CBOR だけに署名するのは仕様非準拠です。
なぜ external_aad は空なのか
Label 309 はドメイン分離子を COSE の external_aad ではなく to_sign の内側に置きます。external_aad
スロット(Sig_structure[2])は常に空バイト列 h'' です。これはドメイン文字列を external_aad に置くという
通常の COSE のパターンからの意図的な逸脱であり、その理由はウォレットとの相互運用性にあります。Cardano における
標準的なウォレット署名経路である
CIP-30
の signData は、external_aad を一切用いないと規定しており、dApp がそれを供給する手段を提供しません。空でない
external_aad は、ウォレットが生成したすべての署名を失敗させてしまいます。プレフィックスをペイロードに埋め込むことで、
同一のリプレイ防止特性を保ちつつ、ウォレットが生成したバイト列と検証者が再計算したバイト列をバイト単位で等しく
保ちます。
Sig_structure
Sig_structure は、RFC 9052 §4.4 で定義される
4 要素の COSE_Sign1 署名用配列です。
| スロット | 値 | 注記 |
|---|---|---|
[0] | "Signature1" | 固定の COSE コンテキスト識別子。むき出しの UTF-8 ではなく、完全な CBOR テキスト文字列(11 バイト)として出力されます。 |
[1] | protected | 署名者の bstr でラップされた正準 CBOR の保護ヘッダーバイト列。そのまま用いられ、検証者が再正準化することはありません。 |
[2] | external_aad | 常に h''(長さ 0 の bstr)。 |
[3] | to_sign | 25 バイトのプレフィックスを record_body_bytes に連結したもの。 |
公開される COSE_Sign1 は、そのペイロードフィールド(COSE_Sign1[2])を CBOR の null(0xF6)として持ちます。
これが分離型の形式です。長さ 0 のバイト列を含め、付加されたペイロードは拒否されます。ペイロードを分離することこそが、
署名されたバイト列を、検証者が独立に再計算するレコード本体に固定する仕組みです。付加型の形式では、生成者が
オンチェーンの主張と何の関係もない借り物のバイト列に署名できてしまいます。
ハードウェアウォレットのハッシュモード
CIP-30 / CIP-8 は、
リソースの制約された共同署名ハードウェアが設定しうる、任意指定の非保護ヘッダーフラグ "hashed": true を 定義しています。これが存在し、かつ真である場合、Sig_structure[3] は to_sign
そのものではなく、28 バイトの Blake2b-224(to_sign) ダイジェストになります。他の 3
つのスロットは変わりません。検証者は厳格な Ed25519 検証の前に
非保護ヘッダーを検査し、この置き換えを行わなければなりません(MUST)。ソフトウェアおよび SDK
の生成者は、これを 設定すべきではありません(SHOULD
NOT)。ワイヤ上のバイト数を節約できず、検証者のコード経路を複雑にするだけだからです。
署名アルゴリズム
v1 における唯一の署名アルゴリズムは EdDSA over Ed25519
(RFC 8032)であり、COSE_Sign1 の保護ヘッダーに存在する COSE の
alg = -8(RFC 9053 §2.2)で識別されます。v1 検証者の
必須ベースラインは {-8} です。検証者は、これに加えて -19(Ed25519、完全指定)を受け入れ、両方のコードポイントを
同一の Ed25519 プリミティブの下で検証することもできます(MAY)。このレジストリは拡張可能です。将来の改訂では
ポスト量子署名が破壊的変更としてではなく、付加的に追加されます。
署名鍵の解決
公開検証者は、いかなるサービスにも接続することなく署名者の公開鍵を解決できなければなりません。そのため すべての署名は、その鍵そのものか、あるいは署名内に置かれた一意な鍵への参照のいずれかをオンチェーンで運搬します。 v1 には運搬形式がちょうど 2 通りあり、これらは単一のエントリ内で相互排他的です。両方を用いるエントリは構造エラーです。
経路 1 — アイデンティティ署名(署名内 kid)
32 バイトの生の Ed25519 公開鍵を、COSE_Sign1 の保護ヘッダー内の COSE ヘッダーラベル 4(kid、
RFC 9052 §3.1)に置きます。このエントリは cose_key
フィールドを持ちません。Label 309 の慣例により、ちょうど 32 バイトの保護ヘッダー kid は、それ自体が公開鍵であり、
帯域外で参照される鍵への不透明なポインタではありません。32 バイトという長さは一意な判別子です。Ed25519 公開鍵は
常に 32 バイトだからです。鍵を(非保護ヘッダーではなく)保護ヘッダーに置くことで、それを署名に束縛します。これを書き換えた
攻撃者は検証を壊すことになります。
この慣例は、RFC 9052 における kid を不透明な識別子として読む解釈からの、意図的で文書化された逸脱です。これこそが
アイデンティティ経路を、鍵ディレクトリを必要とせず、サービスに依存しないものにしています。鍵モデルは 鍵
で定義します。
経路 2 — ウォレット署名(インライン cose_key)
CIP-30 の signData 署名は、署名者の公開鍵を COSE_Sign1 の内側ではなく、独立した cbor<COSE_Key> ブロブとして
返します。そのような署名をレコードに連結する生成者は、その COSE_Key を単一の CBOR バイト文字列として、同じ sigs[i] エントリ内の
キー cose_key の下に置かなければなりません(MUST)。検証者はそれを COSE_Key としてデコードし、ラベル -2 から
Ed25519 公開鍵を読み取ります。COSE_Key は公開鍵側だけ、すなわち kty = OKP (1)、crv = Ed25519 (6)、
ラベル -2 の 32 バイトの x のみを記述しなければならず(MUST)、秘密鍵の素材(ラベル -4 など)を運搬しては
なりません(MUST NOT)。恒久的な台帳に秘密スカラーを公開することは、取り返しのつかない鍵の漏洩です。
相互排他
2 つの経路はワイヤレベルで排他的です。エントリは、32 バイトの保護ヘッダー kid を持ち、cose_key を持たない
(経路 1)か、あるいは cose_key フィールドを持ち、32 バイトの保護ヘッダー kid を持たない(経路 2)かの
いずれかであり、両方を持つことはありません。両方を持つエントリは拒否されます。検証者は検証時に曖昧さを解消する必要が
ありません。したがって解決は、優先順位付けされた優先度ではなく、ワイヤレベルの判別です。
| 経路 | 条件 | 署名鍵 |
|---|---|---|
| 1 | 32 バイトの保護ヘッダー kid、cose_key なし | 32 バイトの kid 値を直接用いる。 |
| 2 | cose_key あり、32 バイトの kid なし | COSE_Key ラベル -2 の Ed25519 鍵。 |
非保護ヘッダーにのみ運搬される kid は、認められた解決経路ではありません。それは署名されたエンベロープの外側に
あるため、リレーが署名を壊さずに書き換えられてしまいます。検証者は、解決のために非保護ヘッダーの kid 値を無視
しなければなりません(MUST)。いずれの許容された経路からも 32 バイトの Ed25519 鍵が得られない場合、そのエントリは
未解決として報告され、いかなる著作者性の主張にも寄与しません。
検証
公開検証者は各 sigs[i] を独立に、次の順序で確認します。
- デコード。
sigs[i].cose_sign1のバイト文字列を COSE_Sign1 としてパースします。ペイロードフィールドはnull(分離型)でなければなりません(MUST)。null でない、または空でないペイロードはいずれも不正な形式です。 - アルゴリズム。 保護ヘッダーの
algを読み取ります。それが検証者のサポート集合の外にある場合、そのエントリは サポート外(後述)であり、レコードに対するエラーではありません。 - 鍵の解決。 上記の経路 1 / 経路 2 の判別を適用して 32 バイトの Ed25519 公開鍵を得ます。いずれの経路からも鍵が 得られない場合、そのエントリは未解決です。
- 再構築と検証。
to_signとSig_structure = ["Signature1", protected, h'', to_sign]を再構築し、正準 CBOR でエンコードして、 厳格な Ed25519 で署名を検証します。(非保護ヘッダーが"hashed": trueを運搬する場合は、まずto_signをBlake2b-224(to_sign)に置き換えます。) - ウォレット束縛(経路 2 のみ)。 解決した鍵からステークアドレスを再計算し、保護ヘッダーの
addressとバイト単位で 比較します。不一致の場合、Ed25519 署名そのものは検証できていても束縛は失敗します。この経路 2 限定の確認こそが、 UI がレコードをウォレットに束縛されたものとして表示できる根拠です。経路 1 のエントリはこれをスキップします。
厳格な Ed25519
検証は RFC 8032 §5.1.7 の厳格な規則に従います。 任意の鍵、メッセージ、署名の組に対して、受理可能な答えはちょうど 1 つだけです。
Rまたは署名スカラーSの非正準なエンコーディング(とりわけ群位数ℓに対するS ≥ ℓ)は拒否 しなければなりません(MUST)。- 小位数・小部分群・ねじれ成分を持つ公開鍵および
R値は拒否しなければなりません(MUST)。 - 余因子付きの検証方程式(ZIP-215 / バッチ向けの形式)を厳格な方程式の代わりに用いてはなりません(MUST NOT)。
判定を実装間で再現可能にしているのは、この厳格さです。余因子付きの検証者は、厳格な検証者が拒否する署名を受理して しまうため、2 つの準拠検証者が食い違うことになります。実装は、厳格で余因子を用いない検証を行うライブラリ、または ライブラリのモードを選ばなければなりません。
判定のセマンティクス
署名は付加的なものであるため、検証できない署名はそのエントリ上で報告され、レコードレベルの失敗へと格上げされる
ことはありません。各 sigs[i] は、次に挙げる型付きのエントリ単位の結果のいずれかに解決されます。完全なエラーカタログと
レコードレベルの判定規則は 検証 に記載されています。
| 結果 | 意味 |
|---|---|
| 検証済み | 厳格な Ed25519(および経路 2 の場合はアドレス束縛)に合格した。 |
| 署名サポート外 | 保護ヘッダーの alg が検証者の集合の外にある。情報であり、決してエラーではない。 |
| 署名鍵未解決 | いずれの許容された経路からも 32 バイトの Ed25519 公開鍵が得られない。 |
| 署名無効 | 再構築した Sig_structure に対し、厳格な Ed25519 が false を返した。 |
| ウォレットアドレス不一致 | 経路 2: 署名は検証できたが、再計算したステークアドレス ≠ 主張されたアドレス。 |
サポート外の署名が証明を無効にすることは決してありません
認識されない、またはサポート外の署名アルゴリズムは、情報レベルの重大度を持つ型付きの署名サポート外という結果を
もたらします。コンテンツとタイムスタンプの主張、すなわちオンチェーンの hashes
コミットメントは、検証者がどの署名
アルゴリズムを実装しているかにかかわらず、構造的に有効です。将来のアルゴリズムによる署名だけを持つレコードであっても、
有効な存在証明として現れ、その種の各エントリにはサポート外のタグが付けられます。署名は付加的なものであり、存在は
それに依存しません。