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 |
UTF-8 文字字串,已 NFC 正規化 |
| ` | |
SHA3-256(x) |
位元組串 x 的 NIST SHA3-256 雜湊(FIPS 202) |
ceil(x) |
向上取整函式:不小於 x 的最小整數 |
| CBOR | 簡明二進位物件表示(RFC 8949) |
| 大端 | 最高有效位元組在前 |
前映像構造中的所有整數均編碼為大端定寬位元組陣列(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 序列化必須符合本配置。對於相同的邏輯結構,兩個實作必須產出相同的位元組。
3.1 編碼規則
| 規則 | 規範 |
|---|---|
| 標準 | RFC 8949 §4.2.1(核心確定性編碼要求) |
| 對映鍵排序 | 先按編碼後的位元組長度排序(短在前長在後),再按字典序排序(長度相同時按位元組逐位元組比較) |
| 整數編碼 | 最短形式:0–23 嵌入初始位元組;24–255 用 2 位元組;256–65535 用 3 位元組;以此類推。 |
| 長度編碼 | 僅使用定長。 不允許不定長陣列、對映、位元組串或文字串(禁止附加資訊 = 31)。 |
| 標籤 | 不允許 CBOR 標籤(禁止主類型 6)。 |
| 浮點數 | 不允許浮點(禁止主類型 7 中值為 0xF9–0xFB 的項)。 |
| 文字串 | UTF-8 編碼,已 NFC 正規化(Unicode Normalization Form C)。 |
| 位元組串 | 原始位元組。CBOR 層不進行 base64 編碼。 |
| 重複鍵 | 報錯拒絕。 解析器禁止靜默接受重複的對映鍵。 |
| 簡單值 | 僅允許 true(0xF5)、false(0xF4)與 null(0xF6)。 |
| 可選欄位 | 不存在的可選欄位從 CBOR 對映中完全省略(而非編碼為 null)。存在的可選欄位按排序後的鍵序加入。 |
3.2 已驗證的規範鍵序
以下鍵序是規範性的。實作必須嚴格按照此順序輸出鍵。非發布建置中除錯斷言應當驗證該順序。
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(單位元組) |
|
| 內容類型(u8,值 1) | 0x01(單位元組) |
|
| sig_alg(u8,值 0) | 0x00(單位元組) |
|
| 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 位元組。附加單個 0x00 填充位元組以達到 10 位元組對齊。實作必須嚴格使用以下 10 位元組:[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]。
outcome_at 編碼: V1.1 將原像從 92 位元組擴展為 100 位元組,以將可選欄位 outcome_at 折入此繫結。outcome_at 不存在時編碼為 8 個零位元組;協議驗證器在任何地方都拒絕 outcome_at <= 0,因此此標記不會與合法值衝突。見 §3.2(線路格式)與庫內 tasks/verdict-uplift-plan.md 中觸發此欄位的判決機制。
drand_round 編碼: V1.2 將原像從 100 位元組擴展為 108 位元組,以將 drand_round(目標 drand 輪,§4.3)折入此繫結,並將域分隔符提升為 QUB_ID_V2。這將時間鎖的輪繫結進 qub 身分:閘道無法將密文重新繫結到與所顯示 unlock_at 所隱含者不同(例如已過期)的輪。公開流程(§8)另外驗證烘焙進 tlock 密文 stanza 的輪與 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 身分失效的情況下替換倒數計時上顯示的公開前判決日期。 - 改變
drand_round(其他欄位不變)會經由原像改變qub_id。閘道無法在不讓 qub 身分失效的情況下將時間鎖密文重新繫結到不同的輪;結合 §8 公開時的 stanza 輪檢查,所顯示的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 必須位於未來。unlock_at 距 created_at 不得超過 10 年(以限制長期 drand 相依風險;UI 應當對超過 2 年的公開日期發出警告)。
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 對映頭開始。完整的結構校驗在解析時進行,而非構造時,以避免重複解析。
6. 內容類型註冊表
| 值 | 類型 | 最大主體大小 | 備註 |
|---|---|---|---|
0x00 |
保留(無效) | — | 禁止使用 |
0x01 |
純文字(UTF-8,受限 Markdown) | 50 KB 付費 / 10 KB 免費 | 渲染規則見 §10。免費 / 付費切分由上傳服務強制執行;協定層硬性上限為 50 KB。 |
0x02 |
保留(未來) | — | 為未來的內容類型保留;在 v1 中無效。查看者必須按下文規則拒絕。 |
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。 |
查看者必須以清晰的使用者可見錯誤拒絕未知內容類型。查看者禁止嘗試將未知類型作為文字渲染。
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)> }
三個對映的規範 CBOR 鍵序見 §3.2。序列化後的約定 CBOR 總大小不得超過 100 KB(與 §6 一致)。
Schema 鑑別欄位。 對 structured/v1 約定,terms 中的第一列必須為 { key: "pact_schema", value: "structured/v1" }。沒有此標記的列屬於「自訂」約定,不接受結構化校驗或 schema 感知的渲染。
凍結的確認槽位。 structured/v1 約定恰好在以下鍵下攜帶四列確認列:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
每列的 value 是按 (role, kind) 對所選定的八個凍結英文字串之一,其中 role ∈ { seller, buyer, provider, client },kind ∈ { standard, capacity }。這些字串本身是規範性協定資料——雙方的 ML-DSA-65 簽章透過 body_hash 承諾到這些精確位元組。它們不會被在地化;簽章所涵蓋的主體是語言中立的。任何措辭變更都需要新的 schema 版本(structured/v2)。
這八個字串、它們的查找方式(acknowledgement_for(role, kind))以及每條的理由由參考實作固定。符合規範的實作必須輸出位元組相同的確認值;涵蓋全部四種角色組合的黃金固件 SHA3-256 body-hash 測試可捕獲任何漂移。
查看者展示順序。 這些確認字串包含諸如 "described above" 等措辭,前提是描述 / 範圍列先於確認列渲染。查看者必須按 CBOR 順序渲染 terms 陣列;重新排序會破壞文字語義。
對方聯絡方式。 當 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(與上方註冊表列一致)。
結果列舉。 線路位元組是意圖中立的;四個分類 Right / Partial / Wrong / Unfalsifiable 涵蓋每一種帶判定意圖的結果空間。各意圖專屬的標籤(如 Right 對應「說中了」/「做到了」/「如期推出」/「經事實證實」等)是查看者端的渲染議題,會對照父 qub 的意圖解析——線路本身保持語言與意圖中立。1..=4 範圍外的值必須在解碼時拒絕。
親連結。 判定 qub 不會在其主體中攜帶對親的參照。親 qub 的 Arweave 交易 ID 在上傳時作為 Parent-Tx-Id 儲存標籤發出(§7 儲存標籤層)。如此一來,主體保持為自我評估的自包含已簽名陳述;稽核鏈(「對什麼為對?」)透過 Arweave 標籤查詢建立。
證據 URL 安全性(規範性)。 當 evidence_url 存在時,驗證者(撰寫端、線路端、Worker 邊緣)必須執行:
- 僅限 HTTPS。 字串必須以位元組序列
https://開頭。任何其他協定——http、ftp、javascript、data、file等——皆拒絕。 - 長度上限。 ≤ 2,048 位元組(瀏覽器 URL 實務上限)。
- NFC + 敵意碼點檢查。 與
title和reflection相同的規則——拒絕雙向覆寫 / 零寬 / 標籤區塊 / BOM / C0 / C1 碼點。定義與 Rustcrate::handle::contains_hostile_text_codepoint及 TSworkers/api/src/utils/unicode.ts::isHostileCodepoint一致(保持同步)。 - 無空白、無 ASCII 控制字元。 URL 任何位置出現空白 / DEL / 低於
0x20的位元組皆拒絕——封堵雙向規則未涵蓋的\n/\t注入向量。 - 非空的主機段。
https://與第一個/、?或#之間的所有內容必須非空。
伺服器端不抓取。 Worker 禁止代理、抓取或預覽該 URL。協定僅儲存字串;渲染在查看者端進行,使用 rel="nofollow noopener noreferrer" target="_blank" 並在連結文字旁可見顯示主機名稱。
反思。 創作者自行撰寫的反思文字(「有什麼改變、學到了什麼」),為選填。與 title 採用相同的 NFC + 敵意碼點驗證。空字串 / 僅含空白的輸入會在建構時收合為不存在。
Schema 版本。 v1 僅支援 verdict_version = 0x01。未來的 schema 修訂將遞增此位元組,並依 §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 是規範性必需的。參考服務在創作者選擇展現時額外附加三個可選標籤:Intent(經過白名單校驗的撰寫意圖——例如 quote、reply、commitment)、Author(創作者的 §9.3 公鑰指紋,64 字元小寫十六進位),以及 Parent-Tx-Id(回覆鏈中父 qub 的儲存交易 ID,43 字元 base64url)。
Author 標籤是按 qub 選擇加入的:參考創作者應用僅在使用者在封印時顯式啟用公開歸屬時附加它。當此開關關閉——這是預設值——不寫入 Author 標籤,且 qub 在鏈上是非歸屬的:永久儲存中沒有任何東西將該次上傳與創作者的 handle、信箱或其他 qub 關聯起來。當開關開啟時,Author 指紋經由 §9.5 的認證鏈解析為創作者所選的 @handle。回覆鏈關係與 Intent 是非身分性的。外層封裝(§13)保護內部主體不被密文相關性識別——防止採集者識別並批次解密在 drand 輪發布後呈 qub 形態的上傳。
參考服務有意不附加 App-Name、App-Version 或 Type 標籤:任何此類單值篩選都會讓 GraphQL 查詢返回整個 qub 語料庫,這與封裝的「僅主體保密」作用域不一致。
符合規範的驗證者在執行 §11 的第三方驗證時禁止依賴任何儲存標籤;主體雜湊 / 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 採用後量子的 ML-DSA-65 方案(FIPS 204)而非某個其安全性可能在 qub 的永久生命週期內退化的傳統方案的原因。
9.2 演算法註冊表
sig_alg |
方案 | 公鑰大小 | 簽章大小 |
|---|---|---|---|
0x00 |
無簽章(未簽署) | — | — |
0x01 |
ML-DSA-65(FIPS 204) | 1,952 位元組 | 3,309 位元組 |
查看者必須拒絕未知的 sig_alg 值。
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。參考實作將其作為常數 ORG_ID_PRESENT_INDIVIDUAL = 0x00 暴露於 crates/qub-core/src/signing.rs;為驗證而重建 sig_input 的查看者必須輸出相同的位元組。
簽章作用域——涵蓋與未涵蓋的內容。 sig_input 承諾到信封的四個欄位:version、qub_id、body_hash、unlock_at(外加固定的域分隔符與 org_id_present 位元組)。這四個中有三個是結構性不變量:qub_id 本身經由 §4.1 的前映像從 version、content_type、created_at、unlock_at、outcome_at、drand_round 與 body_hash 推導而來,因此對上述任一欄位的任何改動都會產生不同的 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仍是真正的身分錨——查看者必須從author_pubkey(經由 §9.5 的認證層)推導顯示身分,而非信任sender_label。 reply_to欄位同樣可以在簽章後被編輯。由於qub_id是內容定址的,攻擊者無法將reply_to指向不存在的目標,但可以默默地將回覆重新掛載到另一個已存在的 qub 上。
向終端使用者展示 sender_label 或 reply_to 的實作必須把已認證的身分(公鑰指紋、認證)作為主要身分訊號呈現,而非標籤。
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)都通過之後再執行。
9.5 身分認證
身分認證——將 author_pubkey 映射到人類可識別的身分聲明(如 qub handle、信箱地址、社交帳號或 passkey 憑證)——是查看者側的漸進增強,對簽章驗證並非必需。解析認證以得出顯示身分的查看者必須按下列優先級使用:
handle > email > social > fingerprint
指紋回退是 SHA3-256(author_pubkey) 的小寫十六進位;對任何已簽署的 qub 始終可用。查看者可以將其縮寫以供顯示——參考查看者渲染 qub: 後跟首尾各四個位元組(qub:<8 hex>…<8 hex>)。
符合規範的驗證者可在不聯絡 qub API、除永久儲存與 drand 外不使用任何網路、也不進行任何伺服器端查找的情況下,完成 §9.4 中的全部檢查。認證解析是僅在簽章驗證成功之後執行的、獨立的盡力而為步驟。
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 大約讓儲存大小翻三倍。絕對成本可忽略不計。
9.7 共同簽名者驗證(約定雙邊協議)
對於雙邊協議(content_type = 0x03),第二層簽章證明雙方對相同條款表示同意。
信封欄位:
cosigner_pubkey:共同簽名者(Party B)的 ML-DSA-65 公鑰。cosigner_signature:在與作者相同的sig_input上的簽章(§9.3)。
兩個欄位必須同時存在或同時缺失。若僅其中一個存在,查看者必須回報完整性錯誤。
驗證流程:
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 及該聯絡方式正規化後的信箱雜湊同時匹配的短期信箱驗證標記。該標記在 /api/v1/auth/verify 處於魔法連結權杖攜帶 staging_id 且已驗證地址匹配 SHA-256(normalise_email(party_b.contact)) 時寫入——其中 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 渲染與清洗
本節是安全關鍵章節。查看者使用受限的 Markdown 子集渲染文字 qub(content_type = 0x01)。
10.1 允許的元素
- 標題:
#至####(不允許#####或######) - 強調:粗體(
**)、斜體(*)、刪除線(~~) - 清單:有序(
1.)與無序(-、*) - 區塊引用(
>) - 程式碼:行內片段(```)與圍欄程式碼區塊(`````)
- 水平線(
---) - 換行(行尾兩個空格或空行)
- 段落
10.2 禁用的元素
| 元素 | 處理方式 |
|---|---|
原始 HTML(<div>、<script> 等) |
完全剝離。HTML 不會通過。 |
圖片() |
剝離。輸出中刪除圖片語法。 |
連結([text](url)) |
URL 渲染為可見的純文字。不自動建立連結。未經使用者顯式操作不可點擊。 |
| 危險 URL 協定 | javascript:、data:、vbscript:、file:——剝離。 |
| iframe、嵌入、物件 | 剝離。 |
| HTML 實體 | 僅在安全時解碼為可顯示字元。 |
10.3 實作
實作必須使用嚴格的白名單解析器,而非黑名單。推薦做法:
- 用
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)標識協定主版本。
- 查看者必須以清晰錯誤拒絕未知的主版本。
- 已知主版本可以在向前相容規則允許時容忍未知的可選欄位(不在規範鍵序中的可選欄位會被忽略)。
- 內容類型(
content_type)與簽章方案(sig_alg)按版本受控:新值僅可在新協定版本或顯式註冊表更新時引入。
12.2 版本歷程
| 版本 | 值 | 描述 |
|---|---|---|
| v1 | 0x01 |
公開文字 qub(content_type 0x01)、約定雙邊協議(0x03,structured/v1 schema,ML-DSA-65 作者 + 共同簽名者)、tlock、SHA3-256 |
12.3 向前相容
v1 查看者遇到帶有未知可選 CBOR 對映鍵(不屬於 §3.2 規範順序的鍵)的 QubEnvelope 時,應當忽略這些鍵並使用已知欄位繼續驗證。這允許未來的小幅補充(例如新元資料)在不需要主版本升級的情況下加入。
v1 查看者遇到 sig_alg = 0x01(ML-DSA-65)但缺少 ML-DSA-65 驗證支援時,應當以「存在但不可驗證的簽章」提示展示 qub 內容,而不是完全拒絕該 qub。參考實作目前會拒絕除 0x00 與 0x01 之外的任何 sig_alg 值,因為 v1 註冊表中沒有其他有效演算法——在第三個演算法被註冊之前,嚴格拒絕與軟失敗在外觀上是相同的。一旦 §9.2 接納了新條目,上述軟失敗行為就具有實質意義,參考查看者也會在那時更新為軟失敗。
12.4 外層封裝版本
§13 描述的 OuterWrapper 攜帶自己的 version 位元組,與 SealedQub.version 及 QubEnvelope.version 獨立。兩個版本空間各自演進:未來的後量子安全對稱替代方案會升級封裝位元組而不觸及內部協定版本,而未來的協定層新增(例如新的信封欄位)會升級內部版本而不觸及封裝位元組。
OUTER_WRAPPER_VERSION_* |
值 | 演算法 | 狀態 |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM,12 位元組隨機數,16 位元組認證標籤,AAD 繫結到 qub_id |
v1 預設 |
| — | 0x02–0xFF |
保留 | 未來 |
查看者必須以清晰錯誤拒絕未知封裝版本。在出現具體遷移驅動因素(例如 NIST 指引傾向不同 AEAD)之前,協定有意保持封裝版本空間狹窄;在引入演算法的同一修訂中將分配 0x02 槽位。
13. 外層加密封裝
13.1 理由
協定各層(QubEnvelope → tlock → SealedQub)使已封印 qub 變為時間鎖定:主體在 unlock_at 與 drand 輪簽章發布之前不可讀。然而,在解鎖後,輪簽章是公開的,且 SealedQub 的規範 CBOR 形態可被識別,因此索引過永久儲存交易的採集者可批次解密整個 qub 語料庫。
外層加密封裝透過在規範 SealedQubCbor 與上傳到永久儲存的位元組之間插入一層附加的對稱 AEAD 來關閉該管道。256 位元金鑰 K 僅存在於傳遞連結的 URL 片段以及使用者裝置上;瀏覽器不會將 URL 片段傳送至伺服器,因此 qub.social、每個儲存閘道以及它們前面的每個 CDN 在觀測意義上對 K 都是盲的。永久儲存中的每個 qub 因此都是一段不透明密文,沒有創作者選擇分享的 URL 就無法復原其明文。
淨效應:
- 預設免疫列舉。 永久儲存中的封裝位元組在位元組層面與任意密文不可區分。「GraphQL 查詢出 qub 形態的上傳,使用公開 drand 簽章批次解密」的採集策略不會在明文處終止。
- 加密粉碎式隱私姿態。 qub.social 字面意義上無法解密自己的語料庫。傳票觸及密文,而非明文。
- 兩級保密階梯。 預設 = 連結受控存取(本節)。接收者加密的私有 qub(保留的第二階段功能,尚未規範)在其之上作為第二級疊加。
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。qub_id必須等於解封後所復原 SealedQub 的qub_id欄位。解封步驟並不直接強制這一點(AEAD AAD 繫結使位元組層級的竄改不可能),但解鎖層透過遞移關係進行檢查:若創作者封裝一個內部qub_id與封裝qub_id不匹配的SealedQubCbor,§8 第 11 步會失敗。nonce必須為 96 位元(12 位元組),由 CSPRNG 為每次封裝操作新生成。在相同金鑰下重用隨機數會帶來允許復原明文的 AEAD 隨機數重用攻擊;生產者必須將(key、nonce)對視為一次性。ciphertext是 AES-256-GCM 的輸出:密文位元組與 16 位元組認證標籤的拼接。ciphertext.len() == SealedQubCbor.len() + 16恰好成立。
CBOR 編碼。 按 §3 規範 CBOR,使用相同的鍵排序規則(先按編碼位元組長度升序,再按字典序)。四個鍵為:
| 鍵 | 編碼位元組 | 順序 |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
因此 OuterWrapper CBOR 的首位元組是 4 項對映的定長對映頭(0xA4)。
13.4 AAD 與 qub_id 的繫結
封裝將 qub_id 作為 AEAD 附加認證資料繫結。這是抵禦以下三類攻擊的承重結構性防禦:
| 攻擊 | 防禦 |
|---|---|
在封裝中將密文移動到不同 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 失敗塌縮為單一錯誤形態。
13.6 金鑰材料與分發
封裝金鑰 K 是由 CSPRNG 為每個 qub 生成的 256 位元均勻隨機值。參考實作的來源為:
- WASM 創作者:
getrandom(在wasm_js後端下使用 WebCrypto)。 - Worker 伺服器端封印路由:
crypto.getRandomValues。
分發:K 必須編碼為 URL 安全 base64(RFC 4648 §5,無填充)並作為片段分量附加到傳遞連結:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
符合規範的瀏覽器不會將片段傳送至任何伺服器。在使用者裝置之外持久化完整傳遞連結(包含片段)的恢復管道——例如伺服器端歷程索引、可選的郵件自動傳送——是對預設加密粉碎姿態的明確折衷,必須由使用者顯式同意。
片段遺失。 若使用者遺失 URL 片段且沒有恢復管道,qub 將不可讀。這是設計中的承重折衷,且必須在封印時向使用者揭露。MVP 透過明確的「儲存此 URL」文案以及對選擇啟用的使用者提供的已驗證信箱恢復管道,加強封印時的揭露。
13.7 本節範圍之外
- 作者簽章(§9)保持不變:簽章在內部
QubEnvelope中計算,並在解封 → tlock 解密 → CBOR 解析後復原。 - 接收者加密的私有 qub(保留的第二階段功能,尚未規範)作為第二級保密層在此封裝之上組合;兩級可同時啟用。
- 約定(§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 的人都能將其解密——不需要 URL 片段,因為並無 K。對於必須由伺服器驅動的介面而言,這是經過權衡的有意取捨:公開通知郵件、第三方嵌入,以及更豐富的解鎖後 SEO,全都需要一個無需伺服器永遠不持有之機密即可運作的連結(§13.6)。
生產者必須考量的後果:
- 無列舉免疫。 公開 qub 在構造上即放棄了 §13.1 的列舉免疫特性。參考上傳服務會在其上(且僅在其上)標記一個
Visibility: public永久儲存標籤,使它們有意可被發現;私有 qub 不帶此標籤,並保留其位元組層面不可區分性。 - 封印時暴露明文標題。 §3.2 的
title欄位在SealedQubCbor內部是明文。在封裝之下,它在查看者提供K之前都是隱藏的;沒有封裝時,它自上傳那一刻起——即在解鎖之前——便在永久儲存上對全世界可讀。符合規範的創作者應用程式必須在封印時揭露這一點。 - 偵測是結構性的。 符合規範的查看者/嵌入透過解析來區分這兩種形態:可解析為
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 值。該測試向量應當作為撰寫的第一個單元測試。上述規範值由參考實作計算,必須逐位匹配。歷史原像配置(發佈前 — 沒有任何已上線的 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)。這是一項性質測試,而非單一向量。
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。
實作還必須驗證對所有有效 PactTerms 輸入都滿足 serialize(parse(serialize(pact))) == serialize(pact)(性質測試)。
14.5 外層封裝跨語言向量
外層封裝(§13)的規範固件位於 crates/qub-core/tests/vectors/wrapper_v1.json。每個用例將一個 (key, nonce, qub_id, sealed_cbor) 元組固定為不透明十六進位輸入,並斷言一個特定的 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)。
該固件當前固定三個用例:
| 用例 | 涵蓋範圍 |
|---|---|
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。重新生成該固件需要 QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors,且僅用於刻意的格式變更。
15. 加密配置治理(未來)
本節對 v1 而言是參考性的,且在 qub 任一加密原語中首次進入第二個演算法時變為規範性。
15.1 當前姿態
協定 v1 對每種原語恰好繫結一個演算法:
- 簽章:ML-DSA-65(
sig_alg = 0x01;1952 位元組公鑰,3309 位元組簽章)與未簽署(sig_alg = 0x00)。§9.2 註冊表未定義其他任何值;v1 驗證者必須拒絕{0x00, 0x01}之外的每個sig_alg。未來預期會有一個 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 目標形態
當第二個演算法進入協定時,驗證者將以命名化的 CryptoProfile(例如 ExqubV1)進行配置,列出每種原語所允許的精確取值集合——sig_algs、drand 鏈、封裝版本、內容類型。該配置在驗證時固定,決不在帶內協商。任何不在活動配置內的取值都將被拒絕。
這保證新增 ML-DSA-87 或啟用 Ed25519 不能追溯性地削弱已有的驗證者配置:v1 驗證者在 v2 配置發布之後仍然是 v1 驗證者。
15.3 觸發條件
當出現以下任一情況時,將 §15 提升為規範性狀態:
- 第二個
sig_alg位元組(Ed25519 啟用、ML-DSA-87,或 §9 註冊表中的任何新條目)。 - 生產中使用的第二條 drand 鏈。
- 第二個外層封裝版本。
在此之前,§15 是一個占位章節,用於固定遷移形態,使未來的 PR 朝既定目標落地,而非從零開始重新爭論協商介面。