qub プロトコル仕様
qub は暗号学的な時間的コミットメントのためのプロトコルです。すなわち、未来の日付に向けて言葉を封印し、その日付が到来したときに、何がいつ語られたかを正確に証明するためのシステムです。
これを成り立たせている要素は三つあります。drand は分散型のランダム性ビーコンであり、公開日時は誰の善意でもなく物理によって強制されます。永久公開ストレージ は改ざん不能な公開ストアであり、いったん封印された qub をどの当事者も編集または削除できません。ML-DSA-65 は耐量子デジタル署名であり、各 qub は秘密鍵が作成者の端末から決して出ない鍵ペアに結び付けられます。
これらの要素が組み合わさることで、時間ロックされ、改ざんが明らかになり、帰属可能な発言が生まれます。これは、過去を捏造する世界の能力が向上するほど価値が高まる受領証です。
本書の残りは、相互運用可能な実装に必要な規範的仕様です。
qub プロトコル仕様
| 項目 | 値 |
|---|---|
| バージョン | 1.0(プロトコルバージョン 0x01、外側ラッパーバージョン 0x01) |
| 日付 | 2026-05-01 |
| ステータス | ドラフト |
| レビュー対象範囲 | 2026-05-01 |
本書は qub 時限コミットメントシステムの規範的なプロトコル仕様です。相互運用可能な実装に必要なデータ構造、シリアライズ規則、導出式、検証手順を定義します。
スコープ: プロトコル層は意図的に言語中立です。qub の本文は不透明な平文 / Markdown / 約定バイトであり、ロケールに応じた表示はビューアー(qub.social ウェブアプリ、<qub-embed> iframe、MCP クライアントなど)の責任です。
1. 表記と規約
| 表記 | 意味 |
|---|---|
u8, u64, i64 |
指定ビット幅の符号なし / 符号付き整数 |
[u8; N] |
N バイトの固定長バイト配列 |
Vec<u8> |
可変長バイト配列 |
Option<T> |
型 T の値、または不在 |
String |
NFC 正規化された UTF-8 テキスト文字列 |
| ` | |
SHA3-256(x) |
バイト列 x の NIST SHA3-256 ハッシュ(FIPS 202) |
ceil(x) |
天井関数: x 以上の最小整数 |
| CBOR | Concise Binary Object Representation(RFC 8949) |
| big-endian | 最上位バイトを先頭に配置 |
プリイメージ構成におけるすべての整数は、特に指定がない限り、big-endian の固定幅バイト配列として符号化されます(i64 → 8 バイト、u8 → 1 バイト)。
すべてのタイムスタンプは UTC の Unix 秒 です。
2. データ構造
2.1 ComposeQub(作成者のインメモリ状態)
CBOR へはシリアライズされません。永久ストレージには保存されません。作成者アプリのローカル状態です。
ComposeQub {
draft_id: [u8; 16], // Random, generated locally
created_at: i64, // Unix seconds UTC
unlock_at: Option<i64>, // Unix seconds UTC; None while composing
visibility: u8, // 0x01 = public (only value in MVP)
content_type: u8, // 0x01 = text (only value in MVP)
plaintext: Vec<u8>, // UTF-8 qub body
sender_label: Option<String>, // Decorative display name; not authenticated
status: DraftStatus, // Composing | Sealed | Uploaded | Failed
}
2.2 QubEnvelope(復号後のペイロード)
正規 CBOR(§3)を用いてシリアライズされます。SealedQub の内部で暗号化されます。これは復号後にコンテンツの完全性を証明する構造です。
QubEnvelope {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Derived (see §4.1)
content_type: u8, // Content type registry (see §6)
created_at: i64, // Unix seconds UTC
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // V1.1 — when reality renders judgment (verdict-uplift-plan §3.1)
sender_label: Option<String>, // Decorative; not authenticated in MVP
reply_to: Option<[u8; 32]>,// Parent qub_id for reply chains; not in qub_id preimage; not signed (see §9.3)
body: Vec<u8>, // Content payload (UTF-8 for text, CBOR for pact)
body_hash: [u8; 32], // SHA3-256(body) (see §4.2)
sig_alg: u8, // Signature algorithm (see §9.2)
author_signature: Option<Vec<u8>>, // Set when sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Set when sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
cosigner_signature: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}
ベースライン(署名なしテキスト qub): version = 0x01、content_type = 0x01、sig_alg = 0x00、すべての Option フィールドが不在。
その他の v1 構成: content_type = 0x03(約定本文、§6.1 参照)、sig_alg = 0x01(ML-DSA-65)に author_signature と author_pubkey を伴うもの(§9.3 参照)、共同署名された約定向けに cosigner_pubkey と cosigner_signature が併せて存在するもの(§9.7 参照)、返信チェーンの qub 向けに reply_to を親 qub の qub_id に設定するもの(署名スコープへの影響は §9.3 を参照)。
2.3 SealedQub(正規ワイヤ形式)
正規 CBOR(§3)を用いてシリアライズされます。永久ストレージにアップロードされます。これがオンチェーン成果物です。
SealedQub {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Same as QubEnvelope.qub_id
visibility: u8, // 0x01 = public; v1 viewers reject other values
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // V1.1 — surfaced on the verdict-watch CTA
// before reveal; mirrors QubEnvelope.outcome_at;
// bound to qub_id via the §4.1 preimage.
drand_chain_id: String, // drand chain hash (hex string)
drand_round: u64, // Target drand round number
tlock_ciphertext: Vec<u8>, // tlock-encrypted QubEnvelope CBOR bytes
recipient_pubkey: Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
// but not interpreted by the v1 reference viewer
title: Option<String>, // Plaintext title surfaced on the viewer
// countdown before reveal. Bound to qub_id
// via title_hash (§4.1). 1..=100 NFC code
// points, no control characters.
}
2.4 RevealedQub(ビューアーアプリの状態)
CBOR へはシリアライズされません。ビューアーアプリのローカル状態です。復号と検証の成功後に構築されます。
RevealedQub {
qub_id: [u8; 32],
arweave_tx_id: String,
visibility: u8,
content_type: u8,
created_at: i64,
unlock_at: i64,
outcome_at: Option<i64>, // V1.1 — QubEnvelope.outcome_at / SealedQub.outcome_at から引き継がれ、公開ページの判定待機ブロックを駆動します(verdict-uplift-plan §5.1)
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Carried forward from SealedQub.title
reply_to: Option<[u8; 32]>,
body: Vec<u8>,
body_hash: [u8; 32],
body_hash_verified: bool,
author_signature: Option<Vec<u8>>,
author_pubkey: Option<Vec<u8>>,
signature_verified: Option<bool>,
cosigner_pubkey: Option<Vec<u8>>,
cosigner_signature: Option<Vec<u8>>,
cosigner_verified: Option<bool>,
}
3. 正規 CBOR プロファイル
すべての SealedQub および QubEnvelope のシリアライゼーションは本プロファイルに準拠しなければなりません(MUST)。同一の論理構造を与えられた二つの実装は、同一のバイト列を生成しなければなりません(MUST)。
3.1 符号化規則
| 規則 | 仕様 |
|---|---|
| 標準 | RFC 8949 §4.2.1(Core Deterministic Encoding Requirements) |
| マップキー順序 | まず 符号化バイト長 でソート(短いものを先)、次に 辞書式(同一長の符号化はバイト単位) |
| 整数符号化 | 最短形式: 0–23 は初期バイトに、24–255 は 2 バイトに、256–65535 は 3 バイトに、以下同様 |
| 長さ符号化 | 確定長のみ。 不定長の配列、マップ、バイト文字列、テキスト文字列を使用してはなりません(追加情報 = 31 は禁止)。 |
| タグ | CBOR タグなし(メジャー型 6 は禁止)。 |
| 浮動小数点 | 浮動小数点なし(メジャー型 7 値 0xF9–0xFB は禁止)。 |
| テキスト文字列 | UTF-8 符号化、NFC 正規化済み(Unicode 正規化形式 C)。 |
| バイト文字列 | 生バイト。CBOR レイヤでは base64 符号化しません。 |
| 重複キー | エラーで拒否。 パーサは重複するマップキーを黙って受け入れてはなりません(MUST NOT)。 |
| 単純値 | true(0xF5)、false(0xF4)、null(0xF6)のみ許可されます。 |
| オプションフィールド | 不在のオプションフィールドは CBOR マップから完全に 省略 されます(null として符号化しません)。存在するオプションフィールドはソート済みキー順に含めます。 |
3.2 検証済み正規キー順序
これらのキー順序は規範です。実装はキーを正確にこの順序で出力しなければなりません(MUST)。デバッグアサーションは非リリースビルドで順序を検証することが望ましい(SHOULD)。
QubEnvelope(バージョン 0x01、署名なし、すべてのオプションフィールドが不在):
"body" (5 encoded bytes)
"qub_id" (7 encoded bytes)
"sig_alg" (8 encoded bytes)
"version" (8 encoded bytes)
"reply_to" (9 encoded bytes) ← only if present (reply chains)
"body_hash" (10 encoded bytes)
"unlock_at" (10 encoded bytes)
"created_at" (11 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (V1.1 verdict mechanic)
"content_type" (13 encoded bytes)
"sender_label" (13 encoded bytes) ← only if present
"author_pubkey" (14 encoded bytes) ← only if present
"cosigner_pubkey" (16 encoded bytes) ← only if present (pact cosign)
"author_signature" (17 encoded bytes) ← only if present
"cosigner_signature" (19 encoded bytes) ← only if present (pact cosign)
QubEnvelope キー順序の導出: 各キーは CBOR テキスト文字列です。符号化長 = 1 バイトヘッダ + 文字列長(24 バイト未満の文字列について)。まず合計符号化長でソートし、同一長のキーは辞書式でソートします。
SealedQub(バージョン 0x01、公開、受信者なし):
"title" (6 encoded bytes) ← only if present
"qub_id" (7 encoded bytes)
"version" (8 encoded bytes)
"unlock_at" (10 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (V1.1 verdict mechanic)
"visibility" (11 encoded bytes)
"drand_round" (12 encoded bytes)
"drand_chain_id" (15 encoded bytes)
"recipient_pubkey" (17 encoded bytes) ← only if present
"tlock_ciphertext" (17 encoded bytes)
PactTerms(約定本文、content_type 0x03):
"notes" (6 encoded bytes) ← only if present
"terms" (6 encoded bytes)
"title" (6 encoded bytes)
"party_a" (8 encoded bytes)
"party_b" (8 encoded bytes)
"pact_version" (13 encoded bytes)
PactTerm(terms 配列の行):
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier(party_a / party_b マップ):
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 バイト符号化リファレンス
| 型 | CBOR 符号化 | 例 |
|---|---|---|
| SHA3-256 ハッシュ(32 バイト) | 0x58 0x20 + 32 バイト |
body_hash、qub_id |
| タイムスタンプ(i64) | メジャー型 0(正)または 1(負)、最短符号化 | Unix 秒 |
| バージョン(u8、値 1) | 0x01(1 バイト) |
|
| コンテンツタイプ(u8、値 1) | 0x01(1 バイト) |
|
| sig_alg(u8、値 0) | 0x00(1 バイト) |
|
| ML-DSA-65 署名(3,309 バイト) | 0x59 0x0C 0xED + 3,309 バイト |
author_signature、cosigner_signature |
| ML-DSA-65 公開鍵(1,952 バイト) | 0x59 0x07 0xA0 + 1,952 バイト |
author_pubkey、cosigner_pubkey |
4. 規範的導出
4.1 qub_id
qub_id は qub を一意に識別し、QubEnvelope を SealedQub に結び付けます。エンベロープ内容から決定論的に導出されます。
qub_id = SHA3-256(
"QUB_ID_V2" || // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
version || // u8 (1 byte)
content_type || // u8 (1 byte)
created_at || // i64 big-endian (8 bytes)
unlock_at || // i64 big-endian (8 bytes)
outcome_at_or_zero || // i64 big-endian (8 bytes; 0 when outcome_at is absent)
drand_round || // u64 big-endian (8 bytes)
body_hash || // [u8; 32] (32 bytes)
title_hash // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output
ドメインセパレータの符号化: 文字列 "QUB_ID_V2" は 9 バイトの ASCII です。整列のため 1 バイトの 0x00 パディングを付加して 10 バイトにします。実装は正確にこの 10 バイトを使用しなければなりません(MUST): [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]。
outcome_at の符号化: V1.1 では、オプションの outcome_at フィールドをバインディングに折り込むため、プリイメージを 92 バイトから 100 バイトに拡張しました。outcome_at が不在の場合は 8 つのゼロバイトとして符号化されます。プロトコル検証器はあらゆる箇所で outcome_at <= 0 を拒否するため、このセンチネルが正当な値と衝突することはありません。§3.2(ワイヤ形式)と、このフィールドの動機となる verdict メカニクスについてはリポジトリ内の tasks/verdict-uplift-plan.md を参照してください。
drand_round の符号化: V1.2 では、drand_round(対象の drand ラウンド、§4.3)をバインディングに折り込むため、プリイメージを 100 バイトから 108 バイトに拡張し、ドメインセパレータを QUB_ID_V2 に引き上げました。これにより timelock のラウンドが qub のアイデンティティにバインドされます。ゲートウェイは、表示される unlock_at が示すラウンドとは異なる(例えばすでに過去の)ラウンドに暗号文を再バインドすることはできません。解錠手順(§8)はさらに、tlock 暗号文スタンザに焼き込まれたラウンドが unlock_round(unlock_at) と一致することを検証します。したがって、表示される解錠時刻は復号をゲートするラウンドであることが証明可能です。
性質:
- QubEnvelope のいずれかのフィールド(本文、タイムスタンプ、コンテンツタイプ、バージョン)を変更すると、異なる qub_id が生成されます。
- qub_id は暗号化前に計算されます。QubEnvelope と SealedQub は同じ qub_id を持ちます。ビューアーは復号後に両者が一致することを検証します。
- qub_id は
sender_label、author_signature、author_pubkeyに依存しません。これは、同一の内容が同一の時刻に封印された場合、誰が署名したかに関わらず同じ qub_id が生成されることを意味します。 - SealedQub の
titleを変更すると(他のすべてを固定しても)title_hash経由でqub_idが変わります。したがってゲートウェイは、qub のアイデンティティを無効化することなくカウントダウンに表示される平文タイトルを差し替えることはできません。 - SealedQub の
outcome_atを変更すると(他のすべてを固定しても)プリイメージ経由でqub_idが変わります。ゲートウェイは、qub のアイデンティティを無効化することなく、カウントダウンに表示される公開前の verdict-on 日付を差し替えることはできません。 drand_roundを変更すると(他のすべてを固定しても)プリイメージ経由でqub_idが変わります。ゲートウェイは、qub のアイデンティティを無効化することなく timelock 暗号文を別のラウンドに再バインドすることはできません。§8 の解錠時スタンザラウンドチェックと組み合わさることで、表示されるunlock_atは実際に復号をゲートするラウンドとなります。
4.2 body_hash
body_hash = SHA3-256(body)
ここで body は生の Vec<u8> コンテンツペイロードです。テキスト qub の場合、これは UTF-8 符号化された qub 本文です。
4.2.1 title_hash
title_hash = SHA3-256(NFC(title).utf8_bytes) if title is present
title_hash = [0u8; 32] if title is absent
ここで title は、公開前にビューアーのカウントダウンで表示される任意の平文タイトルです(§3.2 参照)。NFC 正規化はハッシュ時に実行されるため、視覚的に等価なコードポイント列の間でダイジェストが安定します。すべてゼロのセンチネルは不在の場合のために予約されており、空文字列は「不在」を非正規符号化したものとして正規 CBOR の境界で拒否されます(正規符号化はフィールドを完全に省略します)。
4.3 公開ラウンドのマッピング
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| パラメータ | ソース | 例 |
|---|---|---|
unlock_at |
ユーザーが選択した UTC の Unix 秒 | 1735689600(2025-01-01 00:00:00 UTC) |
chain_genesis_time |
drand チェーン情報(genesis_time) |
1595431050 |
chain_period_seconds |
drand チェーン情報(period) |
30 |
ceil() 演算は、公開時刻が unlock_at 以上となる最初の drand ラウンド を選択します。これにより qub が選択された公開時刻より前に復号可能にならないことが保証されます。
境界条件: (unlock_at - chain_genesis_time) が chain_period_seconds で正確に割り切れる場合、結果はちょうどそのラウンドであり、qub はそのラウンドの公開時刻に正確に解錠されます。
検証: unlock_at は封印時点で未来でなければなりません(MUST)。unlock_at は created_at から 10 年を超えてはなりません(MUST NOT)(長期の drand 依存リスクを制限するため。UI は 2 年を超える公開日時について警告することが望ましい(SHOULD))。
5. ワイヤ形式 newtype
ワイヤ形式の newtype は、CBOR バイトを JSON、生平文、その他のバイト符号化と取り違えることに対するコンパイル時の安全性を提供します。
| 型 | 内容 | 生成元 | 消費先 |
|---|---|---|---|
SealedQubCbor |
SealedQub の正規 CBOR | serialize_sealed_qub() |
永久ストレージアップロード、ビューアーフェッチ |
QubEnvelopeCbor |
QubEnvelope の正規 CBOR | serialize_qub_envelope() |
tlock 暗号化入力、tlock 復号出力 |
5.1 構築規則
// Production code — only through CBOR serialisers:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);
// There is deliberately NO From<Vec<u8>> implementation.
// You cannot accidentally wrap arbitrary bytes in a wire format type.
// Accessing raw bytes:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();
5.2 構築時の検証
from_encoded() は、入力が有効な CBOR マップヘッダで始まることを検証することが望ましい(SHOULD)。完全な構造検証は二重解析を避けるため、構築時ではなく解析時に行います。
6. コンテンツタイプレジストリ
| 値 | 型 | 最大本文サイズ | 備考 |
|---|---|---|---|
0x00 |
予約済み(無効) | — | 使用してはなりません(MUST NOT) |
0x01 |
プレーンテキスト(UTF-8、制限付き Markdown) | 50 KB 有料 / 10 KB 無料 | 表示規則は §10 を参照。無料 / 有料の分割はアップロードサービスによって強制されます。プロトコル層の絶対上限は 50 KB です。 |
0x02 |
予約済み(将来) | — | 将来のコンテンツタイプ用に割り当て済み。v1 では無効です。ビューアーは以下の規則に従って拒否しなければなりません(MUST)。 |
0x03 |
約定(双方向合意、CBOR 本文) | 100 KB | 本文は正規 CBOR PactTerms(§6.1)。共同署名は §9.7 に従います。 |
0x04 |
判定(作成者の自己採点、CBOR 本文) | 8 KB | 本文は正規 CBOR VerdictBody(§6.2)。システム側の verdict インテントによってのみ発行されます。親子関係は本文ではなく Parent-Tx-Id Arweave タグ上にあります。verdict-uplift-plan §3.4 を参照。 |
ビューアーは、不明なコンテンツタイプを明確なユーザー可視のエラーで拒否しなければなりません(MUST)。ビューアーは不明な型をテキストとして表示しようとしてはなりません(MUST NOT)。
6.1 約定本文(content_type = 0x03)
約定本文は PactTerms 値の正規 CBOR 符号化です。
PactTerms {
pact_version: u8, // 0x01 for structured/v1
title: String, // ≤ 200 bytes, NFC
terms: Vec<PactTerm>, // ≤ 20 rows
party_a: PartyIdentifier, // initiator
party_b: PartyIdentifier, // counter-signer
notes: Option<String>, // ≤ 5,000 bytes, NFC; absent key if none
}
PactTerm { key: String (≤ 100), value: String (≤ 2,000) } // NFC on both sides
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }
3 つのマップすべての正規 CBOR キー順序は §3.2 にあります。約定 CBOR のシリアライズ済み合計は 100 KB を超えてはなりません(MUST NOT)(§6 と一致)。
スキーマ識別子。 structured/v1 約定の terms の最初の行は { key: "pact_schema", value: "structured/v1" } でなければなりません(MUST)。このマーカーを持たない行は「カスタム」約定であり、構造化検証やスキーマ対応の表示は受けません。
固定された承認スロット。 structured/v1 約定は次のキーの下に正確に 4 つの承認行を持ちます。
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
それぞれの value は (role, kind) ペアによって選ばれる 8 つの固定英文字列のいずれかであり、ここで role ∈ { seller, buyer, provider, client }、kind ∈ { standard, capacity } です。これらの文字列自体が 規範的プロトコルデータ であり、両当事者の ML-DSA-65 署名は body_hash を介して正確なバイト列にコミットします。これらはローカライズされません。署名対象の本文は言語中立です。文言を変更するには新しいスキーマバージョン(structured/v2)が必要です。
8 つの文字列、それらのルックアップ(acknowledgement_for(role, kind))、および各々の根拠はリファレンス実装で固定されています。準拠する実装はバイト単位で同一の承認値を出力しなければならず(MUST)、4 つの役割組み合わせをすべてカバーするゴールデンフィクスチャの SHA3-256 body-hash テストがあらゆる差異を捕捉します。
ビューアーの表示順序。 承認文字列には「described above」のようなフレーズが含まれており、これは説明 / スコープ行が承認の前に表示されることを前提としています。ビューアーは terms 配列を CBOR の順序で表示しなければなりません(MUST)。並び替えは本文の意味を壊します。
カウンターパーティの連絡先。 Party B の contact が有効なメールアドレスである場合、qub アップロードサービスはステージ時にレビュー / 共同署名招待メールを自動送信し、最終的な共同署名をその同じアドレスの検証に結び付けます(§9.7)。Party B の連絡先が不在の約定でも共同署名はできますが、帯域外チャネルを通じてのみとなります。サービスは対応する 15 分のメール検証マーカーを生成できない共同署名リクエストを拒否します。
6.2 判定本文(content_type = 0x04)
判定本文は VerdictBody 値の正規 CBOR 符号化です。
VerdictBody {
verdict_version: u8, // 0x01 for structured/v1
outcome: u8, // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
reflection: Option<String>, // ≤ 2,000 bytes NFC; "what changed, what did you learn"
evidence_url: Option<String>, // ≤ 2,048 bytes; HTTPS only; absent key when omitted
}
正規 CBOR キー順序:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
シリアライズ済み判定 CBOR の合計は 8 KB を超えてはなりません(MUST NOT)(上記レジストリ行と一致)。
結果列挙。 ワイヤバイトはインテント中立です。4 つのバケット Right / Partial / Wrong / Unfalsifiable は、判定を持つすべてのインテントの結果空間を網羅します。インテント別ラベル(Right の場合「Called it」/「Kept it」/「Shipped」/「Confirmed」など)はビューアー側の描画上の関心事であり、親 qub のインテントに対して解決されます。ワイヤは言語およびインテントに中立を保ちます。1..=4 の範囲外の値は復号時に拒否しなければなりません(MUST)。
親リンク。 判定 qub は本文中に親への参照を持ちません。親 qub の Arweave トランザクション ID はアップロード時に Parent-Tx-Id ストレージタグとして発行されます(§7 ストレージタグ層)。これにより本文は自己完結した自己採点の署名済み宣言として保たれ、監査チェーン(「何について正しかったのか」)は Arweave タグのルックアップを介して確立されます。
根拠 URL の安全性(規範的)。 evidence_url が存在する場合、検証側(構成側、ワイヤ側、Worker エッジ)は以下を強制しなければなりません(MUST):
- HTTPS のみ。 文字列はバイト列
https://で始まらなければなりません(MUST)。他のスキーム —http、ftp、javascript、data、fileなど — は拒否します。 - 長さ上限。 ≤ 2,048 バイト(ブラウザ URL の実用上限)。
- NFC + 敵対的コードポイントチェック。
titleおよびreflectionと同じ規則 — 双方向制御 / ゼロ幅 / タグブロック / BOM / C0 / C1 のコードポイントは拒否します。定義は Rust のcrate::handle::contains_hostile_text_codepointおよび TS のworkers/api/src/utils/unicode.ts::isHostileCodepointと一致します(同期を保つこと)。 - 空白なし、ASCII 制御文字なし。 URL 内のどこかに空白 / DEL /
0x20未満のバイトがある場合は拒否します — 双方向規則が捕捉しない\n/\t注入ベクターを閉じます。 - 空でないホストセグメント。
https://と最初の/、?、または#の間のすべては空であってはなりません(MUST)。
サーバー側フェッチなし。 Worker は URL をプロキシ、フェッチ、プレビューしてはなりません(MUST NOT)。プロトコルは文字列を保存するだけで、描画はビューアー側で rel="nofollow noopener noreferrer" target="_blank" 付きでリンクテキストとともにホスト名を可視表示して行います。
振り返り。 任意の作成者記述の振り返りテキスト(「何が変わったか、何を学んだか」)。title と同じ NFC + 敵対的コードポイントの検証を行います。空 / 空白のみの入力は構成時に不在へ折りたたまれます。
スキーマバージョン。 v1 は verdict_version = 0x01 のみをサポートします。将来のスキーマ改訂はこのバイトを上げ、§12 に従って新しいプロトコルバージョンとともに着地します。
7. 封印プロトコル
完全な封印シーケンスです。各ステップは規範的です。
1. User composes plaintext and metadata in ComposeQub.
2. Validate:
a. body is non-empty.
b. body size ≤ max for content_type and user tier (see §6).
c. unlock_at is in the future.
d. unlock_at ≤ created_at + 10 years.
e. content_type is a known, supported value.
3. Compute body_hash = SHA3-256(body).
4. Set created_at = current Unix seconds UTC.
5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
compute drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
(Computed here, before qub_id, because drand_round is bound into the qub_id
preimage — §4.1, V1.2.)
6. Compute qub_id (see §4.1), folding in drand_round from step 5.
7. Construct QubEnvelope with all fields.
8. Serialise QubEnvelope using canonical CBOR → bytes B.
Assert: serialised output matches canonical profile (§3).
9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C, and matching qub_id, version,
unlock_at, drand_chain_id, drand_round.
12. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12a. Generate K = 32 random bytes (CSPRNG) and N = 12 random bytes (CSPRNG).
Compute W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
per §13. The bytes uploaded to permanent storage are the OuterWrapper CBOR W,
never the bare SealedQubCbor. K leaves the device only as the URL
fragment in step 16.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit W (the OuterWrapper bytes) to the qub upload service; the service
signs and uploads to permanent storage. The service is byte-blind to the inner
SealedQubCbor and never receives K.
16. Receive arweave_tx_id from the service. Construct delivery URL as
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (or `<origin>/s/<short_code>#<base64url(K)>`
when a short code is allocated). Browsers do not transmit URL fragments
to servers, so K is never observed by qub.social or any storage gateway.
ストレージタグレイヤ(帯域外)。 qub アップロードサービスは、ラップされたペイロードと共に意図的に小さなストレージトランザクションタグのセットを付加します。Content-Type=application/octet-stream は規範的に必須です。リファレンスサービスはさらに、作成者がそれらを表示するよう選択した場合に 3 つの任意タグを付加します。Intent(許可リストで検証された作成意図 — 例: quote、reply、commitment)、Author(64 文字の小文字 16 進数による作成者の §9.3 公開鍵フィンガープリント)、および Parent-Tx-Id(返信チェーン用の親 qub のストレージトランザクション ID、43 文字の base64url)。
Author タグは qub ごとにオプトイン です。リファレンス作成者アプリは、ユーザーが封印時に明示的に公開帰属を有効にした場合にのみこれを付加します。トグルがオフのとき(既定)、Author タグは書き込まれず、qub はチェーン上で帰属表示されません。永久ストレージ上のいかなる情報も、そのアップロードを作成者のハンドル、メール、その他の qub にリンクしません。トグルがオンのとき、Author フィンガープリントは §9.5 のアテステーションチェーンを介して作成者が選んだ @handle に解決されます。返信チェーンの関係性および Intent は識別性を持ちません。外側のラッパー(§13)は内側の 本文 を暗号文相関から保護し、ハーベスターが drand ラウンドの公開後に qub 形状のアップロードを認識し一括復号することを防ぎます。
リファレンスサービスは意図的に App-Name、App-Version、Type タグを付加しません。そのような単一値フィルタは GraphQL クエリに qub コーパス全体を返してしまい、ラッパーの本文限定の秘匿性スコープと矛盾するためです。
準拠する検証者は、§11 の第三者検証のためにいかなるストレージタグにも依存してはなりません(MUST NOT)。本文ハッシュ / qub_id / 署名は内側の CBOR にのみコミットし、タグセットにはコミットしません。
8. 解錠プロトコル
完全な解錠シーケンスです。各ステップは規範的です。
1. Viewer opens delivery URL. Extract arweave_tx_id from path AND
K = base64url_decode(fragment) from the URL fragment. If the fragment
is absent or malformed → display "this URL is missing its decryption
key" and stop; the viewer MUST NOT contact the storage gateway
without K, since fetching wrapped bytes the viewer cannot decrypt
serves no purpose and only leaks the access attempt.
2. Check denylist. If tx_id is denylisted → display block message. Stop.
3. Fetch OuterWrapper bytes from permanent storage (with multi-gateway fallback).
3a. Unwrap: parse the bytes as OuterWrapper (§13), verify the wrapper
`version` byte is `0x01`, and compute SealedQubCbor =
unwrap_sealed_qub(OuterWrapper, key=K). Any AEAD authentication
failure (wrong K, tampered ciphertext, swapped qub_id-as-AAD,
swapped nonce) → display "this URL's decryption key does not match
the stored qub" and stop. Authentication failures are
indistinguishable to the viewer per §13.5.
4. Parse SealedQubCbor → SealedQub.
5. Validate: SealedQub.version is known (0x01). Reject unknown versions.
6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
6a. Round-binding check (V1.2). Recompute expected_round =
ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
Reject unless SealedQub.drand_round == expected_round AND the round baked
into the tlock ciphertext stanza (read via the age/tlock header, no signature
required) == expected_round. The stanza round is the one that actually gates
decryption; without this check a malicious creator could bind the ciphertext
to an already-past round while displaying a future countdown, so anyone
reading the stored bytes could decrypt before unlock_at. Implementations with
no chain identity (test mocks) skip this check.
7. Once current time ≥ SealedQub.unlock_at:
a. Fetch drand round signature for SealedQub.drand_round from drand network.
b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
8. Parse B → QubEnvelope.
9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
Fail → integrity error.
13. Verify: QubEnvelope.content_type is known and renderable.
Known values: 0x01 (text), 0x03 (pact). Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using appropriate renderer (see §10 for text, §6 for pact).
17. Construct RevealedQub for display.
9. 作成者署名
9.1 根拠
qub は永久ストレージに保存されます。作成者の署名は無期限に偽造不可能でなければならず、そのため v1.0 では、qub の永久的な寿命の間にセキュリティが劣化する可能性のある古典スキームではなく、耐量子の ML-DSA-65 スキーム(FIPS 204)を使用します。
9.2 アルゴリズムレジストリ
sig_alg |
スキーム | 鍵サイズ | 署名サイズ |
|---|---|---|---|
0x00 |
署名なし(未署名) | — | — |
0x01 |
ML-DSA-65(FIPS 204) | 1,952 バイト | 3,309 バイト |
ビューアーは不明な sig_alg 値を拒否しなければなりません(MUST)。
9.3 署名プリイメージの構成
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V1" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 // u8 (1 byte): MUST be 0x00 in v1.0
)
// Total preimage: 91 bytes → 32-byte hash
signature = Sign(author_secret_key, sig_input)
ドメインセパレータ: "QUB_AUTHOR_SIG_V1" は 17 バイトの ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]。パディングなし。
末尾バイト: 91 番目のプリイメージバイトは 0x00 でなければなりません(MUST)。リファレンス実装はこれを crates/qub-core/src/signing.rs の定数 ORG_ID_PRESENT_INDIVIDUAL = 0x00 として公開しています。検証用に sig_input を再構築するビューアーは同じバイトを出力しなければなりません(MUST)。
署名スコープ — 何がカバーされ何がカバーされないか。 sig_input は 4 つのエンベロープフィールドにコミットします: version、qub_id、body_hash、unlock_at(加えて固定のドメインセパレータと org_id_present バイト)。これらのうち 3 つは構造的な不変量です: qub_id 自体は §4.1 のプリイメージを介して version、content_type、created_at、unlock_at、body_hash から導出されるため、content_type または created_at を変更すると異なる qub_id が生成され、署名は推移的に無効化されます。したがって直接認証される表面は次のとおりです:
| フィールド | 署名による認証 | 方法 |
|---|---|---|
version |
✓ | sig_input への直接入力 |
qub_id |
✓ | 直接入力 |
body_hash |
✓ | 直接入力 |
unlock_at |
✓ | 直接入力 |
content_type |
✓ | 推移的に、qub_id プリイメージ経由 |
created_at |
✓ | 推移的に、qub_id プリイメージ経由 |
outcome_at |
✓ | 推移的に、qub_id プリイメージ経由 |
drand_round |
✓ | 推移的に、qub_id プリイメージ経由(V1.2) |
body |
✓ | 推移的に、body_hash = SHA3-256(body) 経由 |
author_pubkey |
—(暗黙的) | 署名を検証した鍵が定義上の作成者 |
sender_label |
✗ | 表示専用テキスト。署名を壊さずに変更可能 |
reply_to |
✗ | スレッドポインタ。署名を壊さずに変更可能 |
cosigner_pubkey / cosigner_signature |
— | 同じ sig_input に対して独立に署名される(§9.7 参照) |
drand_chain_id、tlock_ciphertext、visibility |
— | 外側 SealedQub のフィールドでありエンベロープ内にない。それら自身の構造的不変量(ラウンド / チェーンの一貫性)によってカバーされるが、作成者署名ではカバーされない。(drand_round は現在、qub_id プリイメージを介して推移的にバインドされる — 上記参照。) |
認証されないフィールドのセキュリティ含意。
- 保存されたバイトに書き込みアクセスを持つ当事者は、作成者署名を無効化することなく
sender_label(「Alice」→「Mallory」)を差し替えることができます。エンベロープ内のauthor_pubkeyは真のアイデンティティアンカーであり続けます。ビューアーはsender_labelを信頼するのではなく、author_pubkeyから(§9.5 アテステーション層を介して)表示アイデンティティを導出しなければなりません(MUST)。 reply_toフィールドも同様に署名後に編集できます。qub_idはコンテンツアドレス指定であるため、攻撃者はreply_toを存在しないターゲットに向けることはできませんが、返信を別の既存の qub に黙って再親付けすることはできます。
sender_label または reply_to をエンドユーザーに表示する実装は、ラベルではなく認証されたアイデンティティ(公開鍵フィンガープリント、アテステーション)を主要なアイデンティティシグナルとして表示しなければなりません(MUST)。
9.4 検証手順
1. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (same formula as §9.3).
6. Verify(author_pubkey, sig_input, author_signature).
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
署名検証は最もコストの高い操作です(特に ML-DSA-65)。より安価なチェック(ハッシュ、qub_id、unlock_at)がすべてパスした後に実行することが望ましい(SHOULD)。
9.5 アイデンティティアテステーション
アイデンティティアテステーション — author_pubkey を qub ハンドル、メールアドレス、ソーシャルハンドル、パスキー資格情報などの人間が認識可能なアイデンティティクレームにマッピングするもの — は ビューアー側のプログレッシブエンハンスメント であり、署名検証には 必須ではありません。アテステーションを表示アイデンティティに解決するビューアーは、次の優先順位を適用しなければなりません(MUST):
handle > email > social > fingerprint
フィンガープリントフォールバックは SHA3-256(author_pubkey) の小文字 16 進数であり、署名付きの qub であれば常に利用可能です。ビューアーは表示のためにこれを省略してもかまいません(MAY) — リファレンスビューアーは qub: に続けて最初と最後の 4 バイトを表示します(qub:<8 hex>…<8 hex>)。
準拠する検証者は §9.4 のすべてのチェックを、qub API に接続せず、永久ストレージと drand を超えるネットワークを使用せず、サーバー側のルックアップなしで完了できます。アテステーション解決は、署名検証が成功した後にのみ実行される、別個のベストエフォートステップです。
9.6 サイズへの影響
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| 署名 | 64 バイト | 3,309 バイト |
| 公開鍵 | 32 バイト | 1,952 バイト |
| qub あたり合計 | 96 バイト | 5,261 バイト |
| ストレージコスト差分(約 $5/MB) | ~$0.0005 | ~$0.026 |
500–2,000 バイトのテキスト qub の場合、ML-DSA-65 は保存サイズをおおよそ 3 倍にします。絶対コストは無視できるほど小さいです。
9.7 共同署名者検証(約定の双方向合意)
双方向合意(content_type = 0x03)では、両当事者が同じ条件に同意したことを証明するために 2 つ目の署名層が用いられます。
エンベロープフィールド:
cosigner_pubkey: 共同署名者(Party B)の ML-DSA-65 公開鍵。cosigner_signature: 作成者と同じsig_inputに対する署名(§9.3)。
両方のフィールドは共に存在するか、共に不在でなければなりません(MUST)。正確に 1 つのみが存在する場合、ビューアーは完全性エラーを報告しなければなりません(MUST)。
検証手順:
1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3.
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."
性質:
- 共同署名者は作成者と同一の
sig_inputに署名します。両当事者は同じqub_id、body_hash、unlock_atにコミットします。 qub_id導出(§4.1)は共同署名者フィールドを含みません。既存のエンベロープに共同署名者を追加してもqub_idは変わりません。- 約定は作成者のみが署名する(片側のコミットメント)、共同署名者のみ(異例)、または両方(完全な双方向の証明)のいずれにもできます。
メール結合ゲート(運用上)。 ステージングされた約定が Party B のメール連絡先(§6.1)を持つ場合、qub アップロードサービスは、ステージング ID とその連絡先の正規化メールハッシュの両方に一致する短期間のメール検証マーカーが存在しない限り、共同署名リクエストを拒否しなければなりません(MUST)。マーカーは、マジックリンクトークンが staging_id を含み、検証されたアドレスが SHA-256(normalise_email(party_b.contact)) に一致する場合に /api/v1/auth/verify によって書き込まれます。ここで normalise_email(addr) はローカル部分の大文字小文字を保持しドメイン部分のみを小文字化し(RFC 5321 §2.3.11 に従う)、SHA-256 は NIST FIPS 180-4 ハッシュ(§4 の導出で使用される SHA3-256 とは別物)です。マーカーは発行後 900 秒(15 分)で失効します。これは運用上のなりすまし防止ゲートであり、オンチェーン qub 証明の一部では ありません。§11 を再現する第三者検証者は永久ストレージと drand だけを必要とし、サーバー側のルックアップは一切必要ありません。マーカーはサーバー側にのみ存在し、署名された本文の一部になることはありません。
サイズへの影響(ML-DSA-65 作成者 + 共同署名者):
| 要素 | サイズ |
|---|---|
| 作成者署名 | 3,309 バイト |
| 作成者公開鍵 | 1,952 バイト |
| 共同署名者署名 | 3,309 バイト |
| 共同署名者公開鍵 | 1,952 バイト |
| 暗号オーバーヘッド合計 | 10,522 バイト |
| ストレージコスト差分 | ~$0.05 |
10. Markdown 表示とサニタイゼーション
このセクションはセキュリティ上重要です。ビューアーはテキスト qub(content_type = 0x01)を制限付きの Markdown サブセットを用いて表示します。
10.1 許可された要素
- 見出し:
#から####まで(#####および######はなし) - 強調: 太字(
**)、斜体(*)、取り消し線(~~) - リスト: 順序付き(
1.)および順序なし(-、*) - 引用ブロック(
>) - コード: インラインスパン(```)およびフェンスブロック(`````)
- 水平線(
---) - 改行(末尾の 2 つの空白または空行)
- 段落
10.2 禁止された要素
| 要素 | 処理 |
|---|---|
生 HTML(<div>、<script> など) |
完全に除去。HTML は通過しません。 |
画像() |
除去。画像構文は出力から削除されます。 |
リンク([text](url)) |
URL は可視のプレーンテキストとして表示。自動リンク化なし。明示的なユーザー操作なしにはクリック不可。 |
| 危険な URL スキーム | javascript:、data:、vbscript:、file: — 除去。 |
| iframe、エンベッド、オブジェクト | 除去。 |
| HTML エンティティ | 安全な場合にのみ表示文字へデコード。 |
10.3 実装
実装は、ブロックリストではなく 厳密な許可リストパーサ を使用しなければなりません(MUST)。推奨アプローチ:
pulldown-cmark(または同等品)を使用して Markdown を解析します。- AST を走査し、許可リスト(§10.1)にないノードを破棄します。
- リンクノードについて: URL をクリック可能な
<a>要素ではなく可視テキストとして出力します。 - フィルタリングされた AST を 型付き中間表現(例えば、安全なバリアントのみを持つ
MarkdownNode列挙型)に変換します。生 HTML はこの IR では構造的に表現不可能です。 - 型付き IR から目標のビュー層(例えば、リアクティブビューコンポーネント、DOM ノード)へ表示します。HTML 文字列の連結や
innerHTMLはどの段階でも使用しません。
ブロックリストアプローチは、新しい Markdown 拡張やパーサの癖がフィルタされない要素を導入し得るため脆弱です。型付き AST アプローチは XSS を構造的に不可能にします。任意の HTML を運ぶことができるバリアントが存在しないためです。
10.4 サイズと構造の制限
- 表示される見出しの最大深度:
####(H4)。#####以降は太字テキストとして表示されます。 - 段落数に制限なし(本文サイズの制限は §6 にあります)。
- フェンスコードブロック: MVP では構文ハイライトなし。等幅の整形済みテキストとして表示されます。
11. 第三者検証
任意の第三者は、qub の協力なしに公開 qub を検証できます。検証手順:
1. Obtain arweave_tx_id (from delivery URL or direct knowledge).
2. Fetch SealedQubCbor from any storage gateway.
3. Confirm storage block inclusion (block height, block timestamp).
4. Parse SealedQubCbor → SealedQub.
5. Fetch drand round signature for SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify QubEnvelope.qub_id == SealedQub.qub_id.
10. Verify QubEnvelope.unlock_at == SealedQub.unlock_at.
11. If sig_alg != 0x00: verify author_signature (see §9.4).
12. All checks pass → qub is verified.
検証が証明するもの:
| 証明 | それが確立するもの |
|---|---|
| コミットメント | 暗号文はストレージブロックタイムスタンプの時点で存在していたこと。 |
| 完全性 | 平文の本文がコミットされたハッシュと一致しており、改変されていないこと。 |
| タイミング | コンテンツは drand ラウンドまで読めなかったこと。これは選択された公開時刻に対応します(tlock および drand のセキュリティ前提に従う)。 |
検証が証明 しない もの:
| 非証明 | 理由 |
|---|---|
| 作成者性 | sender_label は装飾的です。sig_alg ≥ 0x01 がない場合、誰でもこの内容を封印できた可能性があります。 |
| 意図 | qub は内容とタイミングを証明しますが、作成者が主観的に何を意味したかは証明しません。 |
| 事前イベントタイミング | ストレージブロックの取り込みは実際のアップロードから数分遅れる場合があります。コミットメントタイムスタンプはブロック時刻であり、ユーザーが「封印」を押した瞬間ではありません。 |
12. バージョン管理
12.1 プロトコルバージョン
SealedQub と QubEnvelope の両方にある version フィールド(u8)はメジャープロトコルバージョンを識別します。
- ビューアーは不明なメジャーバージョンを明確なエラーで拒否しなければなりません(MUST)。
- 既知のメジャーバージョンは、前方互換性規則が許容する場合、不明なオプションフィールドを許容してよい(MAY)(正規キー順序に存在しないオプションフィールドは無視されます)。
- コンテンツタイプ(
content_type)と署名スキーム(sig_alg)はバージョンゲート化されています。新しい値は、新しいプロトコルバージョンまたは明示的なレジストリ更新と共にのみ導入できます。
12.2 バージョン履歴
| バージョン | 値 | 説明 |
|---|---|---|
| v1 | 0x01 |
公開テキスト qub(content_type 0x01)、約定の双方向合意(0x03、structured/v1 スキーマ、ML-DSA-65 作成者 + 共同署名者)、tlock、SHA3-256 |
12.3 前方互換性
不明なオプション CBOR マップキー(§3.2 の正規順序にないキー)を持つ QubEnvelope に遭遇した v1 ビューアーは、それらのキーを無視し、既知のフィールドを使用して検証を続行することが望ましい(SHOULD)。これにより、メジャーバージョンの引き上げを必要とせずに将来のマイナーな追加(例: 新しいメタデータ)が可能になります。
sig_alg = 0x01(ML-DSA-65)に遭遇したが ML-DSA-65 検証サポートを欠く v1 ビューアーは、qub 全体を拒否するのではなく、「署名は存在するが検証できません」という通知と共に qub の内容を表示することが望ましい(SHOULD)。リファレンス実装は今日、0x00 と 0x01 以外のすべての sig_alg 値を拒否します。v1 レジストリには他の有効なアルゴリズムが含まれていないためです。厳格な拒否とソフトフェイルは、第三のアルゴリズムが登録されるまで観測上は同一です。上記のソフトフェイル動作は §9.2 が新エントリを承認した時点で重要となり、リファレンスビューアーはその時点でソフトフェイルに更新されます。
12.4 外側ラッパーバージョン
§13 に記載される OuterWrapper は、SealedQub.version および QubEnvelope.version から 独立した 独自の version バイトを持ちます。2 つのバージョン空間は別個に進化します。将来の耐量子対称置換は内側のプロトコルバージョンに触れずにラッパーバイトを引き上げ、将来のプロトコル層の追加(例: 新しいエンベロープフィールド)はラッパーバイトに触れずに内側のバージョンを引き上げます。
OUTER_WRAPPER_VERSION_* |
値 | アルゴリズム | ステータス |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
12 バイトのナンス、16 バイトの認証タグ、qub_id に結合された AAD を持つ AES-256-GCM |
v1 既定 |
| — | 0x02–0xFF |
予約済み | 将来 |
ビューアーは不明なラッパーバージョンを明確なエラーで拒否しなければなりません(MUST)。プロトコルは、具体的な移行ドライバ(例: 異なる AEAD を支持する NIST のガイダンス)が現れるまでラッパーバージョン空間を意図的に狭く保ちます。0x02 スロットはそのアルゴリズムを導入する同じリビジョンで割り当てられます。
13. 外側暗号ラッパー
13.1 根拠
プロトコル層(QubEnvelope → tlock → SealedQub)は、封印された qub を 時間ロック します。本文は unlock_at および drand ラウンド署名が公開されるまで読み取り不可能です。しかし解錠後、ラウンド署名は公開され、SealedQub の正規 CBOR 形状は認識可能であるため、永久ストレージのトランザクションをインデックス化したハーベスターは qub コーパス全体を一括復号できます。
外側暗号ラッパーは、正規 SealedQubCbor と永久ストレージにアップロードされるバイトの間に追加の対称 AEAD 層を挿入することでそのチャネルを閉じます。256 ビットの鍵 K は配信 URL の URL フラグメントとユーザー端末にのみ存在します。ブラウザは URL フラグメントをサーバーに送信しないため、qub.social、すべてのストレージゲートウェイ、およびそれらの前にあるすべての CDN は、観測上 K に対して盲目です。したがって永久ストレージ上のすべての qub は、作成者が共有することを選んだ URL なしには平文を回復不能な不透明な暗号文です。
正味の効果:
- 既定での列挙耐性。 永久ストレージ上のラップされたバイトは任意の暗号文とバイト識別不可能です。「qub 形状のアップロードを GraphQL クエリし、公開された drand 署名で一括復号する」というハーベスター戦略は平文に到達しません。
- 暗号シュレッディング・プライバシー姿勢。 qub.social は自らのコーパスを文字通り復号できません。召喚状は暗号文に届くだけで平文には届きません。
- 2 層の機密性階層。 既定 = リンク制御アクセス(本セクション)。受信者暗号化された非公開 qub(予約済みのフェーズ 2 機能であり、未仕様)が第 2 層として上に重なります。
13.2 レイヤリング
plaintext body ← QubEnvelope.body (§2.2)
↓ canonical CBOR (§3)
envelope CBOR
↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
↓ canonical CBOR (§3)
SealedQubCbor bytes ← inner wire artifact
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12a, this section)
OuterWrapper CBOR bytes ← uploaded to permanent storage (§7 step 15)
プロトコル層(§7、§8)の封印と解錠はラッパー境界より下では変わりません。ラッパーは seal() の呼び出しサイトで取り付けられ、unlock() の呼び出しサイトで取り外されます。
13.3 OuterWrapper データ構造
struct OuterWrapper {
version: u8, // 0x01, see §12.4
qub_id: [u8; 32], // copied from inner SealedQub; AEAD AAD
nonce: [u8; 12], // 96-bit AEAD nonce
ciphertext: Vec<u8>, // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}
フィールドの不変条件。
versionは v1.0 ラッパーバイトについて0x01でなければなりません(MUST)。qub_idは、アンラップ後に回復される SealedQub のqub_idフィールドと等しくなければなりません(MUST)。アンラップステップはこれを直接強制しません(AEAD AAD バインディングがバイトレベルの改ざんを不可能にします)が、解錠層が関係を推移的にチェックします。作成者が内側のqub_idがラッパーのqub_idと一致しないSealedQubCborをラップする場合、§8 ステップ 11 が失敗します。nonceは 96 ビット(12 バイト)でなければならず(MUST)、すべてのラップ操作のために CSPRNG によって新たに生成されます。同じ鍵の下でナンスを再利用すると、平文を回復する AEAD ナンス再利用攻撃が許されます。生成者は(key、nonce)ペアをワンショットとして扱わなければなりません(MUST)。ciphertextは AES-256-GCM 出力です。暗号文バイトと 16 バイトの認証タグを連結したものです。ciphertext.len() == SealedQubCbor.len() + 16が正確に成り立ちます。
CBOR 符号化。 §3 に準拠した正規 CBOR、同じキー順序規則(符号化バイト長の昇順、次に辞書式)を使用します。4 つのキーは次のとおりです。
| キー | 符号化バイト | 順序 |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
したがって OuterWrapper CBOR の最初のバイトは 4 エントリのマップの確定長マップヘッダ(0xA4)です。
13.4 qub_id への AAD バインディング
ラッパーは qub_id を AEAD 追加認証データとしてバインドします。これは 3 種類の攻撃に対する負担を担う構造的防御です。
| 攻撃 | 防御 |
|---|---|
暗号文をラッパー内の異なる qub_id フィールドの下に移動する |
AAD 不一致 → AEAD 認証失敗 |
| qub A の URL フラグメントを qub B の永久ストレージバイトと混合する | AAD 不一致 → AEAD 認証失敗 |
アップロード後にラッパーの qub_id フィールドを改ざんする |
AAD 不一致 → AEAD 認証失敗 |
qub_id をラッパー平文に持ち込むことは列挙耐性を意味のある形で弱めません。qub_id 自体が §4.1 プリイメージの SHA3-256 ハッシュであり、ダイジェストから回復可能なプリイメージはなく、すでにラッパーバイトをハーベストした列挙者は、可視の qub_id からアップロードの存在自体から推測できる以上のことを学びません。
13.5 ラップおよびアンラップアルゴリズム
wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
require K.len() == 32 and N.len() == 12 and Q.len() == 32
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C includes the 16-byte authentication tag at the end
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
require K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
require O.version == 0x01 // §12.4
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
return P // P is the inner SealedQubCbor
失敗モードの集約。 不正な K、不正なナンス、AAD 不一致、改ざんされた暗号文はすべて同じ DECRYPT_FAILED エラーを生成します。これは意図的な AEAD の性質です。失敗モードを区別すると、リモート攻撃者が不正なラッパーを送信して応答時間を計ることで探れるサイドチャネルが生まれます。リファレンス実装はすべての AEAD 失敗を単一のエラー形状に集約しなければなりません(MUST)。
13.6 鍵素材と配布
ラッピング鍵 K は qub ごとに CSPRNG によって生成される 256 ビットの一様ランダム値です。リファレンス実装は次のソースから取得します。
- WASM 作成者:
getrandom(wasm_jsバックエンド配下の WebCrypto)。 - Worker サーバー側の seal ルート:
crypto.getRandomValues。
配布: K は URL セーフ base64(RFC 4648 §5、パディングなし)として符号化されなければならず(MUST)、配信 URL のフラグメント成分として付加されます。
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
フラグメントは準拠ブラウザによっていかなるサーバーにも送信されません。完全な配信 URL(フラグメントを含む)をユーザー端末を超えて永続化する回復チャネル(サーバー側の履歴インデックス、オプトイン式のメール自動送信)は、既定の暗号シュレッディング姿勢に対する明示的なトレードオフであり、明示的なユーザー同意でゲート化されなければなりません(MUST)。
フラグメント喪失。 ユーザーが URL フラグメントを失い、回復チャネルを持たない場合、qub は読み取り不可能です。これは設計の負担を担うトレードオフであり、封印時にユーザーに開示されなければなりません(MUST)。MVP は封印時開示を明示的な「この URL を保存」コピーと、オプトインするユーザー向けの検証済みメール回復チャネルで強化します。
13.7 本セクションの範囲外
- 作成者署名(§9)は変更されません。署名は内側
QubEnvelopeの内部で計算され、アンラップ → tlock 復号 → CBOR 解析の後に回復されます。 - 受信者暗号化された非公開 qub(予約済みのフェーズ 2 機能であり、未仕様)は、第 2 層の機密性階層としてこのラッパーの上に重なります。両層は同時に有効化できます。
- 約定(§6、content_type
0x03)はテキスト qub と完全に同じようにラップされます。ラッパーは内側のコンテンツタイプに対してバイト的に盲目です。
13.8 公開 qub(ラッパーの省略)
外側ラッパーは 配信層において任意 です。作成者は qub を 公開 として封印でき、その場合、正規 SealedQubCbor は OuterWrapper 層も鍵 K もなしに 直接 永久ストレージへ書き込まれます。
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
公開 qub は 時間ロックされますが、リンクでゲート化はされません。その drand ラウンドが公開されるまで読み取り不可能なまま(tlock 層は変わりません)ですが、解錠後は arweave_tx_id を持つ誰もがそれを復号できます。K が存在しないため、URL フラグメントは不要です。これは、サーバーが駆動しなければならない面のために意図的に選ばれたトレードオフです。公開通知メール、サードパーティの埋め込み、より充実した公開後 SEO はいずれも、サーバーが決して保持しないシークレットなしに機能するリンクを必要とします(§13.6)。
生成者が考慮しなければならない帰結:
- 列挙耐性なし。 公開 qub は構造上、§13.1 の列挙耐性プロパティを放棄します。リファレンスアップロードサービスはそれら(およびそれらのみ)に
Visibility: publicの永久ストレージタグを刻印するため、意図的に発見可能です。非公開 qub はそのようなタグを持たず、バイト識別不可能性を保ちます。 - 封印時に平文タイトルが露出。 §3.2 の
titleフィールドはSealedQubCbor内では平文です。ラッパー下では、ビューアーがKを供給するまで隠されています。ラッパーがなければ、解錠前の アップロードの瞬間から 永久ストレージ上で誰でも読み取れます。準拠する作成者アプリは封印時にこれを開示しなければなりません(MUST)。 - 検出は構造的。 準拠するビューアー / 埋め込みは、解析によって 2 つの形状を区別します。
OuterWrapperとして解析されるバイトはKによるアンラップ経路を取り、素のSealedQubCborとして解析されるバイトは直接受理されます。ワイヤフラグは不要であり、qub_idは可視性をバインドしません。公開で封印されても非公開で封印されても、同じコンテンツはSealedQub層でバイト的に同一です。
非公開(ラップ済み)が既定のままであり、公開は qub ごとの明示的な作成者の選択です。
14. テストベクトル
14.1 qub_id 導出
Input:
version = 0x01
content_type = 0x01
created_at = 1735689600 (2025-01-01 00:00:00 UTC)
unlock_at = 1736294400 (2025-01-08 00:00:00 UTC)
outcome_at = absent
drand_round = 4695445 (= (1736294400 - 1595431050) / 30, drand mainnet params §14.2)
body = "Hello, future." (UTF-8, 14 bytes)
title = absent
Intermediate:
body_hash = SHA3-256("Hello, future.")
= 76ab8b3f843c6ed4f2d0fd75b9f457b4
ad49dd4450f9c22723ae430e3af3211d
title_hash = [0u8; 32] (title absent — §4.2.1 sentinel)
Domain separator (10 bytes):
[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]
Preimage (108 bytes — V1.2):
domain_separator || // 10 bytes
0x01 || // version
0x01 || // content_type
0x0000000067748580 || // created_at as i64 big-endian (1735689600)
0x00000000677DC000 || // unlock_at as i64 big-endian (1736294400)
0x0000000000000000 || // outcome_at_or_zero (outcome_at absent)
0x000000000047A595 || // drand_round as u64 big-endian (4695445)
body_hash || // 32 bytes
title_hash // 32 bytes (all-zeros sentinel; title absent)
Expected output:
qub_id = SHA3-256(preimage)
= 3a9fcb31b750d985c262fada6d4f777f
d6a28be831d941d85c131f5a4bbaf8a4
実装は、この入力に対して同一の body_hash および qub_id 値を生成しなければなりません(MUST)。このテストベクトルは最初に書く単体テストとすることが望ましい(SHOULD)。上記の正規値はリファレンス実装によって計算されており、ビット単位で一致しなければなりません(MUST)。過去のプリイメージレイアウト(ローンチ前であり、これらに依存している稼働中の qub はありません): 92 バイトの V1.0 qub_id は 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0 でした。100 バイトの V1.1 qub_id(outcome_at_or_zero を折り込んだもの)は b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed でした。V1.2 は drand_round を折り込み、ドメインセパレータを QUB_ID_V2 に引き上げます。
14.2 公開ラウンドのマッピング
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calculation:
(1735689600 - 1595431050) / 30 = 4675285.0
ceil(4675285.0) = 4675285
drand_round = 4675285
14.3 正規 CBOR ラウンドトリップ
実装は、すべての有効入力について serialize(parse(serialize(qub))) == serialize(qub) を検証しなければなりません(MUST)。これは単一のベクトルではなくプロパティテストです。
14.4 PactTerms CBOR(content_type 0x03)
Input:
pact_version = 1
title = "Scooter deposit"
terms = [
{ key: "Item", value: "Honda Metropolitan scooter" },
{ key: "Price", value: "$100" },
{ key: "Deposit", value: "$10" }
]
party_a = { label: "Alice" }
party_b = { label: "Bob", contact: "bob@example.com" }
notes = absent
Canonical CBOR key order (PactTerms):
"notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)
Canonical CBOR key order (PactTerm):
"key"(4) < "value"(6)
Canonical CBOR key order (PartyIdentifier):
"label"(6) < "contact"(8)
正規 CBOR バイトと SHA3-256 body_hash はリファレンス実装によって計算されます。実装はこの入力に対してバイト単位で同一の CBOR を生成しなければなりません(MUST)。
実装はまた、すべての有効な PactTerms 入力について serialize(parse(serialize(pact))) == serialize(pact) を検証しなければなりません(MUST)(プロパティテスト)。
14.5 外側ラッパーのクロス言語ベクトル
外側ラッパー(§13)は crates/qub-core/tests/vectors/wrapper_v1.json に別の正規フィクスチャを持ちます。各ケースは (key, nonce, qub_id, sealed_cbor) のタプルを不透明な 16 進入力として固定し、特定の expected_wrapper_hex 出力をアサートします。両方のリファレンス実装が同じ JSON ファイルを消費します。
- Rust:
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors)。 - TypeScript:
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test)。
このフィクスチャは現在 3 つのケースを固定しています。
| ケース | カバレッジ |
|---|---|
basic-text-public |
最小の現実的な SealedQub 形状。オプションフィールドなし。v1.0 典型の qub の正規ラッパー形状を確立。 |
with-recipient-pubkey |
recipient_pubkey が設定された SealedQub(フェーズ 2 パス)。異なる内側 CBOR キーセット、異なる qub_id。 |
longer-body |
約 4 KiB の本文 — 内側エンベロープと外側暗号文の両方の中で複数バイトの CBOR 長さプレフィックスを行使します。 |
実装は記録された入力に対してバイト単位で同一の expected_wrapper_hex を生成しなければなりません(MUST)。フィクスチャの再生成には QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors が必要であり、意図的な形式変更のために予約されています。
15. 暗号プロファイル統治(将来)
このセクションは v1 については情報提供であり、qub の暗号プリミティブのいずれかに 2 つ目のアルゴリズムが入った最初の時点で規範となります。
15.1 現在の姿勢
プロトコル v1 はプリミティブごとに正確に 1 つのアルゴリズムをバインドします。
- 署名: ML-DSA-65(
sig_alg = 0x01、1952 バイトの公開鍵、3309 バイトの署名)および署名なし(sig_alg = 0x00)。§9.2 のレジストリは他の値を定義しておらず、v1 検証者は{0x00, 0x01}以外のすべてのsig_algを拒否しなければなりません(MUST)。将来の Ed25519 エントリが想定されています(§15.3)が、v1 では割り当てられていません。 - タイムロック: drand quicknet のみ — チェーンハッシュ、公開鍵、ジェネシス時刻、および周期は固定のネットワークパラメータであり、リファレンスの
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs)およびconfig/drand-endpoints.jsonによって保持されます。 - 外側ラッパー: AES-256-GCM v1 のみ(§13)。
検証者は現在、プリミティブごとに鍵と署名の長さをハードコーディングしています。ワイヤ形式によって公開されるアジリティ面はありません。
15.2 意図された形状
2 つ目のアルゴリズムがプロトコルに入るとき、検証者は名前付きの CryptoProfile(例: ExqubV1)向けに構成され、プリミティブごとに許可された値の正確なセット(sig_algs、drand チェーン、ラッパーバージョン、コンテンツタイプ)が列挙されます。プロファイルは検証時に固定され、帯域内で交渉されることはありません。アクティブプロファイル外の値はすべて拒否されます。
これにより、ML-DSA-87 の追加や Ed25519 の有効化が既存の検証者構成を遡及的に弱体化できないことが保証されます。v1 検証者は、v2 プロファイルが公開された後も v1 検証者のままです。
15.3 トリガー条件
次のいずれかが提案されたときに §15 を規範ステータスに昇格させます。
- 2 つ目の
sig_algバイト(Ed25519 の有効化、ML-DSA-87、または §9 レジストリへの新エントリ)。 - 本番使用での 2 つ目の drand チェーン。
- 2 つ目の外側ラッパーバージョン。
それまで §15 は、将来の PR が交渉面を一から再議論するのではなく既知のターゲットに対して着地するように移行形状を固定するプレースホルダーです。