Specifikace protokolu qub
qub je protokol pro kryptografické časové závazky: systém pro zapečetění slov k budoucímu datu a prokázání, až toto datum nastane, přesně toho, co bylo řečeno a kdy.
Fungování zajišťují tři primitiva. drand je decentralizovaný maják náhodnosti — datum odhalení je vynutitelné fyzikou, nikoli dobrou vůlí kterékoli strany. Trvalé veřejné úložiště je veřejné úložiště odolné proti manipulaci — jakmile je qub zapečetěn, žádná strana jej nemůže upravit ani smazat. ML-DSA-65 je postkvantový digitální podpis — každý qub je vázán na pár klíčů, jehož tajná část nikdy neopustí zařízení autora.
Společně tato primitiva vytvářejí výrok, který je časově uzamčený, prokazatelně neporušený a přiřaditelný — potvrzení, jehož hodnota roste s tím, jak se zlepšuje schopnost světa falšovat minulost.
Zbytek tohoto dokumentu je normativní specifikace nutná pro interoperabilní implementace.
Specifikace protokolu qub
| Pole | Hodnota |
|---|---|
| Verze | 1.0 (verze protokolu 0x01, verze vnějšího obalu 0x01) |
| Datum | 2026-05-01 |
| Stav | Návrh |
| Zkontrolováno do | 2026-05-01 |
Tento dokument je normativní specifikací protokolu pro systém časových závazků qub. Definuje datové struktury, pravidla serializace, odvozovací vzorce a ověřovací postupy nutné pro interoperabilní implementace.
Rozsah: vrstva protokolu je záměrně jazykově neutrální — tělo qubu je neprůhledný prostý text / markdown / bajty paktu a vykreslení s ohledem na lokalizaci je odpovědností prohlížeče (webová aplikace qub.social, iframe <qub-embed>, klienti MCP atd.).
1. Notace a konvence
| Notace | Význam |
|---|---|
u8, u64, i64 |
Celá čísla bez znaménka / se znaménkem o specifikované bitové šířce |
[u8; N] |
Pole bajtů s pevnou délkou N bajtů |
Vec<u8> |
Pole bajtů s proměnnou délkou |
Option<T> |
Hodnota typu T, nebo nepřítomná |
String |
Textový řetězec UTF-8, normalizovaný do NFC |
| ` | |
SHA3-256(x) |
Hash NIST SHA3-256 řetězce bajtů x (FIPS 202) |
ceil(x) |
Funkce horní celá část: nejmenší celé číslo ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Nejvýznamnější bajt první |
Všechna celá čísla v konstrukcích předobrazů jsou kódována jako big-endian pole bajtů s pevnou šířkou (i64 → 8 bajtů, u8 → 1 bajt), pokud není uvedeno jinak.
Všechny časové značky jsou sekundy Unixu v UTC.
2. Datové struktury
2.1 ComposeQub (stav tvůrce v paměti)
Neserializuje se do CBOR. Nezapisuje se do trvalého úložiště. Lokální pro aplikaci tvůrce.
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 (dešifrovaná datová část)
Serializuje se pomocí kanonického CBOR (§3). Šifrováno uvnitř SealedQub. Toto je struktura, která po dešifrování prokazuje integritu obsahu.
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
}
Základní stav (nepodepsaný textový qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, všechna pole Option nepřítomna.
Další konfigurace v1: content_type = 0x03 (tělo paktu, viz §6.1); sig_alg = 0x01 (ML-DSA-65) s přítomnými author_signature a author_pubkey (viz §9.3); cosigner_pubkey a cosigner_signature přítomné společně pro spolupodepsané pakty (viz §9.7); reply_to nastavené na qub_id nadřazeného qubu pro quby v řetězcích odpovědí (důsledky pro rozsah podpisu viz §9.3).
2.3 SealedQub (kanonický formát na drátě)
Serializuje se pomocí kanonického CBOR (§3). Zapisuje se do trvalého úložiště. Toto je artefakt na řetězci.
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 (stav aplikace prohlížeče)
Neserializuje se do CBOR. Lokální pro aplikaci prohlížeče. Konstruováno po úspěšném dešifrování a ověření.
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 — přenášeno z QubEnvelope.outcome_at / SealedQub.outcome_at; pohání blok sledování verdiktu na stránce odhalení (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. Kanonický profil CBOR
Veškerá serializace SealedQub a QubEnvelope MUSÍ odpovídat tomuto profilu. Dvě implementace dostávající stejnou logickou strukturu MUSÍ produkovat identické bajty.
3.1 Pravidla kódování
| Pravidlo | Specifikace |
|---|---|
| Standard | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Řazení klíčů mapy | Seřazeno nejprve podle délky zakódovaných bajtů (kratší před delším), poté lexikograficky (bajt po bajtu pro kódování stejné délky) |
| Kódování celých čísel | Nejkratší forma: 0–23 v úvodním bajtu; 24–255 ve 2 bajtech; 256–65535 ve 3 bajtech; atd. |
| Kódování délky | Pouze definitivní délky. Žádná pole, mapy, řetězce bajtů ani textové řetězce s neurčitou délkou (doplňková informace = 31 je zakázána). |
| Značky | Žádné značky CBOR (hlavní typ 6 je zakázán). |
| Plovoucí desetinná čárka | Žádné plovoucí hodnoty (hodnoty hlavního typu 7 0xF9–0xFB jsou zakázány). |
| Textové řetězce | Kódováno v UTF-8, normalizováno do NFC (Unicode Normalization Form C). |
| Řetězce bajtů | Surové bajty. Žádné kódování base64 na vrstvě CBOR. |
| Duplicitní klíče | Odmítnout s chybou. Parsery NESMÍ tiše akceptovat duplicitní klíče mapy. |
| Neznámé klíče | Odmítnout s chybou. Parsery NESMÍ tolerovat klíče mapy mimo kanonickou sadu klíčů daného typu — dva odlišné kanonické bajtové řetězce se nikdy nesmí dekódovat na tutéž hodnotu (encode(decode(x)) == x) a u podepsaných dat by klíč navíc byl skrytým obsahem, ke kterému se zavazují oba podpisy. Vývoj schématu probíhá přes version, nikdy přes klíče navíc. |
| Jednoduché hodnoty | Povoleny jsou pouze true (0xF5), false (0xF4) a null (0xF6). |
| Volitelná pole | Nepřítomná volitelná pole jsou z mapy CBOR zcela vynechána (nekódují se jako null). Přítomná volitelná pole jsou zahrnuta v seřazeném pořadí klíčů. |
3.2 Ověřená kanonická pořadí klíčů
Tato pořadí klíčů jsou normativní. Implementace MUSÍ vydávat klíče přesně v tomto pořadí. Ladicí kontrolní příkazy BY MĚLY ověřovat řazení v neuvolňovacích sestaveních.
QubEnvelope (verze 0x01, nepodepsaný, všechna volitelná pole nepřítomna):
"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)
Odvození pořadí klíčů QubEnvelope: každý klíč je textový řetězec CBOR. Zakódovaná délka = 1 bajt hlavičky + délka řetězce (pro řetězce kratší než 24 bajtů). Řadí se nejprve podle celkové zakódované délky, poté lexikograficky pro klíče stejné délky.
SealedQub (verze 0x01, veřejný, bez příjemce):
"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 (tělo paktu, 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 (řádek pole terms):
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier (mapa party_a / party_b):
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 Referenční tabulka kódování bajtů
| Typ | Kódování CBOR | Příklad |
|---|---|---|
| Hash SHA3-256 (32 bajtů) | 0x58 0x20 + 32 bajtů |
body_hash, qub_id |
| Časové značky (i64) | Hlavní typ 0 (kladný) nebo 1 (záporný), nejkratší kódování | sekundy Unixu |
| Verze (u8, hodnota 1) | 0x01 (jeden bajt) |
|
| Typ obsahu (u8, hodnota 1) | 0x01 (jeden bajt) |
|
| sig_alg (u8, hodnota 0) | 0x00 (jeden bajt) |
|
| Podpis ML-DSA-65 (3 309 bajtů) | 0x59 0x0C 0xED + 3 309 bajtů |
author_signature, cosigner_signature |
| Veřejný klíč ML-DSA-65 (1 952 bajtů) | 0x59 0x07 0xA0 + 1 952 bajtů |
author_pubkey, cosigner_pubkey |
4. Normativní odvození
4.1 qub_id
qub_id jednoznačně identifikuje qub a váže QubEnvelope na SealedQub. Je deterministicky odvozen z obsahu obálky.
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
Kódování oddělovače domény: Řetězec "QUB_ID_V2" má 9 bajtů ASCII. Připojuje se jeden výplňový bajt 0x00, aby se dosáhlo 10 bajtů pro zarovnání. Implementace MUSÍ použít přesně těchto 10 bajtů: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
Kódování outcome_at: V1.1 rozšířila předobraz z 92 na 100 bajtů, aby do vazby zahrnula volitelné pole outcome_at. Nepřítomné outcome_at se kóduje jako 8 nulových bajtů; validátory protokolu všude odmítají outcome_at <= 0, takže tato sentinelová hodnota nemůže kolidovat s legitimní hodnotou. Viz §3.2 (formát na drátě) a stromový dokument tasks/verdict-uplift-plan.md pro mechaniku verdiktu, která toto pole motivuje.
Kódování drand_round: V1.2 rozšířila předobraz ze 100 na 108 bajtů, aby do vazby zahrnula drand_round (cílové kolo drand, §4.3), a zvýšila oddělovač domény na QUB_ID_V2. Tím se kolo časového zámku váže do identity qubu: brána nemůže přepojit šifrový text na jiné (např. již uplynulé) kolo, než jaké implikuje zobrazené unlock_at. Postup odemčení (§8) navíc ověřuje, že kolo zapečené do stanzy šifrového textu tlock se shoduje s unlock_round(unlock_at), takže zobrazený čas odemčení je prokazatelně tím kolem, které řídí dešifrování.
Vlastnosti:
- Změna kteréhokoli pole v QubEnvelope (tělo, časové značky, typ obsahu, verze) vytvoří jiný qub_id.
- qub_id je vypočítán před šifrováním. QubEnvelope i SealedQub nesou stejný qub_id. Prohlížeč po dešifrování ověří, že se shodují.
- qub_id nezávisí na
sender_label,author_signatureaniauthor_pubkey. To znamená, že stejný obsah zapečetěný ve stejnou dobu vytvoří stejný qub_id bez ohledu na to, kdo jej podepíše. - Změna
titlev SealedQub (při jinak neměnných hodnotách) změníqub_idprostřednictvímtitle_hash. Brána tedy nemůže vyměnit prostý titulek zobrazený na odpočtu, aniž by tím zneplatnila identitu qubu. - Změna
outcome_atv SealedQub (při jinak neměnných hodnotách) změníqub_idprostřednictvím předobrazu. Brána nemůže před odhalením vyměnit datum verdiktu zobrazené na odpočtu, aniž by tím zneplatnila identitu qubu. - Změna
drand_round(při jinak neměnných hodnotách) změníqub_idprostřednictvím předobrazu. Brána nemůže přepojit šifrový text časového zámku na jiné kolo, aniž by tím zneplatnila identitu qubu; ve spojení s kontrolou kola stanzy při odemčení dle §8 je zobrazenéunlock_attím kolem, které skutečně řídí dešifrování.
4.2 body_hash
body_hash = SHA3-256(body)
Kde body je surová datová část obsahu typu Vec<u8>. U textových qubů je to tělo qubu kódované v UTF-8.
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
Kde title je volitelný prostý titulek vyvedený na odpočtu prohlížeče před odhalením (viz §3.2). Normalizace NFC běží v okamžiku výpočtu hashe, takže digest je stabilní napříč vizuálně rovnocennými sekvencemi kódových bodů. Návěstí samých nul je vyhrazeno pro nepřítomný případ; prázdný řetězec je na hranici kanonického CBOR odmítnut jako nekanonické kódování „nepřítomné“ (kanonické kódování pole zcela vynechává).
4.3 Mapování na kolo odemčení
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Parametr | Zdroj | Příklad |
|---|---|---|
unlock_at |
Uživatelem zvolené sekundy Unixu UTC | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
Informace o řetězci drand (genesis_time) |
1595431050 |
chain_period_seconds |
Informace o řetězci drand (period) |
30 |
Operace ceil() vybírá první kolo drand, jehož čas odhalení je ≥ unlock_at. Tím se zajistí, že qub se nestane dešifrovatelným před zvoleným časem odemčení.
Hraniční případ: pokud je (unlock_at - chain_genesis_time) přesně dělitelné chain_period_seconds, výsledkem je přesně to kolo — qub se odemkne přesně v čase odhalení tohoto kola.
Validace: unlock_at MUSÍ být v okamžiku zapečetění v budoucnosti. unlock_at NESMÍ být více než 10 let od created_at (kvůli omezení rizika dlouhodobé závislosti na drand; uživatelské rozhraní BY MĚLO varovat u dat odemčení přesahujících 2 roky).
5. Newtypy formátu na drátě
Newtypy formátu na drátě poskytují bezpečnost v době kompilace proti záměně bajtů CBOR s JSON, surovým prostým textem nebo jiným kódováním bajtů.
| Typ | Obsahuje | Vyrábí | Spotřebovává |
|---|---|---|---|
SealedQubCbor |
Kanonický CBOR SealedQub | serialize_sealed_qub() |
Nahrání do trvalého úložiště, načtení prohlížečem |
QubEnvelopeCbor |
Kanonický CBOR QubEnvelope | serialize_qub_envelope() |
Vstup šifrování tlock, výstup dešifrování tlock |
5.1 Pravidla konstrukce
// 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 Validace při konstrukci
from_encoded() BY MĚLO ověřit, že vstup začíná platnou hlavičkou mapy CBOR. Plná strukturální validace probíhá v době parsování, nikoli v době konstrukce, aby se předešlo dvojímu parsování.
6. Registr typů obsahu
| Hodnota | Typ | Max. velikost těla | Poznámky |
|---|---|---|---|
0x00 |
Vyhrazeno (neplatné) | — | NESMÍ se používat |
0x01 |
Prostý text (UTF-8, omezený Markdown) | 50 KB placené / 10 KB zdarma | Pravidla vykreslování viz §10. Rozdělení zdarma / placené vynucuje služba nahrávání; pevný strop na vrstvě protokolu je 50 KB. |
0x02 |
Vyhrazeno (budoucí) | — | Alokováno pro budoucí typ obsahu; v v1 neplatné. Prohlížeče MUSÍ odmítnout dle pravidla níže. |
0x03 |
Pakt (dvoustranná dohoda, tělo CBOR) | 100 KB | Tělo je kanonický CBOR PactTerms (§6.1). Podepisování spolupodepisujícím dle §9.7. |
0x04 |
Verdikt (sebehodnocení tvůrce, tělo CBOR) | 8 KB | Tělo je kanonický CBOR VerdictBody (§6.2). Vydáváno pouze záměrem verdict na straně systému. Vztah k rodiči je na štítku trvalého úložiště Parent-Tx-Id, nikoli v těle. Viz verdict-uplift-plan §3.4. |
Prohlížeče MUSÍ odmítnout neznámé typy obsahu se srozumitelnou chybou viditelnou pro uživatele. Prohlížeče NESMÍ pokoušet vykreslovat neznámé typy jako text.
6.1 Tělo paktu (content_type = 0x03)
Tělo paktu je kanonické kódování CBOR hodnoty PactTerms:
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)> }
Kanonická pořadí klíčů CBOR pro všechny tři mapy jsou uvedena v §3.2. Celkový serializovaný CBOR paktu NESMÍ překročit 100 KB (odpovídá §6).
Diskriminátor schématu. První řádek v terms u paktu structured/v1 MUSÍ být { key: "pact_schema", value: "structured/v1" }. Řádky bez tohoto označení jsou „vlastní“ pakty a nedostávají žádnou strukturovanou validaci ani vykreslování s ohledem na schéma.
Zmrazené sloty potvrzení. Pakty structured/v1 nesou přesně čtyři řádky potvrzení pod těmito klíči:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
value pro každý z nich je jeden z osmi zmrazených anglických řetězců zvolených podle dvojice (role, kind), kde role ∈ { seller, buyer, provider, client } a kind ∈ { standard, capacity }. Samotné řetězce jsou normativní data protokolu — podpisy ML-DSA-65 obou stran se zavazují k přesným bajtům prostřednictvím body_hash. NEJSOU lokalizovány; podepsané tělo je jazykově neutrální. Jakákoli změna formulace vyžaduje novou verzi schématu (structured/v2).
Osm řetězců, jejich vyhledání (acknowledgement_for(role, kind)) a zdůvodnění každého z nich jsou ukotveny referenční implementací. Vyhovující implementace MUSÍ vydávat bajtově identické hodnoty potvrzení; testy hashe těla SHA3-256 z referenčních (golden) fixtur pokrývající všechny čtyři kombinace rolí zachytí jakýkoli posun.
Pořadí zobrazení v prohlížeči. Řetězce potvrzení obsahují fráze jako „popsáno výše“, které předpokládají, že řádky popisu / rozsahu se vykreslí před potvrzeními. Prohlížeče MUSÍ vykreslit pole terms v pořadí CBOR; přeskupení naruší prozaickou sémantiku.
Kontakt protistrany. Když je contact strany B platná e-mailová adresa, služba nahrávání qubů automaticky odešle v okamžiku přípravy e-mailovou pozvánku k posouzení / spolupodpisu a váže případný spolupodpis na ověření téže adresy (§9.7). Pakty, jejichž kontakt strany B chybí, lze stále spolupodepsat, ale pouze kanálem mimo pásmo — služba odmítá požadavky na spolupodpis, které nemohou vytvořit odpovídající 15minutové návěstí ověření e-mailu.
6.2 Tělo verdiktu (content_type = 0x04)
Tělo verdiktu je kanonické kódování CBOR hodnoty VerdictBody:
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
}
Kanonické pořadí klíčů 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)
Celkový serializovaný CBOR verdiktu NESMÍ překročit 8 KB (odpovídá řádku registru výše).
Výčet outcome. Bajt na drátě je nezávislý na záměru; čtyři kategorie Right / Partial / Wrong / Unfalsifiable pokrývají prostor výsledků každého záměru nesoucího verdikt. Štítky specifické pro daný záměr („Trefa“ / „Dodrženo“ / „Vydáno“ / „Potvrzeno“ pro Right atd.) jsou věcí vykreslování na straně prohlížeče, řešenou podle záměru nadřazeného qubu — drát zůstává jazykově i záměrově neutrální. Hodnoty mimo 1..=4 MUSÍ být při dekódování odmítnuty.
Vazba na rodiče. Tělo qubu s verdiktem NENESE odkaz na rodiče. Identifikátor transakce trvalého úložiště nadřazeného qubu se vydává jako štítek úložiště Parent-Tx-Id v okamžiku nahrání (§7, vrstva štítků úložiště). Díky tomu zůstává tělo samostatným podepsaným prohlášením o sebehodnocení; auditní řetězec („pravdu o čem?“) se ustavuje vyhledáním štítku v trvalém úložišti.
Bezpečnost URL důkazu (normativní). Když je evidence_url přítomna, validátory (na straně skládání, na drátě, na hraně Workeru) MUSÍ vynutit:
- Pouze HTTPS. Řetězec MUSÍ začínat bajtovou sekvencí
https://. Jakékoli jiné schéma —http,ftp,javascript,data,fileapod. — je odmítnuto. - Omezení délky. ≤ 2 048 bajtů (praktický limit URL v prohlížečích).
- Kontrola NFC a nepřátelských kódových bodů. Stejné pravidlo jako u
titleareflection— kódové body bidi-override / nulové šířky / tag-block / BOM / C0 / C1 jsou odmítnuty. Definice odpovídá Rustcrate::handle::contains_hostile_text_codepointa TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(udržujte v synchronizaci). - Žádné bílé znaky, žádné řídicí znaky ASCII. Bílé znaky / DEL / bajty pod
0x20kdekoli v URL jsou odmítnuty — uzavírá to vektor injekce\n/\t, který pravidlo bidi nepokrývá. - Neprázdný úsek hostitele. Vše mezi
https://a prvním/,?nebo#MUSÍ být neprázdné.
Žádné stahování na straně serveru. Worker NESMÍ URL proxovat, stahovat ani předběžně zobrazovat. Protokol ukládá řetězec; vykreslení probíhá na straně prohlížeče s rel="nofollow noopener noreferrer" target="_blank" a viditelným hostitelem zobrazeným vedle textu odkazu.
Reflexe. Volitelný text reflexe psaný tvůrcem („co se změnilo, co jste se naučili“). Stejná validace NFC a nepřátelských kódových bodů jako u title. Prázdný vstup nebo vstup složený jen z bílých znaků se při konstrukci redukuje na nepřítomný.
Verze schématu. v1 podporuje pouze verdict_version = 0x01. Budoucí revize schématu zvýší tento bajt a budou nasazeny společně s novou verzí protokolu dle §12.
7. Protokol zapečetění
Kompletní sekvence zapečetění. Každý krok je normativní.
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.
Vrstva značek úložiště (mimo pásmo). Služba nahrávání qubů připojuje vedle obalené datové části záměrně malou sadu transakčních značek úložiště. Content-Type=application/octet-stream je normativně vyžadováno. Referenční služba navíc připojuje tři volitelné značky, když se je tvůrce rozhodne vyvést na povrch: Intent (záměr sestavení validovaný proti povolovacímu seznamu — např. quote, reply, commitment), Author (otisk veřejného klíče tvůrce dle §9.3 jako 64znakový hexadecimální řetězec malými písmeny) a Parent-Tx-Id (id transakce úložiště nadřazeného qubu pro řetězce odpovědí, 43znakový base64url).
Značka Author je přihlašovaná pro každý qub zvlášť: referenční aplikace tvůrce ji připojí pouze tehdy, když uživatel v okamžiku zapečetění výslovně povolí veřejné přiřazení autorství. Když je přepínač vypnutý — což je výchozí stav — žádná značka Author se nezapisuje a qub je na řetězci bez přiřazeného autora: nic v trvalém úložišti nespojuje nahrání s identifikátorem (handle), e-mailem ani jinými quby tvůrce. Když je přepínač zapnutý, otisk Author se rozřeší na zvolený @handle tvůrce prostřednictvím atestačního řetězce dle §9.5. Vztahy řetězců odpovědí a Intent neidentifikují. Vnější obal (§13) chrání vnitřní tělo před korelací šifrového textu — brání tomu, aby sklízeč rozpoznal a hromadně dešifroval nahrání ve tvaru qubu poté, co se zveřejní jejich kolo drand.
Referenční služba záměrně NEPŘIPOJUJE značky App-Name, App-Version ani Type: jakýkoli takový filtr o jediné hodnotě by na dotaz GraphQL vrátil celý korpus qubů, což je v rozporu s rozsahem důvěrnosti obalu omezeným pouze na tělo.
Vyhovující ověřovatel NESMÍ při ověřování třetí stranou dle §11 záviset na žádné značce úložiště; hash těla / qub_id / podpis se zavazují pouze k vnitřnímu CBOR, nikdy k sadě značek.
8. Protokol odemčení
Kompletní sekvence odemčení. Každý krok je normativní.
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. Podepisování autorství
9.1 Zdůvodnění
Každý qub se ukládá do trvalého úložiště. Podpisy autorství musí zůstat neomezeně nepadělatelné, a proto verze 1.0 používá postkvantové schéma ML-DSA-65 (FIPS 204) namísto klasického schématu, jehož bezpečnost se může v rámci trvalé životnosti qubu zhoršit.
9.2 Registr algoritmů
sig_alg |
Schéma | Velikost klíče | Velikost podpisu |
|---|---|---|---|
0x00 |
Bez podpisu (nepodepsaný) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1 952 bajtů | 3 309 bajtů |
Prohlížeče MUSÍ odmítnout neznámé hodnoty sig_alg.
9.3 Konstrukce podepsaného předobrazu
Existovaly dvě verze předobrazu. Všechny podpisy MUSÍ používat V2 a ověřovatelé MUSÍ přijímat pouze V2. Starší předobraz V1 (níže zdokumentovaný pro historickou referenci) byl během migrace na V2 přijímán jako záložní varianta pouze pro ověření; tato záložní varianta byla vyřazena a podpis pouze podle V1 je nyní odmítán.
V2 (aktuální — vytvářený veškerým novým podepisováním autora i oběma podpisy toku přípravy / spolupodpisu paktu):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V2" || // 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.x
sender_label_hash || // [u8; 32]: SHA3-256(NFC(sender_label)),
// or 32 zero bytes when absent
reply_to_or_zero // [u8; 32]: parent qub_id, or 32 zero
// bytes when absent
)
// Total preimage: 155 bytes → 32-byte hash
signature = Sign(author_secret_key, sig_input)
sender_label_hash se řídí stejnou konvencí sentinelu nepřítomnosti jako title_hash (§4.2.1): 32 nulových bajtů není platný výstup SHA3-256, takže „nepřítomný“ nikdy nemůže kolidovat s přítomným štítkem. Všechna pole mají pevnou šířku, takže předobraz je jednoznačný i bez délkových prefixů.
V1 (starší — VYŘAZENÝ; již se nevytváří a při ověření se již nepřijímá):
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
Předobraz V1 vynechával sender_label a reply_to. Během migrace na V2 byl přijímán jako záložní varianta pouze pro ověření; tato záložní varianta byla od té doby vyřazena — ověřovatelé MUSÍ přijímat pouze předobraz V2. Definice je zde ponechána pro historickou referenci a k vysvětlení níže uvedeného oddělovače domény. Podpis, který se ověří pouze proti V1, MUSÍ být považován za selhání ověření.
Oddělovače domény: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" mají každý 17 bajtů ASCII ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Bez výplně. Odlišný oddělovač zajišťuje doménové oddělení obou konstrukcí, takže podpis nad jedním předobrazem se nikdy nemůže ověřit jako druhý.
Bajt org_id_present: bajt následující po unlock_at MUSÍ být 0x00. Referenční implementace jej vystavuje jako konstantu ORG_ID_PRESENT_INDIVIDUAL = 0x00 v crates/qub-core/src/signing.rs; prohlížeče rekonstruující sig_input pro ověření MUSÍ vydat stejný bajt.
Rozsah podpisu — co je a není pokryto. sig_input verze V2 se přímo zavazuje k polím version, qub_id, body_hash, unlock_at, sender_label a reply_to (plus pevný oddělovač domény a bajt org_id_present). qub_id je sám odvozen z version, content_type, created_at, unlock_at, outcome_at, drand_round a body_hash prostřednictvím předobrazu dle §4.1, takže jakákoli změna těchto polí vytvoří jiný qub_id a tranzitivně zneplatní podpis. Přímo ověřená plocha je tedy:
| Pole | Ověřeno podpisem | Jak |
|---|---|---|
version |
✓ | Přímý vstup do sig_input |
qub_id |
✓ | Přímý vstup |
body_hash |
✓ | Přímý vstup |
unlock_at |
✓ | Přímý vstup |
sender_label |
✓ | Přímý vstup přes sender_label_hash (předobraz V2 — jediná přijímaná forma) |
reply_to |
✓ | Přímý vstup přes reply_to_or_zero (předobraz V2 — jediná přijímaná forma) |
content_type |
✓ | Tranzitivně, přes předobraz qub_id |
created_at |
✓ | Tranzitivně, přes předobraz qub_id |
outcome_at |
✓ | Tranzitivně, přes předobraz qub_id |
drand_round |
✓ | Tranzitivně, přes předobraz qub_id (V1.2) |
body |
✓ | Tranzitivně, přes body_hash = SHA3-256(body) |
author_pubkey |
— (implicitně) | Klíč, který ověřil podpis, je z definice autor |
cosigner_pubkey / cosigner_signature |
— | Nezávisle podepsáno nad stejným sig_input (viz §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Vnější pole SealedQub, nikoli uvnitř obálky — pokryta vlastními strukturálními invarianty (konzistence kola / řetězce), nikoli však podpisem autora. (drand_round je nyní vázán tranzitivně prostřednictvím předobrazu qub_id — viz výše.) |
Proč je V2 jediný přijímaný předobraz.
- Za vyřazeného předobrazu V1 mohla strana se zápisovým přístupem k uloženým bajtům vyměnit
sender_label(„Alice“ → „Mallory“) nebo přepojitreply_tok jinému nadřazenému qubu — a znovu zašifrovat po uplynutí kola — bez zneplatnění podpisu autora, protože ani jedno z těchto polí nebylo v podepsaném předobrazu. V2 pokrývá obě, takže jakákoli změna kteréhokoli z těchto polí překlopí ověření na „selhalo“. Protože ověřovatelé nyní přijímají pouze V2, je tato záměna uzavřena pro každý podpis: podpis, který neváže ani jedno z polí (tj. ověří se pouze proti V1), je rovnou odmítnut, místo aby na něj byla ověřovací úroveň snížena. author_pubkeyuvnitř obálky zůstává pravým kotvícím bodem identity — prohlížeče MUSÍ odvozovat zobrazovanou identitu zauthor_pubkey(prostřednictvím atestační vrstvy dle §9.5), nikoli důvěřovatsender_label.
Implementace, které koncovým uživatelům zobrazují sender_label nebo reply_to, MUSÍ vyvést na povrch ověřenou identitu (otisk veřejného klíče, atestaci) jako primární signál identity, nikoli štítek.
9.4 Ověřovací postup
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 (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
only accepted form — the legacy V1 fallback is retired (§9.3), so a
signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
Ověření podpisu je nejnákladnější operace (zejména ML-DSA-65). BY MĚLO být provedeno až poté, co projdou všechny levnější kontroly (hash, qub_id, unlock_at).
9.5 Atestace identity
Atestace identity — mapování author_pubkey na člověkem rozpoznatelná tvrzení o identitě, jako je identifikátor (handle) qubu, e-mailová adresa, identifikátor na sociální síti nebo přihlašovací údaj passkey — jsou progresivní vylepšení na straně prohlížeče a nejsou vyžadovány pro ověření podpisu. Prohlížeče, které rozřeší atestace na zobrazovanou identitu, MUSÍ uplatnit přednost:
handle > email > social > fingerprint
Záložní otisk je hexadecimální zápis malými písmeny SHA3-256(author_pubkey); je vždy k dispozici pro jakýkoli podepsaný qub. Prohlížeče MOHOU jej pro zobrazení zkrátit — referenční prohlížeč vykresluje qub: následované prvními a posledními čtyřmi bajty (qub:<8 hex>…<8 hex>).
Vyhovující ověřovatel může dokončit každou kontrolu v §9.4 bez kontaktování API qubu, bez jakékoli sítě kromě trvalého úložiště a drand a bez jakéhokoli vyhledávání na straně serveru. Rozřešení atestace je samostatný krok podle nejlepší snahy prováděný pouze poté, co ověření podpisu uspělo.
9.6 Dopad na velikost
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Podpis | 64 bajtů | 3 309 bajtů |
| Veřejný klíč | 32 bajtů | 1 952 bajtů |
| Celkem na qub | 96 bajtů | 5 261 bajtů |
| Rozdíl nákladů na úložiště (při ~5 $/MB) | ~0,0005 $ | ~0,026 $ |
U textového qubu o 500–2 000 bajtech zhruba ztrojnásobí ML-DSA-65 uloženou velikost. Absolutní náklady jsou zanedbatelné.
9.7 Ověření spolupodepisujícího (dvoustranné dohody paktů)
U dvoustranných dohod (content_type = 0x03) druhá vrstva podpisu prokazuje, že obě strany souhlasily se stejnými podmínkami.
Pole obálky:
cosigner_pubkey: veřejný klíč ML-DSA-65 spolupodepisujícího (strana B).cosigner_signature: podpis nad stejnýmsig_inputjako u autora (§9.3).
Obě pole MUSÍ být přítomna společně, nebo obě nepřítomna. Je-li přítomno přesně jedno, prohlížeče MUSÍ ohlásit chybu integrity.
Ověřovací postup:
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 (V2 only — the
legacy V1 fallback is retired; all pact clients produce V2 signatures).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."
Vlastnosti:
- Spolupodepisující podepisuje identický
sig_inputjako autor — obě strany se zavazují ke stejnémuqub_id,body_hashaunlock_at(a v rámci V2 i ke stejnémusender_label_hashareply_to_or_zero). - Aby mohl spolupodepisující rekonstruovat předobraz V2 bez přístupu k surovým bajtům obálky, vynucuje služba přípravy v okamžiku přípravy, že
sender_labelobálky paktu se rovnápact_terms.party_a.labela žereply_toje nepřítomné. Obojí platí pro každý pakt referenčního klienta; obálky, které to porušují, jsou při přípravě odmítnuty. - Odvození
qub_id(§4.1) NEZAHRNUJE pole spolupodepisujícího. Přidání spolupodepisujícího do existující obálky neměníqub_id. - Pakt může být pouze podepsaný autorem (jednostranný závazek), pouze spolupodepisujícím (neobvyklé), nebo obojí (úplný dvoustranný důkaz).
Brána vázání na e-mail (provozní). Když připravený pakt nese e-mailový kontakt strany B (§6.1), služba nahrávání qubů MUSÍ odmítnout požadavek na spolupodpis, pokud neexistuje krátkodobé návěstí ověření e-mailu odpovídající jak id přípravy, tak hashi normalizovaného e-mailu tohoto kontaktu. Návěstí zapisuje /api/v1/auth/verify, když token přihlašovacího odkazu nese staging_id a ověřená adresa odpovídá SHA-256(normalise_email(party_b.contact)) — kde normalise_email(addr) zachovává velikost písmen lokální části a převádí na malá písmena pouze část domény (dle RFC 5321 §2.3.11) a SHA-256 je zde hash NIST FIPS 180-4 (odlišný od SHA3-256 použitého v odvozeních dle §4) — a vyprší 900 sekund (15 minut) po vydání. Toto je provozní brána proti zosobnění, NIKOLI součást důkazu qubu na řetězci — ověřovatel třetí strany přehrávající §11 potřebuje pouze trvalé úložiště a drand, bez jakéhokoli vyhledávání na straně serveru. Návěstí existuje pouze na straně serveru a nikdy není součástí podepsaného těla.
Dopad na velikost (autor ML-DSA-65 + spolupodepisující):
| Komponenta | Velikost |
|---|---|
| Podpis autora | 3 309 bajtů |
| Veřejný klíč autora | 1 952 bajtů |
| Podpis spolupodepisujícího | 3 309 bajtů |
| Veřejný klíč spolupodepisujícího | 1 952 bajtů |
| Celková kryptografická režie | 10 522 bajtů |
| Rozdíl nákladů na úložiště | ~0,05 $ |
10. Vykreslování a sanitizace Markdownu
Tato část je kriticky důležitá pro bezpečnost. Prohlížeč vykresluje textové quby (content_type = 0x01) pomocí omezené podmnožiny Markdownu.
10.1 Povolené prvky
- Nadpisy:
#až####(žádné#####ani######) - Zvýraznění: tučné (
**), kurzíva (*), přeškrtnuté (~~) - Seznamy: číslované (
1.) a nečíslované (-,*) - Citace bloku (
>) - Kód: vložené úseky (```) a oplocené bloky (`````)
- Vodorovné čáry (
---) - Zalomení řádku (dvě koncové mezery nebo prázdný řádek)
- Odstavce
10.2 Zakázané prvky
| Prvek | Zpracování |
|---|---|
Surové HTML (<div>, <script> atd.) |
Zcela odstraněno. Žádné HTML neprojde. |
Obrázky () |
Odstraněno. Syntaxe obrázku je z výstupu odstraněna. |
Odkazy ([text](url)) |
URL vykreslena jako viditelný prostý text. Bez automatického propojení. Bez možnosti kliknutí bez výslovné akce uživatele. |
| Nebezpečná schémata URL | javascript:, data:, vbscript:, file: — odstraněna. |
| Iframy, vložené prvky, objekty | Odstraněno. |
| Entity HTML | Dekódovány na zobrazované znaky pouze, je-li to bezpečné. |
10.3 Implementace
Implementace MUSÍ použít přísný parser s povolovacím seznamem, nikoli zakazovací seznam. Doporučený přístup:
- Parsovat Markdown pomocí
pulldown-cmark(nebo ekvivalentu). - Projít AST a zahodit jakýkoli uzel, který není v povolovacím seznamu (§10.1).
- U uzlů odkazů: vydat URL jako viditelný text, nikoli jako klikatelný prvek
<a>. - Převést filtrovaný AST na typovanou mezireprezentaci (např. výčet
MarkdownNodepouze s bezpečnými variantami). Surové HTML je v této IR strukturálně nereprezentovatelné. - Vykreslit z typované IR do cílové vrstvy zobrazení (např. reaktivní komponenty zobrazení, uzly DOM). V žádném okamžiku žádné zřetězení řetězců HTML ani
innerHTML.
Přístupy se zakazovacím seznamem jsou křehké, protože nová rozšíření Markdownu nebo zvláštnosti parseru mohou zavést nefiltrované prvky. Přístup s typovaným AST činí XSS strukturálně nemožným — neexistuje varianta, která by mohla nést libovolné HTML.
10.4 Limity velikosti a struktury
- Maximální hloubka vykreslovaného nadpisu:
####(H4).#####a hlubší se vykreslují jako tučný text. - Žádný limit na počet odstavců (omezením jsou limity velikosti těla v §6).
- Oplocené bloky kódu: žádné zvýrazňování syntaxe v MVP. Vykresleno jako neproporcionální předformátovaný text.
11. Ověření třetí stranou
Kterákoli třetí strana může ověřit veřejný qub bez spolupráce qubu. Ověřovací postup:
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.
Co ověření prokazuje:
| Důkaz | Co stanovuje |
|---|---|
| Závazek | Šifrový text existoval k časové značce bloku úložiště. |
| Integrita | Tělo prostého textu odpovídá zavázanému hashi a nebylo pozměněno. |
| Časování | Obsah byl nečitelný až do kola drand, které odpovídá zvolenému času odemčení (s výhradou bezpečnostních předpokladů tlock a drand). |
Co ověření NEPROKAZUJE:
| Nedůkaz | Proč |
|---|---|
| Autorství | sender_label je dekorativní. Bez sig_alg ≥ 0x01 mohl tento obsah zapečetit kdokoli. |
| Záměr | Samotný qub prokazuje obsah a časování, nikoli to, co tvůrce subjektivně mínil. |
| Časování před událostí | Zařazení bloku úložiště může za skutečným nahráním zaostávat o minuty. Časová značka závazku je čas bloku, nikoli okamžik, kdy uživatel stiskl „zapečetit“. |
12. Verzování
12.1 Verze protokolu
Pole version (u8) jak v SealedQub, tak v QubEnvelope identifikuje hlavní verzi protokolu.
- Prohlížeče MUSÍ odmítnout neznámé hlavní verze se srozumitelnou chybou.
- V rámci známé hlavní verze MUSÍ dekodéry odmítnout neznámé klíče mapy (§3.1) — vývoj schématu probíhá zavedením nové
version, nikoli přidáváním klíčů, které by stávající dekodéry přeskočily. (Dřívější revize této specifikace dovolovaly tolerovat neznámá volitelná pole; toto ustanovení bylo staženo — činiloencode(decode(x))neinjektivním a otevíralo vektor skrytého podepsaného obsahu v tělech paktů.) - Typy obsahu (
content_type) a podpisová schémata (sig_alg) jsou vázány na verzi: nové hodnoty smí být zavedeny pouze spolu s novou verzí protokolu nebo výslovnou aktualizací registru.
12.2 Historie verzí
| Verze | Hodnota | Popis |
|---|---|---|
| v1 | 0x01 |
Veřejné textové quby (content_type 0x01), dvoustranné dohody paktů (0x03, schéma structured/v1, autor + spolupodepisující ML-DSA-65), tlock, SHA3-256 |
12.3 Dopředná kompatibilita
Prohlížeč v1 narazivší na QubEnvelope s neznámými klíči mapy CBOR (klíče nezahrnuté v kanonickém pořadí dle §3.2) jej MUSÍ odmítnout s chybou dekódování (§3.1). Dopředná kompatibilita stojí na poli version, nikoli na toleranci klíčů: budoucí přídavky — i drobná metadata — se dodávají pod novou hodnotou version, kterou prohlížeč v1 odmítne se srozumitelnou chybou „novější protokol“, místo aby tiše zahazoval obsah, ke kterému se zavazují podpisy.
Prohlížeč v1 narazivší na sig_alg = 0x01 (ML-DSA-65), avšak postrádající podporu ověřování ML-DSA-65, BY MĚL zobrazit obsah qubu s upozorněním „podpis přítomen, ale neověřitelný“, nikoli qub zcela odmítnout. Referenční implementace dnes odmítá každou hodnotu sig_alg jinou než 0x00 a 0x01, protože registr v1 neobsahuje žádný jiný platný algoritmus — přísné odmítnutí a měkké selhání jsou pozorovatelně totožné, dokud není registrován třetí algoritmus. Výše popsané chování měkkého selhání se stane nosným, jakmile §9.2 přijme novou položku, a referenční prohlížeč bude v tom okamžiku aktualizován tak, aby selhával měkce.
12.4 Verze vnějšího obalu
OuterWrapper popsaný v §13 nese vlastní bajt version, nezávislý na SealedQub.version a QubEnvelope.version. Oba prostory verzí se vyvíjejí samostatně: budoucí postkvantově bezpečná symetrická náhrada zvýší bajt obalu, aniž by se dotkla verze vnitřního protokolu, a budoucí přídavek na vrstvě protokolu (např. nové pole obálky) zvýší vnitřní verzi, aniž by se dotkl bajtu obalu.
OUTER_WRAPPER_VERSION_* |
Hodnota | Algoritmus | Stav |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM s 12bajtovým nonce, 16bajtovou autentizační značkou, AAD vázaným na qub_id |
výchozí v1 |
| — | 0x02–0xFF |
Vyhrazeno | Budoucí |
Prohlížeče MUSÍ odmítnout neznámé verze obalu se srozumitelnou chybou. Protokol záměrně udržuje prostor verzí obalu úzký, dokud se neobjeví konkrétní hnací důvod migrace (např. pokyny NIST upřednostňující jiný AEAD); slot 0x02 bude přidělen ve stejné revizi, která zavede algoritmus.
13. Vnější šifrovací obal
13.1 Zdůvodnění
Vrstvy protokolu (QubEnvelope → tlock → SealedQub) činí zapečetěný qub časově uzamčeným: tělo je nečitelné až do unlock_at a do zveřejnění podpisu kola drand. Po odemčení je však podpis kola veřejný a kanonický tvar CBOR SealedQub je rozpoznatelný, takže sklízeč, který zaindexoval transakce trvalého úložiště, by mohl hromadně dešifrovat celý korpus qubů.
Vnější šifrovací obal tento kanál uzavírá vložením dodatečné symetrické vrstvy AEAD mezi kanonický SealedQubCbor a bajty zapsané do trvalého úložiště. 256bitový klíč K žije pouze ve fragmentu URL doručovací URL a na zařízeních uživatelů; prohlížeče nepřenášejí fragmenty URL na servery, takže qub.social, každá brána úložiště a každá CDN před nimi jsou pozorovatelně slepé vůči K. Každý qub v trvalém úložišti je tedy neprůhledný šifrový text, jehož prostý text je neobnovitelný bez URL, kterou se tvůrce rozhodl sdílet.
Čistý efekt:
- Imunita vůči výčtu ve výchozím nastavení. Obalené bajty v trvalém úložišti jsou bajtově nerozlišitelné od libovolného šifrového textu. Strategie sklízeče „dotaz GraphQL na nahrání ve tvaru qubu, hromadné dešifrování veřejnými podpisy drand“ neskončí prostým textem.
- Postoj soukromí typu kryptografické skartace. qub.social doslova nemůže dešifrovat vlastní korpus. Soudní příkazy dosáhnou na šifrový text, nikoli na prostý text.
- Dvoustupňový žebřík důvěrnosti. Výchozí = přístup řízený odkazem (tato část). Soukromé quby šifrované pro příjemce (rezervovaná funkce 2. fáze, zatím nespecifikovaná) se nakládají navrch jako druhý stupeň.
13.2 Vrstvení
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)
Zapečetění a odemčení na vrstvě protokolu (§7, §8) jsou pod hranicí obalu beze změny; obal se připojuje na místě volání seal() a odpojuje na místě volání unlock().
13.3 Datová struktura 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
}
Invarianty polí.
versionMUSÍ být roven0x01pro bajty obalu v1.0.qub_idMUSÍ být roven poliqub_idSealedQub obnoveného po rozbalení. Krok rozbalení toto přímo nevynucuje (vázání AEAD AAD činí manipulaci na úrovni bajtů nemožnou), ale vrstva odemčení kontroluje vztah tranzitivně: pokud tvůrce obalíSealedQubCbor, jehož vnitřníqub_idneodpovídáqub_idobalu, §8 krok 11 selže.nonceMUSÍ mít 96 bitů (12 bajtů), generovaný čerstvě pomocí CSPRNG pro každou operaci obalení. Opětovné použití nonce pod stejným klíčem umožňuje útoky na opětovné použití nonce AEAD, které obnoví prostý text; producenti MUSÍ zacházet s dvojicemi (key,nonce) jako s jednorázovými.ciphertextje výstup AES-256-GCM: bajty šifrového textu zřetězené se 16bajtovou autentizační značkou.ciphertext.len() == SealedQubCbor.len() + 16přesně.
Kódování CBOR. Kanonický CBOR dle §3, se stejným pravidlem řazení klíčů (seřazeno vzestupně podle zakódované délky bajtů, poté lexikograficky). Čtyři klíče jsou:
| Klíč | Zakódované bajty | Pořadí |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
Prvním bajtem CBOR OuterWrapper je tedy hlavička mapy s definitivní délkou pro mapu se 4 položkami (0xA4).
13.4 Vázání AAD na qub_id
Obal váže qub_id jako dodatečná autentizovaná data AEAD. To je nosná strukturální obrana proti třem třídám útoků:
| Útok | Obrana |
|---|---|
Přesunout šifrový text pod jiné pole qub_id v obalu |
Neshoda AAD → autentizace AEAD selže |
| Smíchat fragment URL qubu A s bajty trvalého úložiště qubu B | Neshoda AAD → autentizace AEAD selže |
Manipulovat s polem qub_id obalu po nahrání |
Neshoda AAD → autentizace AEAD selže |
Nesení qub_id v prostém textu obalu významně neoslabuje imunitu vůči výčtu — qub_id je sám hash SHA3-256 předobrazu dle §4.1 bez obnovitelného předobrazu z digestu a vyčíslovatel, který už sklidil bajty obalu, se z viditelného qub_id nedozví nic, co by nemohl odvodit ze samotné existence nahrání.
13.5 Algoritmy obalení a rozbalení
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
Sloučení režimů selhání. Špatný K, špatný nonce, neshoda AAD a pozměněný šifrový text — všechny produkují stejnou chybu DECRYPT_FAILED. Toto je záměrná vlastnost AEAD: rozlišování režimu selhání by vytvořilo postranní kanál, který by vzdálený útočník mohl zkoumat odesíláním poškozených obalů a měřením času odezvy. Referenční implementace MUSÍ sloučit všechna selhání AEAD do jediného tvaru chyby.
13.6 Klíčový materiál a distribuce
Obalovací klíč K je 256bitová rovnoměrně náhodná hodnota generovaná pro každý qub pomocí CSPRNG. Referenční implementace jej získávají z:
- Tvůrce WASM:
getrandom(WebCrypto pod backendemwasm_js). - Volající serverového API zapečetění: jeho lokální CSPRNG; volající dodává a uchovává
Kjakowrapper_key_b64url. Worker používáKv paměti pro obal, ale NESMÍ jej trvale uchovávat. Díky tomu může idempotentní opakování obnovit redigovanou odpověď pomocí oprávnění uchovaného volajícím, namísto spoléhání na jednorázové tajemství generované serverem.
Distribuce: K MUSÍ být kódován jako base64 bezpečné pro URL (RFC 4648 §5, bez výplně) a připojen k doručovací URL jako komponenta fragmentu:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
Fragment není vyhovujícím prohlížečem nikdy přenášen na žádný server. Obnovovací kanály (index historie na straně serveru, přihlašované automatické zasílání e-mailem), které trvale uchovávají úplnou doručovací URL — včetně fragmentu — mimo zařízení uživatele, jsou výslovným kompromisem oproti výchozímu postoji kryptografické skartace a MUSÍ být podmíněny výslovným souhlasem uživatele.
Ztráta fragmentu. Pokud uživatel ztratí fragment URL a nemá obnovovací kanál, qub je nečitelný. Toto je nosný kompromis návrhu a MUSÍ být uživateli sdělen v okamžiku zapečetění. MVP posiluje sdělení v okamžiku zapečetění výslovným textem „uložte si tuto URL“ a obnovovacím kanálem s ověřeným e-mailem pro uživatele, kteří se přihlásí.
13.7 Mimo rozsah této části
- Podepisování autorství (§9) je beze změny: podpisy se počítají uvnitř vnitřního
QubEnvelopea obnovují se po rozbalení → dešifrování tlock → parsování CBOR. - Soukromé quby šifrované pro příjemce (rezervovaná funkce 2. fáze, zatím nespecifikovaná) se skládají navrch tohoto obalu jako druhý stupeň důvěrnosti; oba stupně mohou být aktivní současně.
- Pakty (§6, content_type
0x03) se obalují přesně jako textové quby; obal je bajtově slepý vůči vnitřnímu typu obsahu.
13.8 Veřejné quby (vynechání obalu)
Vnější obal je na doručovací vrstvě volitelný. Tvůrce může zapečetit qub jako veřejný, v kterémžto případě se kanonický SealedQubCbor zapíše do trvalého úložiště přímo, bez vrstvy OuterWrapper a bez klíče K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
Veřejný qub je časově uzamčený, ale není uzamčený odkazem: zůstává nečitelný, dokud se nezveřejní jeho kolo drand (vrstva tlock je beze změny), ale po odemčení jej může kdokoli, kdo má arweave_tx_id, dešifrovat — žádný fragment URL není potřeba, protože žádné K neexistuje. To je záměrný kompromis pro plochy, které musí řídit server: e-maily s oznámením o odhalení, vložení (embedy) třetích stran i bohatší SEO po odhalení — všechny potřebují odkaz, který funguje bez tajemství, jež server nikdy nedrží (§13.6).
Důsledky, s nimiž producent MUSÍ počítat:
- Žádná imunita vůči výčtu. Veřejné quby se z konstrukce vzdávají vlastnosti imunity vůči výčtu z §13.1. Referenční služba nahrávání jim (a pouze jim) razí značku trvalého úložiště
Visibility: public, takže jsou záměrně dohledatelné; soukromé quby žádnou takovou značku nenesou a zachovávají si svou bajtovou nerozlišitelnost. - Prostý text titulku odhalen v okamžiku zapečetění. Pole
titlez §3.2 je uvnitřSealedQubCborprostým textem. Pod obalem je skryto, dokud divák nedodáK; bez obalu je v trvalém úložišti čitelné pro všechny od okamžiku nahrání, ještě před odemčením. Vyhovující aplikace tvůrce to MUSÍ sdělit v okamžiku zapečetění. - Detekce je strukturální. Vyhovující divák/embed rozliší oba tvary parsováním: bajty, které se naparsují jako
OuterWrapper, jdou cestou rozbalení pomocíK; bajty, které se naparsují jako holýSealedQubCbor, jsou přijaty přímo. Žádný příznak na drátě není potřeba aqub_idneváže viditelnost — tentýž obsah je na vrstvěSealedQubbajtově identický, ať je zapečetěn jako veřejný, či soukromý.
Soukromý (obalený) zůstává výchozím; veřejný je výslovnou volbou tvůrce u jednotlivého qubu.
14. Testovací vektory
14.1 Odvození 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
Implementace MUSÍ pro tento vstup produkovat identické hodnoty body_hash a qub_id. Tento testovací vektor BY MĚL být prvním napsaným jednotkovým testem. Výše uvedené kanonické hodnoty byly vypočítány referenční implementací a MUSÍ se shodovat bit po bitu. Historická uspořádání předobrazu (před spuštěním — žádné živé quby na nich nebyly závislé): 92bajtový qub_id verze V1.0 byl 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; 100bajtový qub_id verze V1.1 (po zahrnutí outcome_at_or_zero) byl b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 zahrnuje drand_round a zvyšuje oddělovač domény na QUB_ID_V2.
14.2 Mapování na kolo odemčení
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 Obousměrná konverze kanonického CBOR
Implementace MUSÍ ověřit, že serialize(parse(serialize(qub))) == serialize(qub) pro všechny platné vstupy. Toto je vlastnostní test, nikoli jediný vektor.
14.4 CBOR PactTerms (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)
Kanonické bajty CBOR a body_hash SHA3-256 jsou vypočítány referenční implementací. Implementace MUSÍ pro tento vstup produkovat bajtově identický CBOR.
Implementace MUSÍ rovněž ověřit, že serialize(parse(serialize(pact))) == serialize(pact) pro všechny platné vstupy PactTerms (vlastnostní test).
14.5 Mezijazykové vektory vnějšího obalu
Vnější obal (§13) má samostatnou kanonickou fixturu v crates/qub-core/tests/vectors/wrapper_v1.json. Každý případ fixuje n-tici (key, nonce, qub_id, sealed_cbor) jako neprůhledné hexadecimální vstupy a vyžaduje konkrétní výstup expected_wrapper_hex. Obě referenční implementace spotřebovávají stejný soubor 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).
Fixtura aktuálně ukotvuje tři případy:
| Případ | Pokrytí |
|---|---|
basic-text-public |
Nejmenší realistický tvar SealedQub; žádná volitelná pole. Ustanovuje kanonický tvar obalu pro qub typický pro v1.0. |
with-recipient-pubkey |
SealedQub s nastaveným recipient_pubkey (trasa fáze 2). Odlišná sada klíčů vnitřního CBOR, odlišný qub_id. |
longer-body |
Tělo ~4 KiB — procvičuje vícebajtové délkové prefixy CBOR uvnitř jak vnitřní obálky, tak vnějšího šifrového textu. |
Implementace MUSÍ pro zaznamenané vstupy produkovat bajtově identický expected_wrapper_hex. Opětovné generování fixtury vyžaduje QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors a je vyhrazeno pro záměrné změny formátu.
15. Správa kryptografického profilu (budoucí)
Tato část je pro v1 informativní a stává se normativní v okamžiku, kdy poprvé vstoupí druhý algoritmus do kteréhokoli z kryptografických primitiv qubu.
15.1 Současný postoj
Protokol v1 váže přesně jeden algoritmus na primitivum:
- Podpis: ML-DSA-65 (
sig_alg = 0x01; 1952bajtový veřejný klíč, 3309bajtový podpis) a nepodepsaný (sig_alg = 0x00). Registr §9.2 nedefinuje žádné jiné hodnoty; ověřovatel v1 MUSÍ odmítnout každýsig_algmimo{0x00, 0x01}. Budoucí položka Ed25519 se předpokládá (§15.3), ale ve v1 není alokována. - Časový zámek: pouze drand quicknet — hash řetězce, veřejný klíč, čas geneze a perioda jsou pevné síťové parametry nesené referenčním
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) aconfig/drand-endpoints.json. - Vnější obal: pouze AES-256-GCM v1 (§13).
Ověřovatelé v současnosti pevně zakódovávají délky klíčů a podpisů pro každé primitivum. Formát na drátě nevystavuje žádnou plochu pružnosti.
15.2 Zamýšlený tvar
Když do protokolu vstoupí druhý algoritmus, ověřovatel bude nakonfigurován pro pojmenovaný CryptoProfile (např. ExqubV1) vyjmenovávající přesnou sadu povolených hodnot pro každé primitivum — sig_algs, řetězce drand, verze obalu, typy obsahu. Profil je pevně daný v okamžiku ověření, nikdy se nesjednává v pásmu. Jakákoli hodnota mimo aktivní profil je odmítnuta.
To zaručuje, že přidání ML-DSA-87 nebo aktivace Ed25519 nemůže zpětně oslabit existující konfigurace ověřovatelů: ověřovatel v1 zůstává ověřovatelem v1 i poté, co je zveřejněn profil v2.
15.3 Spouštěcí podmínky
Povýšte §15 na normativní stav, když je navržena kterákoli z následujících možností:
- Druhý bajt
sig_alg(aktivace Ed25519, ML-DSA-87 nebo jakákoli nová položka v registru §9). - Druhý řetězec drand v produkčním použití.
- Druhá verze vnějšího obalu.
Do té doby je §15 zástupný text, který fixuje tvar migrace, aby budoucí PR dopadaly proti známému cíli, místo aby od základu znovu řešily plochu sjednávání.
16. Log transparentnosti a stupně trvanlivosti (návrh — revize dokončena)
Stav. Tato část je specifikací návrhu. Formáty na drátě, hashování a model důvěry níže jsou normativní pro implementaci, ale dosud nebyl nasazen žádný kód logu transparentnosti. Externí revize W5 je dokončena: §16.15 zaznamenává vyřešená rozhodnutí a závazná spouštěcí omezení, která z ní vzešla. Implementace může pod těmito omezeními pokračovat. §16 zůstává dopředu hledící ve stejném smyslu jako §15 — fixuje cíl, aby implementace dopadla proti ustálenému návrhu, místo aby model důvěry znovu odvozovala při kontrole kódu. Je striktně aditivní — každý existující qub si ponechává svou individuální transakci Arweave a formát na drátě
SealedQub/QubEnvelopese nemění.
16.1 Zdůvodnění a stupně trvanlivosti
Dnes trvanlivost qubu i jeho časový závazek spočívají na jediné transakci Arweave pro každý qub (§11). To svazuje latenci zapečetění s finalitou Arweave, činí z nahrání každého qubu produktový strop nákladů (ARWEAVE_DAILY_CEILING) a neposkytuje žádné řazení napříč quby odolné proti manipulaci. Log transparentnosti přidává pod tento jediný stupeň a kolem něj dvě vrstvy:
| Stupeň | Název | Záruka | Kdy |
|---|---|---|---|
| T1 | Synchronní potvrzení s prioritou R2 | Spodní hranice trvanlivosti — zapečetěné bajty jsou zapsány do trvalého úložiště dříve, než se zapečetění vrátí (< 300 ms p95). |
Každý qub, synchronně (§16.10). |
| T2 | Dávkové zařazení do logu transparentnosti | Univerzální závazek pouze pro připojování, odolný proti manipulaci, + úplné řazení, zakotvený v Arweave. | Každý qub, odložené + dávkové (§16.5–16.7). |
| T3 | Trvalost v Arweave pro každý qub | Individuální transakce Arweave pro daný qub. | Placený upsell a záložní řešení při nedostupnosti Arweave (§16.8). |
T2 činí z Arweave pro každý qub volbu (T3), nikoli jedinou cestu trvanlivosti. ARWEAVE_DAILY_CEILING je vyřazen jako produktový strop a degradován na pojistku (circuit breaker) pouze na vyhrazené kotvicí peněžence (§16.7); zapečetění uživatelů nejsou nikdy odmítnuta za jeho překročení.
Upřímnost ohledně trvanlivosti (vyřešeno — §16.15 Q6). Trvanlivost se neregreuje: zápis T1 do R2 je synchronní a jednorázový, takže qub na bezplatném stupni, který si nekoupil T3, je plně trvanlivý v okamžiku, kdy se zapečetění vrátí. Co hrubne, je prokazatelný čas závazku jako horní mez: u bezplatného qubu se jím stává čas bloku kotvy, nikoli čas bloku transakce každého qubu. Při nízkém objemu — reálný stav při raném startu a mimo špičku — je plná denní kadence typickou spodní hranicí, nikoli vzácným okrajovým případem. Produktové rámování je tudíž horní mez s žádnou závaznou číselnou latencí — „zapečetěno a trvanlivé nyní; nezávislé veřejné časové razítko je přidáno u příští kotvy logu (typicky denně)“ — a důkaz závazku s přesností na hodinu je placenou vlastností T3, zveřejněnou na ploše porovnání stupňů a v podmínkách (§16.11, §16.15 Q6). Jakákoli časová mez je pouze interním SLO, nikdy marketingovým SLA.
16.2 Struktura LogLeaf (dva závazné tvary)
Položka logu je LogLeaf, zakódovaná jako ručně psaný kanonický CBOR pod profilem §3.1 (definitivní délka, žádné tagy, žádné floaty, celá čísla v nejkratším tvaru, NFC text, volitelná pole vynechána, jsou-li nepřítomna, klíče seřazené vzestupně podle zakódované délky bajtů a poté bytově). Kanonická pojistka §3.1 parsovat → znovu zakódovat → porovnat se uplatňuje na kódovací cestě před hashováním (nejen při dekódování), takže se dvě implementace nemohou neshodnout na bajtech listu kvůli rozdílu v šířce celého čísla nebo pořadí klíčů. Všechna celá čísla jsou u8 / u64 / i64; všechny otisky jsou 32bajtové bajtové řetězce (bstr[32]). Uložené id transakce Arweave je surový 32bajtový otisk SHA-256 nesený jako bstr[32], nikdy textový řetězec base64url (odpovídá §3.3).
List má dva tvary vybírané bajtem kind, protože na výchozí cestě nahrávání je Worker slepý vůči bajtům: POST /api/v1/upload přijímá pouze qub_id a unlock_at jako nedůvěryhodná klientská tvrzení — body_hash, drand_round, created_at a drand_chain_version jsou všechna zapečetěna uvnitř vnějšího obalu dle §13, jehož klíč Worker nikdy nedrží. Pouze cesta serverového zapečetění (POST /api/v1/seal) odvozuje body_hash / drand_round z prostého textu. Jediný tvar listu nesoucí body_hash + drand_round by se tudíž u většiny reálných qubů zavazoval k hodnotám, které operátor nikdy neověřil. Rozdělení udržuje každou zavázanou hodnotu upřímnou:
| Klíč | Zak. délka | Typ | Přítomnost | Význam |
|---|---|---|---|---|
seq |
4 | u64 |
povinné | Globální index listu od nuly; pozice, k níž se zavazuje důkaz o zařazení. |
kind |
5 | u8 |
povinné | 0x01 atestovaný (serverové zapečetění) nebo 0x02 tvrzený (klientské zapečetění / nahrání slepé vůči bajtům). |
ref |
4 | bstr[32] |
povinné | Referenční id listu. Atestovaný → surový qub_id. Tvrzený → zaslepené id SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
6 | bstr[32] |
povinné | Adresa obsahu SHA3-256(stored_bytes) — jediná vazba na obsah, kterou Worker dokáže vždy upřímně vypočítat, na obou cestách. |
unlock_at |
10 | i64 |
povinné | Zkopírováno (atestovaný) nebo tvrzeno (tvrzený); validováno > 0 dříve, než vstoupí do listu. |
received_at |
12 | i64 |
povinné | Reálný čas Workeru při potvrzení R2. Nedůkazní (tvrzeno operátorem; §16.6). Přítomno pro sebepopis, nikdy důkaz. Validováno > 0. |
body_hash |
10 | bstr[32] |
pouze kind=0x01 |
Vynecháno u 0x02 — Worker jej dle §13 nemá. |
drand_round |
12 | u64 |
pouze kind=0x01 |
Vynecháno u 0x02. |
List kind=0x02 se záměrně nezavazuje ani k body_hash, ani k drand_round: atestuje závazek a řazení neprůhledného šifrového textu na adrese obsahu chash, tvrdíc qub_id a unlock_at — nikoli jeho prostý text či kolo. Větve prostého textu / kola pro tvrzený qub pocházejí z existujícího ověření balíčku .qub dle §11, nikoli z logu (§16.11). drand_chain_version není v listu (na výchozí cestě je uvnitř obalu); granularita řetězce sídlí na kotvě (§16.7). Disciplína kodéru: odmítnout ref či chash plné nul a odmítnout nekladné unlock_at / received_at, zrcadlíc pojistku sentinelu outcome_at > 0 v cbor.rs.
16.2.1 Zaslepení soukromého qubu
Log se nesmí stát výčtovou věštírnou, které má vnější obal dle §13 zabránit (§13.1). U soukromého (obaleného) qubu se asserted list zavazuje k zaslepenému identifikátoru SHA3-256(qub_id ‖ log_blind_secret), kde log_blind_secret je serverem držené tajemství, a vynechává body_hash. Třetí strana nemůže takový list svázat s konkrétním qub_id; držitel qubu, který má doručovací URL a tedy qub_id, může zaslepení přepočítat, aby potvrdil své vlastní zařazení. Veřejný qub (již vyčíslitelný, již nesoucí značku Arweave Visibility: public dle §13.8) se zavazuje k surovému qub_id. Toto je jediné místo, kde samostatná ověřitelnost záměrně ustupuje nosné invariantě soukromí; samostatnou vazbou pro soukromé quby je chash (§16.9).
Úschova log_blind_secret (vyřešeno — §16.15 Q4). Zaslepení chrání nespojitelnost listů, nikoli důvěrnost prostého textu (tu drží obal dle §13 nezávisle). Při kompromitaci log_blind_secret pro jakýkoli qub_id, který protivník již drží nebo dokáže rekonstruovat (každý qub, jehož balíček/URL má, plus jakýkoli qub_id s nízkou entropií nebo veřejný), přepočítá ref listu v jediném hashi a spojí jej — jde o přímé spojení známé populace, nikoli o útok hrubou silou v neznámém prostoru. Klasifikujte log_blind_secret jako tajemství úrovně korelace/Sybil ve stejném stupni úschovy jako ostatní serverová tajemství a rotujte pouze dopředu (rotace znovu zaslepí budoucí listy; nemůže zpětně rozpojit již zakotvené).
16.3 Hashování listů a uzlů
Doménově oddělené hashování dle RFC 6962 §2.1 s SHA-256 nahrazeným SHA3-256:
leaf_hash = SHA3-256(0x00 || canonical_cbor(LogLeaf))
node_hash(l,r) = SHA3-256(0x01 || l || r)
empty tree = SHA3-256("") // defined but never anchored
Bajty doménových prefixů 0x02 (řetězec položek, §16.4) a 0x03 (hash STH, §16.6) jsou vyhrazené a disjunktní s těmito. Jsou to jednotlivé bajty, a nemohou se tedy srazit s existujícími 10bajtovými ASCII oddělovači domény (QUB_ID_V2 atd.). Strom je vlevo zaplněný nevyvážený strom dle RFC 6962 (každé vnitřní rozdělení na největší mocnině dvojky striktně menší než počet listů podstromu), což umožňuje, aby důkazy o zařazení a o konzistenci sdílely jediný algoritmus auditní cesty. Referenční specifikace nese explicitní pseudokód odvození vlevo/vpravo a fixuje testovací vektor s počtem listů, který není mocninou dvojky (5 listů), aby byl procvičen případ povýšení pravého okraje — který vektor se 4 listy skrývá.
16.4 Řetězení hashů (interní)
LogDO udržuje interní řetězec položek pouze kvůli konzistenci při pádu. Není nikdy publikován ani vystaven ověřovateli:
entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1] = SHA3-256("QUB_TLOG_GENESIS_V1")
Publikovanou autoritou pouze pro připojování je kumulativní kořen Merkle + jeho kotva (§16.5–16.6), nikdy surové pořadí, v němž operátor zrovna listy podává: řetězec se přepočítá pro libovolné podané pořadí, takže pouze zakotvený kořen fixuje kanonickou pozici.
16.5 Kumulativní strom Merkle a dávkování
Existuje jeden neustále rostoucí strom dle RFC 6962 nad všemi listy v pořadí seq — nikoli izolované stromy pro každou dávku. (Konstrukce řetězená přenosovým listem pro každou dávku byla odmítnuta: není to pravý prefixový vztah, takže její „důkazy o konzistenci“ jsou nepodložené.) Kumulativní strom poskytuje skutečné důkazy o konzistenci dle RFC 9162 a umožňuje, aby jediná nedávná kotva prokázala zařazení jakéhokoli staršího qubu.
Durable Object LogDO je jediný zapisovatel (blockConcurrencyWhile, zrcadlíc QuotaDO / EntitlementDO) — připojování ke sdílenému logu je čtení-úprava-zápis nad sdíleným stavem, a proto MUSÍ procházet přes DO, nikdy přes KV. Ukládá do mezipaměti pravookrajové čelo stromu (O(log n) hashů), takže uzavření dávky je O(batch). Dávka je množina listů zakotvených společně; její spouštěče jsou konfigurovatelné, nikoli zamrazené v protokolu: posun tree_size alespoň o LOG_BATCH_MAX_LEAVES (výchozí 4096), nebo dosažení kotvicí kadence stářím, nebo vynucené vyprázdnění, když dopadne placené zapečetění T3. root_i je kumulativní hash stromu Merkle nad listy 0 .. tree_size_i.
16.6 Podepsaná hlava stromu prostřednictvím kotvy Arweave
Kotvicí transakce Arweave je podepsanou hlavou stromu (Signed Tree Head) a nahrazuje podpis operátora pro samotnou hlavu stromu: denní kotva nepotřebuje žádný klíč qubu, protože owner transakce Arweave je oním podpisem. Teze o příkopu (moat) platí — nosný pro zakotvený kořen je neměnný substrát, nikoli tajemství držené qubem.
V návrhu je přesně jeden teplý podpisový klíč qubu a je fixovaný: klíč potvrzení pro každé zapečetění (§16.10). Jeho veřejný klíč je zavázán v LogProfile (distribuovaném s ověřovatelem) a křížově podepsán anchor_owner, takže ověřovatel validuje potvrzení proti témuž fixovanému kořeni jako kotvu. Toto je řešení §16.15 Q2 — nefixovaný, operátorem rotovatelný klíč potvrzení by byl popíratelný (operátor by mohl popřít, že klíč byl jeho), což by zneplatnilo hodnotu odpovědnosti potvrzení vůči protivníkovi na úrovni operátora, kvůli němuž potvrzení existuje. Takže: qub nedrží žádný nefixovaný podpisový klíč logu; klíč potvrzení je fixovaný a křížově podepsaný anchor_owner.
SignedTreeHead je kanonický CBOR (klíče dle zakódované délky): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (předchozí sth_hash; geneze = 32 nulových bajtů), log_id:bstr[32], first_seq:u64, anchored_at:i64. Jeho hash je sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).
Fixovaný kořen důvěry. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Vyhovující ověřovatel MUSÍ vyžadovat anchor_tx.owner == LogProfile.anchor_owner, kde anchor_owner (a veřejný klíč klíče potvrzení) je zabudován do qub_core jako LogProfile — vedle konstant quicknet již přítomných v DrandTimelockProvider::quicknet() — a distribuován s binárkou ověřovatele. Ověřovatel MUSÍ rovněž ověřit vazbu dat transakce Arweave → tx_id lokálně, místo aby důvěřoval odpovědi brány /raw/. To uzavírá díru ekvivokace s podvodnou peněženkou: „zakotveno v Arweave“ nemá význam, dokud ověřovatel nefixuje, která peněženka.
Rotace je rozšířením správy dle §15, nikoli opětovným použitím (vyřešeno — §16.15 Q3). Plocha profilu dle §15.2 nyní vyjmenovává pouze sig_algs / řetězce drand / verze obalu / typy obsahu a spouštěče §15.3 z nich neuvádějí žádný — LogProfile / anchor_owner ještě není v ploše §15. Správa rotace tudíž musí být vybudována: §15.3 je rozšířena (níže) o přidání spouštěče LogProfile a rotace je podepsaným zvýšením LogProfile doručeným v aktualizaci ověřovatele. Plánovaná rotace nese křížový podpis odcházející → příchozí; rotace vyvolaná kompromitací jej nést nemůže (odcházející klíč je právě tehdy nedůvěryhodný/nedostupný) a vrací se ke zvýšení spravovanému dle §15, přičemž kontrola rozdvojení (fork) předchozí kotvy (níže) v mezidobí omezuje škody.
Okno ekvivokace (prvořadý parametr důvěry). List je odolný proti ekvivokaci až poté, co je jeho pokrývající kotva v Arweave potvrzena. Okno je received_at → potvrzení kotvy (≤ kadence + finalita Arweave). Uvnitř něj jsou jedinými zárukami fixované potvrzení o zapečetění (§16.10) a provozní integrita qubu. Tři artefakty odpovědnosti činí toto upřímným spíše než mávnutím rukou (model svědka je řešením §16.15 Q2):
- Fixované podepsané potvrzení o zapečetění — analogie SCT vrácená v odpovědi na nahrání (§16.10), podepsaná fixovaným klíčem potvrzení křížově podepsaným
anchor_owner. List zahozený před svou kotvou zanechá oběti nepopiratelné potvrzení k publikaci, čímž uzavírá díru tichého vynechání. - Publikovaná metodika monitoru + procházení řetězce prev — řetězec
prevkotev se prochází od hlavy ke genezi; rozdvojení (dvě kotvy u jednésizes odlišnýmroot, nebo přerušenýprev) je publikovatelným důkazem pochybení. Detekce ekvivokace je deklarovaným provozním závazkem, nikoli tichým předpokladem. - Dvojí samostatně publikované hlavy — každá nová hlava
{sth_hash, tree_size}je zveřejněna do vyhrazeného veřejného úložiště GitHub vlastněného qubem, určeného pouze pro připojování (nosná větev samostatné publikace odolné proti manipulaci), s příspěvkem na sociální síti pouze jako nejlepší možné potvrzení. Neúspěšné zveřejnění MUSÍ vyvolat stránkování (page), nikoli tiše selhat.
Mez upřímnosti (závazné omezení). Protože qub ovládá obě plochy zveřejnění, jde o samostatné publikování, nikoli o nezávislé svědectví. Žádná produktová, marketingová ani právní plocha nesmí tvrdit, že log je „nezávisle dosvědčen“; povoleným tvrzením je, že ekvivokace je detekovatelná a zanechává nepopiratelné potvrzení. Pravý nezávislý svědek třetí strany je odložen do budoucího zvýšení správy dle §15.
received_at je tvrzeno operátorem a žádné tvrzení se na něj nesmí opírat — nikdy není vystaveno jako důkaz ani jako potvrzení sporu na žádné produktové / právní / API / důkaz-vykreslující ploše. Čas bloku kotvy Arweave T je jediným bezdůvěrnostním časovým razítkem (horní mez „zalogováno do“). Jakákoli zdravotní kontrola monitoru nad received_at MUSÍ porovnávat vůči T, nikoli vůči poli STH anchored_at ovládanému operátorem; taková kontrola je pojistkou pouze proti chybě hodin upřímného operátora, nikoli kontrolou odpovědnosti proti zlovolnému operátorovi (§16.15 Q5).
16.7 Formát kotvicí transakce a kadence
AnchorBundle je tělo transakce Arweave v kanonickém CBOR, zapsané přes bundler dle §16.8: ver:u8, sth:bstr (kanonické bajty SignedTreeHead), prev_anchor:bstr (surové bajty id předchozí kotvicí transakce; vynecháno při genezi), chain_hash:tstr (platný řetězec drand — quicknet) a proud leaf-CBOR dávky v pořadí seq, takže kotva je soběstačná: monitor znovu odvodí root z těla s nulovou závislostí na qubu. (Pokud se proud listů stane velkým při vysokém objemu, budoucí revize může zavázat pouze rozsah listů odkazem; poznamenáno, nikoli přijato v1.)
Značky Arweave jsou záměrně vyčíslitelné — log má být nalezen, na rozdíl od soukromých qubů: App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. Značky jsou nedůvěryhodné nápovědy; tělo CBOR je jedinou autoritou.
Kadence: ve výchozím nastavení denně, přehodnocováno s objemem (spouštěč podle velikosti automaticky zkracuje efektivní kadenci pod zátěží); placené zapečetění T3 vynucuje kotvu, takže platící zákazníci nikdy nečekají den. Kotvicí peněženka je vyhrazená a s nízkou rychlostí, oddělená od peněženky nahrávání — MUSÍ to být její vlastní JWK (samostatný klíč, nikoli logická role na peněžence nahrávání), aby kompromitace peněženky nahrávání nemohla padělat kotvy — s tvrdým denním rozpočtem kotvicích transakcí (degradovaný ARWEAVE_DAILY_CEILING). Postoj úschovy je řečen otevřeně: teplý klíč s úzkým rozsahem, těsnou pojistkou a nízkým zůstatkem, nikoli „studený“ — peněženka, která automaticky podepisuje denně, nemůže být studená, a specifikace nepředstírá opak.
16.8 Bundler ANS-104
Vlastní kodér DataItem dle ANS-104 a podepisovač s hloubkovým hashem (deep-hash), zhruba 300 řádků, pouze Web Crypto, nula závislostí npm (obě SDK Turbo neprojdou dodavatelskou pojistkou npm ci --ignore-scripts). Bajtové rozvržení DataItem:
signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data
Podepisování je Arweave deepHash — rekurzivní otisk SHA-384 (požadavek Arweave na drátě, crypto.subtle.digest("SHA-384")) nad ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — poté RSA-PSS nad hloubkovým hashem s JWK peněženky přes crypto.subtle; id = base64url(SHA-256(signature)). SHA-384 je zde uzavřena jako primitivum pouze pro drát Arweave, nikdy jako primitivum důvěry qubu (§15 zaznamenává tuto ohradu; hashování důvěry qubu je SHA3-256 všude).
Jedna cesta kódu obsluhuje tři spotřebitele: placenou trvalost T3 pro každý qub, záložní řešení při nedostupnosti Arweave (zařadit DataItem do fronty, vrátit potvrzení s prioritou R2 bez ohledu na to — toto uzavírá současnou slepou uličku 503 ARWEAVE_UNAVAILABLE) a zápis AnchorBundle. Podpisové schéma (vyřešeno — §16.15 Q8): v1 podepisuje pomocí RSA-PSS (podpisový typ 1) opětovným využitím existujícího mechanismu JWK peněženky Arweave (nula nové dlouhodobé úschovy klíčů, sloužící tezi „o jedno tajemství méně“); Ed25519 je odložen na migrační cestu PQ dle §15.
Ručně psaný hloubkový hash je v W5 kódem s nejvyšším rizikem a nejnižším přirozeným pokrytím, takže jeho jištění je nesmlouvavé (§16.15 Q8):
- Mezijazyková fixtura
tlog_v1.json(Rust + TS, vzorwrapper_v1.jsondle §14.5) pokrývá hloubkový hash, bajty DataItem + id, hashe listů, kořen z 5 listů + auditní cestu, hash STH, důkaz o zařazení a důkaz o konzistenci — v obou směrech, podepisování i ověřování (směr ověřování je důležitý, protože lokální kontrola transakce → tx_id dle §16.6 vtahuje hloubkový hash do každého samostatného ověřovatele, nikoli jen do zapisovatele). - Jednorázový interoperabilní průchod tam a zpět skrze referenční bundler ANS-104, spotřebovaný jako pouze statická testovací data — nikdy běhová závislost npm (postoj pouze Web Crypto / žádné instalační skripty platí).
- Cesta hloubkový hash + RSA-PSS musí projít tam a zpět týmiž primitivy
crypto.subtle, jaká používá produkce, aby byl vlastní kodér bajtově kompatibilní. - Průběžný monitor přijetí po sestavení potvrzuje, že každá kotva / záložní DataItem skutečně dosáhne přijetí v Arweave, s poplachem + pojistkou — protože hloubkový hash slouží i frontě záložního řešení při nedostupnosti Arweave, takže tichá regrese by tuto frontu zaplnila sítí odmítnutými položkami přesně během výpadku, který má pokrýt.
16.9 Důkazy o zařazení a o konzistenci
Oba jsou dle RFC 9162, SHA3-256, podávané jako kanonický CBOR.
InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (přesný CBOR listu — ověřovatel si leaf_hash přepočítá sám a nikdy nedůvěřuje dodanému hashi), index:u64, size:u64, audit:[bstr[32]], root:bstr[32], anchor:{ txid:bstr[32], batch:u64, sth:bstr[32], log_id:bstr[32], block_height?:u64, anchored_at?:i64 }.
ConsistencyProof — GET /api/v1/log/consistency?first=<size_a>&second=<size_b>: ver:u8, first_size:u64, second_size:u64, first_root:bstr[32], second_root:bstr[32], nodes:[bstr[32]], first_anchor, second_anchor. Jediný jednoznačný seznam klíčů, fixovaný testovacím vektorem.
Samostatné ověření (žádný server qubu, rozšiřuje §11):
1. Parse .qub bundle → SealedQub; recompute qub_id (§4.1).
2. Read leaf.kind.
3a. kind=0x01 (attested):
assert leaf.ref == qub_id
assert leaf.body_hash == SHA3-256(body)
assert leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserted):
assert leaf.ref == SHA3-256(qub_id || blind) // holder supplies blind
OR treat ref as opaque and bind via leaf.chash == SHA3-256(stored_bytes)
4. Recompute leaf_hash = SHA3-256(0x00 || leaf); fold `audit` per RFC 6962
using index/size; require derived root == proof.root.
5. Fetch anchor.txid from any gateway; verify the tx data → tx_id binding
(do not trust a gateway /raw/ response); REQUIRE anchor_tx.owner ==
LogProfile.anchor_owner.
6. Parse AnchorBundle; require committed root == proof.root and size ==
proof.size; read the Arweave block time T.
7. Emit the claim scoped by leaf.kind (§16.11).
Úložiště podávající důkazy MUSÍ být klíčované souřadnicemi (vyřešeno — §16.15 Q7, blokující předpoklad). Generování důkazu pro studený list je neutrální vůči správnosti pouze tehdy, je-li auditní materiál v R2 trvalým úložištěm uzlů Merkle klíčovaným absolutní souřadnicí stromu (level, index) — nikoli rozdíly uzlů podle batch. S úložištěm klíčovaným souřadnicemi je libovolná auditní cesta (leaf i, size N) množinou O(log N) přímých GET z R2 bez přepočítávání napříč hranicemi dávek; s úložištěm klíčovaným podle dávek tomu tak není, což je mezera v rozvržení úložiště, kterou toto řešení uzavírá. Těla listů jsou rovněž adresovatelná podle obsahu přes seq. Testovací vektor W5 MUSÍ prokázat studený list z doby geneze proti mnohem pozdějšímu kořeni jen pomocí R2 + Arweave se smazaným úložištěm LogDO, aby tvrzení o bezpečnosti rekultivace v §16.13 bylo podloženo, nikoli pouze proklamováno. Sekvenční O(log N) GET z R2 patří pouze na asynchronní koncový bod důkazu — nikdy na horkou cestu zapečetění (§16.10) ani na cron s rozpočtem na tik.
16.10 Pořadí potvrzení s prioritou R2
Sekvence POST /api/v1/upload se stává:
- Brány přední poloviny (autentizace, validace, shardovací klíč idempotence) — beze změny.
- Synchronně
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— spodní hranice trvanlivosti; rovněž uzavírá závod předmezipaměti z W1 (dřívectx.waitUntilpo odeslání do Arweave). - Synchronně
await LogDO.append(leaf)— jedno RPC DO v rámci kolokace; jediný zapisovatel přidělíseq, rozšíří řetězec položek a aktualizuje čelo. (ČÚZ nad sdíleným stavem → DO, nikdy KV.) RPCappenddělá pouze toto — práce uzavření dávky MerkleO(batch)běží mimo toto RPC na alarmu LogDO, jinak p95 připojení vyskočí u každéhoLOG_BATCH_MAX_LEAVES-tého zapečetění. - Vrátit potvrzení nyní — s potvrzením o zapečetění (podepsaným fixovaným klíčem potvrzení, §16.6) a
{ tx_id, log_seq, anchor_status: "pending" }. Vícesekundové rozvětvení do Arweave je odstraněno z kritické cesty. - Jedno
ctx.waitUntilzařadí odloženou práci: odeslání každého qubu do Arweave (nyní nejlepší možné / placené; při selhání směruje do fronty záložního řešení bundleru namísto vrácení 503 uživateli) plus existující zápisy provizorních metadat. Uzavření dávky a kotvení běží nezávisle z alarmu LogDO a denního cronu kotvení. Žádnéctx.waitUntiluvnitř smyčky; existující shardovací klíč idempotence je zachován.
Rozpočet latence (vyřešeno — §16.15 Q7). Cíl < 300 ms p95 je měřená spouštěcí brána, nikoli předpoklad. Upřímná kritická cesta je čtení KV přední poloviny + jeden PUT do R2 + dva sériové Durable Objects — existující debet kvóty zapečetění QuotaDO a nové připojení LogDO — takže rozpočet musí počítat se dvěma obrátkami DO v rámci kolokace, nikoli s jednou. Nasaďte latenční alarm LogDO zrcadlící alarm QuotaDO a regresi p95 berte jako blokátor vydání.
16.11 Model důvěry — přesné tvrzení, vymezené druhem listu
Pro kind=0x01 (atestovaný): „Tento obsah — tělo odpovídající body_hash, identifikované qub_id — byl zavázán do logu qubu pouze pro připojování na pozici seq a existoval nejpozději k času bloku Arweave T; byl kryptograficky nečitelný až do kola drand R = unlock_round(unlock_at).“ Toto je úplná trojice {vázání kola tlock + zařazení do Merkle + zakotvený kořen}.
Pro kind=0x02 (tvrzený, výchozí): „Neprůhledný šifrový text s adresou obsahu chash, tvrdící qub_id a unlock_at, byl zavázán do logu pouze pro připojování na pozici seq a existoval nejpozději k času bloku Arweave T.“ Větve kola a těla dodává existující ověření balíčku .qub dle §11 (qub_core::unlock), nikoli log; co log přidává nad holou transakci pro každý qub, je řazení odolné proti manipulaci, bezdůvěrnostní čas závazku jako horní mez a odolnost proti ekvivokaci.
Obě tvrzení vylučují, dle §11: autorství bez sig_alg ≥ 0x01, záměr a časování s granularitou pod úrovní kotvy. Ani jedno nedovoluje, aby se jakékoli tvrzení opíralo o received_at.
Strop tvrzení (závazné spouštěcí omezení — vyřešeno §16.15 Q1). U bezplatných / výchozích qubů (kind=0x02) je výše uvedené vymezené tvrzení kind=0x02 stropem toho, co smí jakákoli produktová, marketingová, smluvní či důkaz-vykreslující plocha tvrdit. Žádná plocha nesmí uvádět ani naznačovat, že log prokazuje obsah nebo kolo odemčení výchozího qubu — log prokazuje řazení + bezdůvěrnostní čas závazku neprůhledného šifrového textu jako horní mez. Důkaz obsahu a kola pochází výhradně z existujícího ověření balíčku .qub dle §11, které je nezávislé na logu. Toto je tvrdý spouštěcí blokátor na textech, nikoli stylistická preference; je to řešení, které udržuje výchozí cestu slepou vůči bajtům upřímnou.
16.12 Verzování a koordinace s W3
Nedochází ke zvýšení formátu SealedQub na drátě a tudíž ani ke zvýšení verze protokolu (§12.1): log je sidecar, který se zavazuje k existujícím polím a bajtům, takže nevstupuje do historie verzí protokolu dle §12.2. Volitelné drand_chain_version z W3 zůstává nedotčeno a zůstává jediným volitelným polem SealedQub. Log namísto toho zavádí vlastní nezávislé prostory verzí — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — zrcadlíc nezávislost verze obalu dle §12.4 (obal nese bajt verze nezávislý na verzi protokolu a verze logu sledují totéž oddělení).
Doručení důkazu je ve výchozím nastavení vyzvedáváno, s volitelným přibalením. Důkaz nemůže existovat v okamžiku zapečetění (kotva ještě nebyla zapsána), takže balíček .qub v okamžiku zapečetění zůstává bez důkazu. Ověřovatel z W7 vyzvedne GET …/proof jednou, nebo v plně offline režimu rekonstruuje důkaz z veřejného AnchorBundle přes dotaz Arweave na Log-Id. Balíček .qub (W7) rezervuje volitelný člen inclusion_proof — nepřítomný při zapečetění, naplněný post-kotvicím opětovným exportem pro studenou archivaci — sledujíc týž vzor „volitelné, ve výchozím nastavení vynechané, aditivní“ jako drand_chain_version z W3.
16.13 Retence
Retenční okna pro otevřený konec LogDO, substrát R2 podávající důkazy, čítače pojistky kotvy a frontu záložního řešení bundleru jsou specifikována v docs/DATA-RETENTION.md. Princip: horké úložiště logu na položku (LogDO) je po zakotvení rekultivovatelné; jeho auditní materiál — úložiště uzlů Merkle klíčované souřadnicí (level, index) + těla listů adresovaná podle seq (§16.9) + kotvy Arweave — je trvalý. Rekultivace studeného listu z DO nikdy nezneplatní vydaný důkaz, protože důkaz se řeší proti onomu trvalému úložišti uzlů R2 a kotvě Arweave, nikoli proti DO (a testovací vektor se smazaným DO dle §16.9 to dokazuje).
16.14 Testovací vektory
W5 dodává mezijazykovou fixturu tlog_v1.json (§16.8) plus zpracované vektory: list kind=0x01 a list kind=0x02 → leaf_hash; kumulativní kořen z 5 listů; jeden důkaz o zařazení; jeden důkaz o konzistenci; jeden AnchorBundle; a jedno id DataItem. Tyto žijí vedle vektorů vnějšího obalu dle §14.5 a jsou procvičovány jak implementací v Rustu (qub-core), tak v TypeScriptu (Worker).
16.15 Rozhodnutí revize (W5 — vyřešena)
Externí revize W5 (adversariální průchod návrhem + schválení vlastníkem) je dokončena. Každé rozhodnutí níže je vyřešeno a promítnuto do textu §16 výše; závazná spouštěcí omezení jsou na konci zopakována. Implementace může pod nimi pokračovat.
- Upřímnost listu výchozí cesty (
kind=0x02) — VYŘEŠENO. Nasadit rozdělení na dva druhy listů dle specifikace:kind=0x02se nezavazuje ani kbody_hash, ani kdrand_round. Žádné pole*_body_hashna cestě slepé vůči bajtům (byl by to nejčitelnější falešný signál „ověřeno“ pro integrátory a je to pohodlí, které §11 z balíčku již poskytuje). Nevyžadovat serverové zapečetění pro quby atestované logem (to by protlačilo prostý text Workerem a zničilo příkop kryptografické skartace). Jakákoli sebepopisná zkratka patří do balíčku.qub/ obálky důkazu jako pole přepočítané ověřovatelem, nikdy jako pole listu. Vlastníkem potvrzený strop tvrzení: §16.11. - Odpovědnost za ekvivokaci / vynechání — VYŘEŠENO. Klíč potvrzení o zapečetění je fixován v
LogProfile+ křížově podepsánanchor_owner(uzavírá předchozí rozpor „žádný podpisový klíč“; §16.6). Model svědka při startu: fixované potvrzení + metodika monitoru + procházení řetězce prev + dvojí samostatně publikované hlavy (veřejné úložiště GitHub vlastněné qubem, sociální síť jako nejlepší možné), prodávaný jako detekovatelný + s potvrzením, nikdy nezávisle dosvědčený. Pravý svědek třetí strany je odložen na zvýšení správy dle §15. - Fixovaný kořen důvěry vlastníka kotvy + rotace — VYŘEŠENO. Přijmout fix
LogProfile(§16.6); ověřovatel kontrolujeanchor_tx.owner == anchor_ownera ověřuje vazbu dat transakce → tx_id lokálně. Správa rotace je rozšířením k vybudování dle §15 (přidán spouštěč §15.3), nikoli opětovným použitím; plánované rotace křížově podepisují, rotace vyvolané kompromitací se vracejí ke zvýšení dle §15 s kontrolou rozdvojení omezující škody. - Zaslepení listu soukromého qubu — VYŘEŠENO. Ponechat zaslepení pro soukromé quby (
ref = SHA3-256(qub_id ‖ log_blind_secret)), surovýqub_idpro veřejné quby (již §16.2.1),chashjako samostatnou vazbu.log_blind_secretje tajemství úrovně korelace/Sybil, rotace pouze dopředu (§16.2.1). received_at— VYŘEŠENO. Ponechat jej v listu, zavázaný, avšak explicitně nedůkazní; nikdy nevystavený jako důkaz ani potvrzení sporu na žádné ploše. Jakákoli zdravotní kontrola monitoru porovnává vůči času bloku ArweaveT, nikoli vůčianchored_atovládanému operátorem (§16.6).- Prokazatelné časování bezplatného stupně — VYŘEŠENO (schválení vlastníkem). Trvanlivost se neregreuje; pouze prokazatelný čas závazku jako horní mez hrubne na čas bloku kotvy. Texty bezplatného stupně nepoužívají žádné číselné SLA („…přidáno u příští kotvy logu, typicky denně“); důkaz s přesností na hodinu je placenou vlastností T3, zveřejněnou na ploše porovnání stupňů + v podmínkách (§16.1).
- Kumulativní strom na Workerech — VYŘEŠENO. Jediný kumulativní strom dle RFC 9162 + jediný zapisovatel LogDO s čelem v mezipaměti (pohodlná rezerva vůči stropu DO ~1k zápisů/s; shardování typu Merkle-z-kořenů-shardů odložit, dokud nebude blízko). Blokující předpoklad: úložiště uzlů R2 klíčované souřadnicemi
(level, index)+ testovací vektor studeného listu se smazaným DO (§16.9);< 300 msje měřená spouštěcí brána nad dvěma sériovými DO (§16.10). - Podpisové schéma ANS-104 + hloubkový hash — VYŘEŠENO. RSA-PSS (podpisový typ 1, opětovné využití vyhrazeného JWK kotvicí peněženky); Ed25519 odložen na cestu PQ dle §15. Ručně psaný hloubkový hash SHA-384 je jištěn obousměrnou mezi-implementační fixturou, interoperabilní kontrolou pouze se statickým referenčním bundlerem, průchodem tam a zpět se sdíleným
crypto.subtlea monitorem přijetí v Arweave po sestavení (§16.8).
Závazná spouštěcí omezení (přenést do implementace + produktové/právní revize):
- Strop tvrzení (Q1/Q6). Žádná plocha nesmí říci, že log prokazuje obsah či kolo odemčení výchozího qubu; povoleným tvrzením je seřazeno, odolně proti manipulaci, s bezdůvěrnostním časem závazku jako horní mezí. Texty časového razítka bezplatného stupně nenesou žádnou číselnou latenci; důkaz s přesností na hodinu je pouze placené T3.
- Upřímnost svědka (Q2). Ekvivokaci prodávat jako detekovatelnou + s potvrzením, nikdy nezávisle dosvědčenou.
- Klíče potvrzení + kotvy (Q2/Q8). Klíč potvrzení je fixovaný + křížově podepsaný; kotvicí peněženka je její vlastní JWK odlišný od peněženky nahrávání.
- Brána hloubkového hashe (Q8). Žádná kotva ani transakce T3 nedopadne, dokud neprojde obousměrná fixtura + interoperabilní kontrola; monitor přijetí stránkuje při selhání.
- Předpoklad úložiště (Q7). Úložiště uzlů klíčované souřadnicemi + vektor studeného listu se smazaným DO jsou předpoklady pro záruku „rekultivace nikdy nezneplatní důkaz“.