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. |
| 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. Qub s verdiktem ve svém těle 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í
Quby se ukládají 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
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V1" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 // u8 (1 byte): MUST be 0x00 in v1.0
)
// Total preimage: 91 bytes → 32-byte hash
signature = Sign(author_secret_key, sig_input)
Oddělovač domény: "QUB_AUTHOR_SIG_V1" má 17 bajtů ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Bez výplně.
Koncový bajt: 91. bajt předobrazu 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 se zavazuje ke čtyřem polím obálky: version, qub_id, body_hash, unlock_at (plus pevný oddělovač domény a bajt org_id_present). Tři z těchto čtyř jsou strukturální invarianty: 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 |
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 |
sender_label |
✗ | Text pouze pro zobrazení; měnitelný bez porušení podpisu |
reply_to |
✗ | Ukazatel vlákna; měnitelný bez porušení podpisu |
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.) |
Bezpečnostní důsledky neověřených polí.
- Strana se zápisovým přístupem k uloženým bajtům by mohla vyměnit
sender_label(„Alice” → „Mallory”) bez zneplatnění podpisu autora.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. - Pole
reply_tolze rovněž upravit po podepsání. Protožequb_idje adresován obsahem, útočník nemůže nasměrovatreply_tona neexistující cíl, ale může tiše přepojit odpověď k jinému existujícímu qubu.
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 (same formula as §9.3).
6. Verify(author_pubkey, sig_input, author_signature).
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
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.
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. - 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 magické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 | 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.
- Známé hlavní verze MOHOU tolerovat neznámá volitelná pole, pokud to pravidla dopředné kompatibility umožňují (volitelná pole nepřítomná v kanonickém pořadí klíčů jsou ignorována).
- 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 volitelnými klíči mapy CBOR (klíče nezahrnuté v kanonickém pořadí dle §3.2) BY MĚL tyto klíče ignorovat a pokračovat v ověřování pomocí známých polí. To umožňuje budoucí drobné přídavky (např. nová metadata) bez nutnosti zvyšovat hlavní verzi.
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). - Trasa zapečetění na straně serveru Worker:
crypto.getRandomValues.
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í.