Specyfikacja protokołu qub
qub to protokół kryptograficznych zobowiązań czasowych: system do pieczętowania słów do przyszłej daty i udowadniania, gdy ta data nadejdzie, dokładnie co zostało powiedziane i kiedy.
Działanie zapewniają trzy prymitywy. drand to zdecentralizowane źródło losowości — data ujawnienia jest egzekwowalna fizyką, a nie dobrą wolą jakiejkolwiek strony. Trwały publiczny magazyn to odporny na manipulacje publiczny rejestr — żadna strona nie może edytować ani usunąć qub po jego zapieczętowaniu. ML-DSA-65 to postkwantowy podpis cyfrowy — każdy qub jest powiązany z parą kluczy, której tajny klucz nigdy nie opuszcza urządzenia autora.
Razem te prymitywy tworzą wypowiedź, która jest zablokowana czasowo, odporna na manipulacje i przypisywalna — pokwitowanie, którego wartość rośnie wraz z poprawą zdolności świata do fałszowania przeszłości.
Pozostała część tego dokumentu to specyfikacja normatywna wymagana dla interoperacyjnych implementacji.
Specyfikacja protokołu qub
| Pole | Wartość |
|---|---|
| Wersja | 1.0 (wersja protokołu 0x01, wersja zewnętrznej koperty 0x01) |
| Data | 2026-05-01 |
| Status | Szkic |
| Zrecenzowano do | 2026-05-01 |
Ten dokument jest normatywną specyfikacją protokołu dla systemu zobowiązań czasowych qub. Definiuje struktury danych, reguły serializacji, formuły derywacji oraz procedury weryfikacji wymagane dla interoperacyjnych implementacji.
Zakres: warstwa protokołu jest celowo neutralna językowo — ciało qub to nieprzezroczysty tekst jawny / markdown / bajty paktu, a renderowanie z uwzględnieniem lokalizacji jest odpowiedzialnością widza (aplikacja webowa qub.social, ramka iframe <qub-embed>, klienci MCP, itd.).
1. Notacja i konwencje
| Notacja | Znaczenie |
|---|---|
u8, u64, i64 |
Liczby całkowite bez znaku/ze znakiem o określonej szerokości bitowej |
[u8; N] |
Tablica bajtów o stałej długości N bajtów |
Vec<u8> |
Tablica bajtów o zmiennej długości |
Option<T> |
Wartość typu T, albo nieobecna |
String |
Łańcuch tekstowy UTF-8, znormalizowany NFC |
| ` | |
SHA3-256(x) |
Hash NIST SHA3-256 łańcucha bajtów x (FIPS 202) |
ceil(x) |
Funkcja sufitu: najmniejsza liczba całkowita ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Najbardziej znaczący bajt jako pierwszy |
Wszystkie liczby całkowite w konstrukcjach preimage są kodowane jako tablice bajtów big-endian o stałej szerokości (i64 → 8 bajtów, u8 → 1 bajt), o ile nie określono inaczej.
Wszystkie znaczniki czasu to sekundy uniksowe w UTC.
2. Struktury danych
2.1 ComposeQub (stan twórcy w pamięci)
Nieserializowany do CBOR. Niezapisywany w trwałym magazynie. Lokalny dla aplikacji twórcy.
ComposeQub {
draft_id: [u8; 16], // Losowy, generowany lokalnie
created_at: i64, // Sekundy uniksowe UTC
unlock_at: Option<i64>, // Sekundy uniksowe UTC; None podczas komponowania
visibility: u8, // 0x01 = publiczny (jedyna wartość w MVP)
content_type: u8, // 0x01 = tekst (jedyna wartość w MVP)
plaintext: Vec<u8>, // Ciało qub w UTF-8
sender_label: Option<String>, // Dekoracyjna nazwa wyświetlana; nieuwierzytelniona
status: DraftStatus, // Composing | Sealed | Uploaded | Failed
}
2.2 QubEnvelope (odszyfrowany ładunek)
Serializowany przy użyciu kanonicznego CBOR (§3). Zaszyfrowany wewnątrz SealedQub. To jest struktura, która dowodzi integralności treści po odszyfrowaniu.
QubEnvelope {
version: u8, // Główna wersja protokołu (0x01 dla v1)
qub_id: [u8; 32], // Wyprowadzony (zob. §4.1)
content_type: u8, // Rejestr typów treści (zob. §6)
created_at: i64, // Sekundy uniksowe UTC
unlock_at: i64, // Sekundy uniksowe UTC
outcome_at: Option<i64>, // V1.1 — gdy rzeczywistość wydaje werdykt (verdict-uplift-plan §3.1)
sender_label: Option<String>, // Dekoracyjny; nieuwierzytelniony w MVP
reply_to: Option<[u8; 32]>,// qub_id rodzica dla łańcuchów odpowiedzi; nieobecny w preimage qub_id; niepodpisany (zob. §9.3)
body: Vec<u8>, // Ładunek treści (UTF-8 dla tekstu, CBOR dla paktu)
body_hash: [u8; 32], // SHA3-256(body) (zob. §4.2)
sig_alg: u8, // Algorytm podpisu (zob. §9.2)
author_signature: Option<Vec<u8>>, // Ustawione gdy sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Ustawione gdy sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Ustawione dla współpodpisanych umów bilateralnych typu pact
cosigner_signature: Option<Vec<u8>>, // Ustawione dla współpodpisanych umów bilateralnych typu pact
}
Wariant bazowy (niepodpisany qub tekstowy): version = 0x01, content_type = 0x01, sig_alg = 0x00, wszystkie pola Option nieobecne.
Inne konfiguracje v1: content_type = 0x03 (ciało paktu, zob. §6.1); sig_alg = 0x01 (ML-DSA-65) z obecnymi author_signature i author_pubkey (zob. §9.3); cosigner_pubkey i cosigner_signature obecne razem dla współpodpisanych paktów (zob. §9.7); reply_to ustawione na qub_id rodzica qub dla qubów w łańcuchu odpowiedzi (zob. §9.3 dla implikacji zakresu podpisu).
2.3 SealedQub (kanoniczny format na drucie)
Serializowany przy użyciu kanonicznego CBOR (§3). Wysyłany do trwałego magazynu. To jest artefakt on-chain.
SealedQub {
version: u8, // Główna wersja protokołu (0x01 dla v1)
qub_id: [u8; 32], // Taki sam jak QubEnvelope.qub_id
visibility: u8, // 0x01 = publiczny; widzowie v1 odrzucają inne wartości
unlock_at: i64, // Sekundy uniksowe UTC
outcome_at: Option<i64>, // V1.1 — wyświetlane na CTA śledzenia werdyktu
// przed ujawnieniem; lustro QubEnvelope.outcome_at;
// związane z qub_id przez preimage z §4.1.
drand_chain_id: String, // Hash łańcucha drand (ciąg hex)
drand_round: u64, // Docelowy numer rundy drand
tlock_ciphertext: Vec<u8>, // Bajty CBOR QubEnvelope zaszyfrowane tlock
recipient_pubkey: Option<[u8; 32]>,// Pole zarezerwowane; akceptowane przez kanoniczny CBOR
// ale nieinterpretowane przez referencyjnego widza v1
title: Option<String>, // Tytuł w postaci tekstu jawnego pokazywany na
// odliczaniu widza przed ujawnieniem. Powiązany
// z qub_id przez title_hash (§4.1). 1..=100 punktów
// kodowych NFC, bez znaków sterujących.
}
2.4 RevealedQub (stan aplikacji widza)
Nieserializowany do CBOR. Lokalny dla aplikacji widza. Konstruowany po pomyślnym odszyfrowaniu i weryfikacji.
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 — przeniesione z QubEnvelope.outcome_at / SealedQub.outcome_at; napędza blok obserwowania werdyktu na stronie ujawnienia (verdict-uplift-plan §5.1)
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Przeniesione z 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. Profil kanonicznego CBOR
Cała serializacja SealedQub i QubEnvelope MUSI być zgodna z tym profilem. Dwie implementacje otrzymujące tę samą logiczną strukturę MUSZĄ wytworzyć identyczne bajty.
3.1 Reguły kodowania
| Reguła | Specyfikacja |
|---|---|
| Standard | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Kolejność kluczy mapy | Sortowane najpierw po długości zakodowanych bajtów (krótsze przed dłuższymi), następnie leksykograficznie (bajt po bajcie dla kodowań tej samej długości) |
| Kodowanie liczb całkowitych | Najkrótsza forma: 0–23 w bajcie początkowym; 24–255 w 2 bajtach; 256–65535 w 3 bajtach; itd. |
| Kodowanie długości | Tylko określone długości. Bez tablic, map, łańcuchów bajtów ani łańcuchów tekstowych o nieokreślonej długości (additional info = 31 jest zabronione). |
| Tagi | Bez tagów CBOR (główny typ 6 jest zabroniony). |
| Zmiennoprzecinkowe | Bez liczb zmiennoprzecinkowych (główne typy 7 wartości 0xF9–0xFB są zabronione). |
| Łańcuchy tekstowe | Kodowane UTF-8, znormalizowane NFC (Unicode Normalization Form C). |
| Łańcuchy bajtów | Surowe bajty. Bez kodowania base64 w warstwie CBOR. |
| Duplikaty kluczy | Odrzucenie z błędem. Parsery NIE MOGĄ po cichu akceptować zduplikowanych kluczy mapy. |
| Wartości proste | Dozwolone są tylko true (0xF5), false (0xF4) i null (0xF6). |
| Pola opcjonalne | Nieobecne pola opcjonalne są pomijane całkowicie z mapy CBOR (nie są kodowane jako null). Obecne pola opcjonalne są dołączane w posortowanej kolejności kluczy. |
3.2 Zweryfikowane kanoniczne kolejności kluczy
Te kolejności kluczy są normatywne. Implementacje MUSZĄ emitować klucze dokładnie w tej kolejności. Asercje debugujące POWINNY weryfikować kolejność w buildach nierelease.
QubEnvelope (wersja 0x01, niepodpisany, wszystkie pola opcjonalne nieobecne):
"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)
Wyprowadzenie kolejności kluczy QubEnvelope: każdy klucz jest łańcuchem tekstowym CBOR. Długość zakodowana = 1 bajt nagłówka + długość łańcucha (dla łańcuchów poniżej 24 bajtów). Sortowanie najpierw po całkowitej długości zakodowanej, następnie leksykograficznie dla kluczy o tej samej długości.
SealedQub (wersja 0x01, publiczny, bez odbiorcy):
"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 (ciało 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 (wiersz tablicy 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 Odniesienie kodowania bajtów
| Typ | Kodowanie CBOR | Przykład |
|---|---|---|
| Hash SHA3-256 (32 bajty) | 0x58 0x20 + 32 bajty |
body_hash, qub_id |
| Znaczniki czasu (i64) | Główny typ 0 (dodatni) lub 1 (ujemny), najkrótsze kodowanie | Sekundy uniksowe |
| Wersja (u8, wartość 1) | 0x01 (pojedynczy bajt) |
|
| Typ treści (u8, wartość 1) | 0x01 (pojedynczy bajt) |
|
| sig_alg (u8, wartość 0) | 0x00 (pojedynczy bajt) |
|
| Podpis ML-DSA-65 (3 309 bajtów) | 0x59 0x0C 0xED + 3 309 bajtów |
author_signature, cosigner_signature |
| Klucz publiczny ML-DSA-65 (1 952 bajty) | 0x59 0x07 0xA0 + 1 952 bajty |
author_pubkey, cosigner_pubkey |
4. Derywacje normatywne
4.1 qub_id
qub_id jednoznacznie identyfikuje qub i wiąże QubEnvelope z SealedQub. Jest wyprowadzany deterministycznie z zawartości koperty.
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
Kodowanie separatora domeny: Łańcuch "QUB_ID_V2" ma 9 bajtów ASCII. Dla wyrównania dołączany jest pojedynczy bajt wypełniający 0x00, aby osiągnąć 10 bajtów. Implementacje MUSZĄ używać dokładnie tych 10 bajtów: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
Kodowanie outcome_at: V1.1 rozszerzyło preimage z 92 do 100 bajtów, aby wpleść opcjonalne pole outcome_at w wiązanie. Nieobecne outcome_at jest kodowane jako 8 zerowych bajtów; walidatory protokołu wszędzie odrzucają outcome_at <= 0, więc ten sentinel nie może kolidować z prawowitą wartością. Zob. §3.2 (format na drucie) oraz wewnętrzny tasks/verdict-uplift-plan.md dla mechaniki werdyktu, która motywuje to pole.
Kodowanie drand_round: V1.2 rozszerzyło preimage z 100 do 108 bajtów, aby wpleść drand_round (docelową rundę drand, §4.3) w wiązanie, i podbiło separator domeny do QUB_ID_V2. To wiąże rundę szyfrowania czasowego z tożsamością qub: brama nie może powiązać ponownie tekstu zaszyfrowanego z inną rundą (np. już minioną) niż implikuje wyświetlane unlock_at. Procedura odblokowania (§8) dodatkowo weryfikuje, że runda zaszyta w stanzie tekstu zaszyfrowanego tlock pasuje do unlock_round(unlock_at), więc wyświetlany czas odblokowania jest dowodliwie rundą, która bramkuje odszyfrowywanie.
Właściwości:
- Zmiana dowolnego pola w QubEnvelope (body, znaczniki czasu, typ treści, wersja) wytwarza inny qub_id.
- qub_id jest obliczany przed szyfrowaniem. Zarówno QubEnvelope, jak i SealedQub niosą ten sam qub_id. Widz weryfikuje ich zgodność po odszyfrowaniu.
- qub_id nie zależy od
sender_label,author_signatureaniauthor_pubkey. Oznacza to, że ta sama treść zapieczętowana w tym samym czasie produkuje ten sam qub_id niezależnie od tego, kto ją podpisuje. - Zmiana
titlew SealedQub (przy zachowaniu pozostałych wartości) zmieniaqub_idprzeztitle_hash. Brama nie może więc podmienić tytułu jawnego wyświetlanego na odliczaniu bez unieważnienia tożsamości qub. - Zmiana
outcome_atw SealedQub (przy zachowaniu pozostałych wartości) zmieniaqub_idprzez preimage. Brama nie może podmienić daty werdyktu wyświetlanej na odliczaniu przed ujawnieniem bez unieważnienia tożsamości qub. - Zmiana
drand_round(przy zachowaniu pozostałych wartości) zmieniaqub_idprzez preimage. Brama nie może powiązać ponownie tekstu zaszyfrowanego szyfrowania czasowego z inną rundą bez unieważnienia tożsamości qub; w połączeniu ze sprawdzeniem rundy stanzy w czasie odblokowania z §8 wyświetlaneunlock_atjest rundą, która faktycznie bramkuje odszyfrowywanie.
4.2 body_hash
body_hash = SHA3-256(body)
Gdzie body to surowy ładunek treści Vec<u8>. Dla qubów tekstowych jest to ciało qub zakodowane w 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
Gdzie title to opcjonalny tytuł jawny pokazywany na odliczaniu widza przed ujawnieniem (zob. §3.2). Normalizacja NFC odbywa się w czasie obliczania hasha, dzięki czemu skrót jest stabilny dla wizualnie równoważnych sekwencji punktów kodowych. Wartość-strażnik zawierająca same zera jest zarezerwowana dla przypadku nieobecnego; pusty łańcuch jest odrzucany na granicy kanonicznego CBOR jako niekanoniczne kodowanie "nieobecny" (kanoniczne kodowanie pomija to pole całkowicie).
4.3 Mapowanie rundy odblokowania
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Parametr | Źródło | Przykład |
|---|---|---|
unlock_at |
Sekundy uniksowe UTC wybrane przez użytkownika | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
Informacje o łańcuchu drand (genesis_time) |
1595431050 |
chain_period_seconds |
Informacje o łańcuchu drand (period) |
30 |
Operacja ceil() wybiera pierwszą rundę drand, której czas ujawnienia jest ≥ unlock_at. Zapewnia to, że qub nie staje się odszyfrowywalny przed wybranym czasem odblokowania.
Przypadek brzegowy: jeśli (unlock_at - chain_genesis_time) jest dokładnie podzielne przez chain_period_seconds, wynikiem jest dokładnie ta runda — qub odblokowuje się precyzyjnie w czasie ujawnienia tej rundy.
Walidacja: unlock_at MUSI być w przyszłości w czasie pieczętowania. unlock_at NIE MOŻE być więcej niż 10 lat od created_at (aby ograniczyć ryzyko zależności od drand w długim horyzoncie; UI POWINIEN ostrzegać dla dat odblokowania powyżej 2 lat).
5. Newtype'y formatu drutowego
Newtype'y formatu drutowego zapewniają bezpieczeństwo w czasie kompilacji przed myleniem bajtów CBOR z JSON, surowym tekstem jawnym lub innymi kodowaniami bajtów.
| Typ | Zawiera | Produkowany przez | Konsumowany przez |
|---|---|---|---|
SealedQubCbor |
Kanoniczny CBOR SealedQub | serialize_sealed_qub() |
Wysyłanie do trwałego magazynu, pobieranie widza |
QubEnvelopeCbor |
Kanoniczny CBOR QubEnvelope | serialize_qub_envelope() |
Wejście szyfrowania tlock, wyjście deszyfrowania tlock |
5.1 Reguły konstrukcji
// 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 Walidacja przy konstrukcji
from_encoded() POWINIEN walidować, że wejście zaczyna się od prawidłowego nagłówka mapy CBOR. Pełna walidacja strukturalna odbywa się w czasie parsowania, a nie konstrukcji, aby uniknąć podwójnego parsowania.
6. Rejestr typów treści
| Wartość | Typ | Maks. rozmiar ciała | Uwagi |
|---|---|---|---|
0x00 |
Zarezerwowane (nieprawidłowe) | — | NIE MOŻE być używane |
0x01 |
Tekst jawny (UTF-8, ograniczony Markdown) | 50 KB płatne / 10 KB darmowe | Zobacz §10 dla reguł renderowania. Podział darmowy / płatny jest egzekwowany przez usługę uploadu; twarda granica warstwy protokołu to 50 KB. |
0x02 |
Zarezerwowane (przyszłe) | — | Przydzielone dla przyszłego typu treści; nieprawidłowe w v1. Widzowie MUSZĄ odrzucić zgodnie z regułą poniżej. |
0x03 |
Pact (umowa bilateralna, ciało CBOR) | 100 KB | Ciało to kanoniczny CBOR PactTerms (§6.1). Współpodpisywanie cosignera zgodnie z §9.7. |
0x04 |
Werdykt (samoocena twórcy, ciało CBOR) | 8 KB | Ciało to kanoniczny CBOR VerdictBody (§6.2). Emitowane wyłącznie przez systemowy intent verdict. Powiązanie z qubem rodzicielskim jest na tagu Arweave Parent-Tx-Id, nie w ciele. Zobacz verdict-uplift-plan §3.4. |
Widzowie MUSZĄ odrzucać nieznane typy treści z wyraźnym błędem widocznym dla użytkownika. Widzowie NIE MOGĄ próbować renderować nieznanych typów jako tekstu.
6.1 Ciało paktu (content_type = 0x03)
Ciało paktu to kanoniczne kodowanie CBOR wartości 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)> }
Kanoniczne kolejności kluczy CBOR dla wszystkich trzech map są podane w §3.2. Całkowity serializowany CBOR paktu NIE MOŻE przekraczać 100 KB (zgodnie z §6).
Dyskryminator schematu. Pierwszy wiersz w terms dla paktu structured/v1 MUSI być { key: "pact_schema", value: "structured/v1" }. Wiersze bez tego znacznika to pakty "custom" i nie otrzymują strukturalnej walidacji ani renderowania świadomego schematu.
Zamrożone sloty potwierdzeń. Pakty structured/v1 niosą dokładnie cztery wiersze potwierdzeń pod tymi kluczami:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
value dla każdego jest jednym z ośmiu zamrożonych angielskich łańcuchów wybranych przez parę (role, kind), gdzie role ∈ { seller, buyer, provider, client } i kind ∈ { standard, capacity }. Same łańcuchy są normatywnymi danymi protokołu — podpisy ML-DSA-65 obu stron wiążą się z dokładnymi bajtami przez body_hash. NIE są one zlokalizowane; podpisane ciało jest neutralne językowo. Każda zmiana sformułowania wymaga nowej wersji schematu (structured/v2).
Osiem łańcuchów, ich wyszukiwanie (acknowledgement_for(role, kind)) i uzasadnienie dla każdego są przypięte przez implementację referencyjną. Zgodne implementacje MUSZĄ emitować bajtowo identyczne wartości potwierdzeń; testy hasha ciała SHA3-256 na fikstach złotych obejmujące wszystkie cztery kombinacje ról wychwytują wszelki drift.
Kolejność wyświetlania u widza. Łańcuchy potwierdzeń zawierają frazy takie jak "described above", co zakłada, że wiersze opisu / zakresu renderują się przed potwierdzeniami. Widzowie MUSZĄ renderować tablicę terms w kolejności CBOR; zmiana kolejności łamie semantykę prozy.
Kontakt strony przeciwnej. Gdy contact Strony B to prawidłowy adres email, usługa uploadu qub automatycznie wysyła email z zaproszeniem do recenzji / współpodpisu w czasie etapu i wiąże ewentualny współpodpis z weryfikacją tego samego adresu (§9.7). Pakty, w których kontakt Strony B jest nieobecny, mogą być nadal współpodpisane, ale tylko przez kanał poza pasmem — usługa odmawia żądań współpodpisu, które nie mogą wyprodukować pasującego 15-minutowego markera weryfikacji email.
6.2 Ciało werdyktu (content_type = 0x04)
Ciało werdyktu to kanoniczne kodowanie CBOR wartości 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
}
Kanoniczna kolejność kluczy 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)
Całkowity serializowany CBOR werdyktu NIE MOŻE przekraczać 8 KB (zgodnie z wierszem rejestru powyżej).
Enum rozstrzygnięcia. Bajt na drucie jest neutralny względem intencji; cztery kategorie Right / Partial / Wrong / Unfalsifiable pokrywają przestrzeń rozstrzygnięć każdego intentu niosącego werdykt. Etykiety zależne od intencji ("Trafione" / "Dotrzymane" / "Wydane" / "Potwierdzona" dla Right itd.) są kwestią renderowania po stronie widza, rozstrzyganą względem intencji rodzicielskiego quba — drut pozostaje neutralny językowo i intencyjnie. Wartości spoza zakresu 1..=4 MUSZĄ być odrzucane przy dekodowaniu.
Powiązanie z rodzicem. Qub werdyktu NIE niesie referencji do rodzica w swoim ciele. Identyfikator transakcji Arweave rodzicielskiego quba jest emitowany jako tag magazynowy Parent-Tx-Id w czasie uploadu (warstwa tagów magazynowych §7). Utrzymuje to ciało jako samowystarczalne podpisane oświadczenie samooceny; łańcuch audytu („racja co do czego?") jest ustanawiany przez wyszukiwanie tagu Arweave.
Bezpieczeństwo URL dowodu (normatywne). Gdy evidence_url jest obecny, walidatory (po stronie kompozycji, po stronie drutu, na krawędzi Workera) MUSZĄ wymusić:
- Wyłącznie HTTPS. Łańcuch MUSI zaczynać się od sekwencji bajtów
https://. Każdy inny schemat —http,ftp,javascript,data,fileitd. — jest odrzucany. - Ograniczenie długości. ≤ 2 048 bajtów (praktyczny limit URL w przeglądarce).
- NFC + sprawdzenie wrogich punktów kodowych. Ta sama reguła co dla
titleireflection— punkty kodowe bidi-override / zero-width / tag-block / BOM / C0 / C1 są odrzucane. Definicja zgodna z Rustcrate::handle::contains_hostile_text_codepointi TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(utrzymywać w zgodzie). - Bez białych znaków, bez znaków sterujących ASCII. Białe znaki / DEL / bajty poniżej
0x20w dowolnym miejscu URL są odrzucane — zamyka wektor wstrzyknięć\n/\t, którego reguła bidi nie pokrywa. - Niepusty segment hosta. Wszystko między
https://a pierwszym/,?lub#MUSI być niepuste.
Brak pobierania po stronie serwera. Worker NIE MOŻE proxować, pobierać ani podglądać URL. Protokół przechowuje łańcuch; renderowanie odbywa się po stronie widza z rel="nofollow noopener noreferrer" target="_blank" i widocznym hostem wyświetlanym obok tekstu linku.
Refleksja. Opcjonalny tekst refleksji napisany przez twórcę („co się zmieniło, czego można się nauczyć"). Ta sama walidacja NFC + wrogich punktów kodowych co dla title. Wejście puste / złożone wyłącznie z białych znaków zwija się do nieobecnego w czasie konstrukcji.
Wersja schematu. v1 wspiera wyłącznie verdict_version = 0x01. Przyszłe rewizje schematu zwiększają ten bajt i lądują wraz z nową wersją protokołu zgodnie z §12.
7. Protokół pieczętowania
Pełna sekwencja pieczętowania. Każdy krok jest normatywny.
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.
Warstwa tagów magazynu (poza pasmem). Usługa uploadu qub dołącza celowo niewielki zestaw tagów transakcji magazynu obok zapakowanego ładunku. Content-Type=application/octet-stream jest normatywnie wymagany. Usługa referencyjna dodatkowo dołącza trzy opcjonalne tagi, gdy twórca zdecyduje się je ujawnić: Intent (walidowany allowlistą intencję komponowania — np. quote, reply, commitment), Author (odcisk klucza publicznego twórcy zgodny z §9.3 jako 64-znakowy małymi literami hex) i Parent-Tx-Id (ID transakcji magazynu rodzica qub dla łańcuchów odpowiedzi, 43-znakowy base64url).
Tag Author jest opcjonalny per qub: aplikacja referencyjna twórcy dołącza go tylko wtedy, gdy użytkownik wyraźnie włącza publiczne przypisanie w czasie pieczętowania. Gdy przełącznik jest wyłączony — domyślnie — żaden tag Author nie jest zapisywany, a qub jest nieprzypisany w łańcuchu: nic w trwałym magazynie nie łączy uploadu z uchwytem twórcy, emailem ani innymi qubami. Gdy przełącznik jest włączony, odcisk Author rozwiązuje się do wybranego @uchwytu twórcy poprzez łańcuch poświadczeń z §9.5. Relacje łańcucha odpowiedzi i Intent nie są identyfikujące. Zewnętrzna koperta (§13) chroni wewnętrzne ciało przed korelacją tekstu zaszyfrowanego — uniemożliwiając zbieraczowi rozpoznanie i masowe odszyfrowywanie uploadów o kształcie qub po opublikowaniu ich rundy drand.
Usługa referencyjna celowo NIE dołącza tagów App-Name, App-Version ani Type: każdy taki filtr jednowartościowy zwracałby cały korpus qub na zapytanie GraphQL, co jest niezgodne z zakresem poufności tylko-ciała opakowania.
Zgodny weryfikator NIE MOŻE polegać na żadnym tagu magazynu dla weryfikacji przez stronę trzecią z §11; hash ciała / qub_id / podpis wiążą się wyłącznie z wewnętrznym CBOR, nigdy ze zbiorem tagów.
8. Protokół odblokowania
Pełna sekwencja odblokowania. Każdy krok jest normatywny.
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. Podpisywanie autorstwa
9.1 Uzasadnienie
Quby są przechowywane w trwałym magazynie. Podpisy autorstwa muszą pozostawać niemożliwe do podrobienia bezterminowo, dlatego v1.0 używa postkwantowego schematu ML-DSA-65 (FIPS 204) zamiast klasycznego schematu, którego bezpieczeństwo może osłabnąć w trwałym okresie życia qub.
9.2 Rejestr algorytmów
sig_alg |
Schemat | Rozmiar klucza | Rozmiar podpisu |
|---|---|---|---|
0x00 |
Brak podpisu (niepodpisany) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1 952 bajty | 3 309 bajtów |
Widzowie MUSZĄ odrzucać nieznane wartości sig_alg.
9.3 Konstrukcja podpisanego preimage
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)
Separator domeny: "QUB_AUTHOR_SIG_V1" to 17 bajtów ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Bez wypełnienia.
Bajt końcowy: 91. bajt preimage MUSI być 0x00. Implementacja referencyjna eksponuje to jako stałą ORG_ID_PRESENT_INDIVIDUAL = 0x00 w crates/qub-core/src/signing.rs; widzowie rekonstruujący sig_input do weryfikacji MUSZĄ emitować ten sam bajt.
Zakres podpisu — co jest, a co nie jest objęte. sig_input wiąże się z czterema polami koperty: version, qub_id, body_hash, unlock_at (plus stały separator domeny i bajt org_id_present). Trzy z tych czterech są strukturalnymi niezmiennikami: qub_id sam jest wyprowadzony z version, content_type, created_at, unlock_at, outcome_at, drand_round i body_hash przez preimage z §4.1, więc jakakolwiek zmiana tych pól produkuje inne qub_id i tranzytywnie unieważnia podpis. Bezpośrednio uwierzytelniona powierzchnia jest zatem:
| Pole | Uwierzytelnione przez podpis | Jak |
|---|---|---|
version |
✓ | Bezpośrednie wejście do sig_input |
qub_id |
✓ | Bezpośrednie wejście |
body_hash |
✓ | Bezpośrednie wejście |
unlock_at |
✓ | Bezpośrednie wejście |
content_type |
✓ | Tranzytywnie, przez preimage qub_id |
created_at |
✓ | Tranzytywnie, przez preimage qub_id |
outcome_at |
✓ | Tranzytywnie, przez preimage qub_id |
drand_round |
✓ | Tranzytywnie, przez preimage qub_id (V1.2) |
body |
✓ | Tranzytywnie, przez body_hash = SHA3-256(body) |
author_pubkey |
— (niejawnie) | Klucz, który zweryfikował podpis, jest autorem z definicji |
sender_label |
✗ | Tekst tylko do wyświetlenia; modyfikowalny bez złamania podpisu |
reply_to |
✗ | Wskaźnik wątkowania; modyfikowalny bez złamania podpisu |
cosigner_pubkey / cosigner_signature |
— | Niezależnie podpisane nad tym samym sig_input (zob. §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Pola zewnętrznego SealedQub, nie wewnątrz koperty — objęte własnymi niezmiennikami strukturalnymi (spójność rundy / łańcucha), ale nie przez podpis autora. (drand_round jest teraz wiązane tranzytywnie przez preimage qub_id — zob. wyżej.) |
Implikacje bezpieczeństwa pól nieuwierzytelnionych.
- Strona z dostępem do zapisu przechowywanych bajtów mogłaby zamienić
sender_label("Alice" → "Mallory") bez unieważniania podpisu autora.author_pubkeywewnątrz koperty pozostaje prawdziwą kotwicą tożsamości — widzowie MUSZĄ wyprowadzać tożsamość wyświetlaną zauthor_pubkey(przez warstwę poświadczeń z §9.5) zamiast ufaćsender_label. - Pole
reply_tomoże być podobnie edytowane po podpisaniu. Ponieważqub_idjest adresowane treścią, atakujący nie może wskazaćreply_tona nieistniejący cel, ale może po cichu przekierować odpowiedź do innego istniejącego qub.
Implementacje wyświetlające sender_label lub reply_to użytkownikom końcowym MUSZĄ wystawiać uwierzytelnioną tożsamość (odcisk klucza publicznego, poświadczenie) jako podstawowy sygnał tożsamości, a nie etykietę.
9.4 Procedura weryfikacji
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."
Weryfikacja podpisu jest najbardziej kosztowną operacją (szczególnie ML-DSA-65). POWINNA być wykonywana po przejściu wszystkich tańszych kontroli (hash, qub_id, unlock_at).
9.5 Poświadczenia tożsamości
Poświadczenia tożsamości — mapowanie author_pubkey na rozpoznawalne przez człowieka roszczenia tożsamości takie jak uchwyt qub, adres email, uchwyt społecznościowy lub poświadczenie passkey — są progresywnym ulepszeniem po stronie widza i nie są wymagane do weryfikacji podpisu. Widzowie, którzy rozwiązują poświadczenia do tożsamości wyświetlanej, MUSZĄ stosować precedencję:
handle > email > social > fingerprint
Fallback odcisku to małymi literami hex z SHA3-256(author_pubkey); jest zawsze dostępny dla dowolnego podpisanego qub. Widzowie MOGĄ go skrócić do wyświetlania — referencyjny widz renderuje qub: z następującymi czterema pierwszymi i ostatnimi bajtami (qub:<8 hex>…<8 hex>).
Zgodny weryfikator może wykonać każdą kontrolę z §9.4 bez kontaktowania się z API qub, bez żadnej sieci poza trwałym magazynem i drand i bez żadnego wyszukiwania po stronie serwera. Rozwiązanie poświadczenia jest osobnym krokiem best-effort wykonywanym tylko po pomyślnej weryfikacji podpisu.
9.6 Wpływ na rozmiar
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Podpis | 64 bajty | 3 309 bajtów |
| Klucz publiczny | 32 bajty | 1 952 bajty |
| Razem na qub | 96 bajtów | 5 261 bajtów |
| Delta kosztu magazynu (przy ~5 USD/MB) | ~$0,0005 | ~$0,026 |
Dla qub tekstowego 500–2 000 bajtów ML-DSA-65 mniej więcej potraja przechowywany rozmiar. Bezwzględny koszt jest pomijalny.
9.7 Weryfikacja współpodpisującego (umowy bilateralne typu pact)
Dla umów bilateralnych (content_type = 0x03) druga warstwa podpisu dowodzi, że obie strony wyraziły zgodę na te same warunki.
Pola koperty:
cosigner_pubkey: Klucz publiczny ML-DSA-65 współpodpisującego (Strona B).cosigner_signature: Podpis nad tym samymsig_inputco autor (§9.3).
Oba pola MUSZĄ być obecne razem albo oba nieobecne. Jeśli obecne jest dokładnie jedno, widzowie MUSZĄ zgłosić błąd integralności.
Procedura weryfikacji:
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."
Właściwości:
- Współpodpisujący podpisuje identyczny
sig_inputjak autor — obie strony wiążą się z tym samymqub_id,body_hashiunlock_at. - Derywacja
qub_id(§4.1) NIE zawiera pól współpodpisującego. Dodanie współpodpisującego do istniejącej koperty nie zmieniaqub_id. - Pakt może być podpisany tylko przez autora (jednostronne zobowiązanie), tylko przez współpodpisującego (nietypowe) lub przez obu (pełny dowód bilateralny).
Brama wiązania email (operacyjna). Gdy zaetapowany pakt niesie kontakt email Strony B (§6.1), usługa uploadu qub MUSI odmówić żądania współpodpisu, chyba że istnieje krótkotrwały marker weryfikacji email pasujący zarówno do id etapu, jak i hash znormalizowanego email tego kontaktu. Marker jest zapisywany przez /api/v1/auth/verify gdy token magic-link niesie staging_id, a zweryfikowany adres pasuje do SHA-256(normalise_email(party_b.contact)) — gdzie normalise_email(addr) zachowuje wielkość liter części lokalnej i obniża do małych liter tylko część domeny (zgodnie z RFC 5321 §2.3.11), a SHA-256 tutaj to hash NIST FIPS 180-4 (różny od SHA3-256 używanego w derywacjach z §4) — i wygasa 900 sekund (15 minut) po wystawieniu. Jest to operacyjna brama anty-podszywania, NIE część dowodu qub on-chain — weryfikator strony trzeciej odtwarzający §11 potrzebuje tylko trwałego magazynu i drand, bez żadnego wyszukiwania po stronie serwera. Marker istnieje tylko po stronie serwera i nigdy nie jest częścią podpisanego ciała.
Wpływ na rozmiar (ML-DSA-65 autor + współpodpisujący):
| Komponent | Rozmiar |
|---|---|
| Podpis autora | 3 309 bajtów |
| Klucz publiczny autora | 1 952 bajty |
| Podpis współpodpisującego | 3 309 bajtów |
| Klucz publiczny współpodpisującego | 1 952 bajty |
| Całkowity narzut kryptograficzny | 10 522 bajty |
| Delta kosztu magazynu | ~$0,05 |
10. Renderowanie i sanityzacja Markdown
Ta sekcja jest krytyczna dla bezpieczeństwa. Widz renderuje quby tekstowe (content_type = 0x01) używając ograniczonego podzbioru Markdown.
10.1 Dozwolone elementy
- Nagłówki:
#do####(bez#####i######) - Wyróżnienie: pogrubienie (
**), kursywa (*), przekreślenie (~~) - Listy: uporządkowane (
1.) i nieuporządkowane (-,*) - Cytaty blokowe (
>) - Kod: zakresy inline (```) i bloki ogrodzone (`````)
- Linie poziome (
---) - Łamanie wierszy (dwie końcowe spacje lub pusta linia)
- Akapity
10.2 Zakazane elementy
| Element | Obsługa |
|---|---|
Surowy HTML (<div>, <script>, itd.) |
Całkowicie usuwany. Żaden HTML nie przechodzi. |
Obrazy () |
Usuwane. Składnia obrazów jest usuwana z wyjścia. |
Linki ([text](url)) |
URL renderowany jako widoczny tekst jawny. Bez auto-linkowania. Nie klikalny bez wyraźnej akcji użytkownika. |
| Niebezpieczne schematy URL | javascript:, data:, vbscript:, file: — usuwane. |
| Iframe'y, embedy, obiekty | Usuwane. |
| Encje HTML | Dekodowane do znaków wyświetlanych tylko jeśli bezpieczne. |
10.3 Implementacja
Implementacje MUSZĄ używać ścisłego parsera z allowlistą, a nie blocklistą. Zalecane podejście:
- Sparsuj Markdown używając
pulldown-cmark(lub odpowiednika). - Przejdź AST i odrzuć każdy węzeł nieobecny na allowliście (§10.1).
- Dla węzłów linków: emituj URL jako widoczny tekst, a nie jako klikalny element
<a>. - Skonwertuj przefiltrowane AST na typowaną reprezentację pośrednią (np. enum
MarkdownNodez tylko bezpiecznymi wariantami). Surowy HTML jest strukturalnie niereprezentowalny w tym IR. - Renderuj z typowanego IR do docelowej warstwy widoku (np. reaktywnych komponentów widoku, węzłów DOM). Bez konkatenacji łańcuchów HTML ani
innerHTMLw żadnym momencie.
Podejścia blocklist są kruche, ponieważ nowe rozszerzenia Markdown lub osobliwości parsera mogą wprowadzać niefiltrowane elementy. Podejście typowanego AST sprawia, że XSS jest strukturalnie niemożliwy — nie ma wariantu, który mógłby nieść dowolny HTML.
10.4 Limity rozmiaru i struktury
- Maksymalna głębokość renderowanego nagłówka:
####(H4).#####i głębsze są renderowane jako tekst pogrubiony. - Brak limitu liczby akapitów (limity rozmiaru ciała z §6 są ograniczeniem).
- Bloki kodu ogrodzonego: bez podświetlania składni w MVP. Renderowane jako monospace preformatowany tekst.
11. Weryfikacja przez stronę trzecią
Każda strona trzecia może zweryfikować publiczny qub bez współpracy qub. Procedura weryfikacji:
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 weryfikacja dowodzi:
| Dowód | Co ustanawia |
|---|---|
| Zobowiązanie | Tekst zaszyfrowany istniał do znacznika czasu bloku magazynu. |
| Integralność | Ciało tekstu jawnego pasuje do zobowiązanego hasha i nie zostało zmienione. |
| Czas | Treść była nieczytelna do rundy drand, która odpowiada wybranemu czasowi odblokowania (z zastrzeżeniem założeń bezpieczeństwa tlock i drand). |
Czego weryfikacja NIE dowodzi:
| Niedowód | Dlaczego |
|---|---|
| Autorstwo | sender_label jest dekoracyjny. Bez sig_alg ≥ 0x01, ktokolwiek mógł zapieczętować tę treść. |
| Intencja | qub dowodzi treści i czasu, a nie tego, co twórca subiektywnie miał na myśli. |
| Czas przed zdarzeniem | Inkluzja bloku magazynu może opóźnić rzeczywisty upload o minuty. Znacznik czasu zobowiązania to czas bloku, a nie moment, w którym użytkownik nacisnął "pieczętuj". |
12. Wersjonowanie
12.1 Wersja protokołu
Pole version (u8) zarówno w SealedQub, jak i QubEnvelope identyfikuje główną wersję protokołu.
- Widzowie MUSZĄ odrzucać nieznane wersje główne z wyraźnym błędem.
- Znane wersje główne MOGĄ tolerować nieznane pola opcjonalne, jeśli reguły kompatybilności wprzód na to pozwalają (pola opcjonalne nieobecne w kanonicznej kolejności kluczy są ignorowane).
- Typy treści (
content_type) i schematy podpisów (sig_alg) są bramkowane wersją: nowe wartości mogą być wprowadzane tylko wraz z nową wersją protokołu lub wyraźną aktualizacją rejestru.
12.2 Historia wersji
| Wersja | Wartość | Opis |
|---|---|---|
| v1 | 0x01 |
Publiczne quby tekstowe (content_type 0x01), umowy bilateralne typu pact (0x03, schemat structured/v1, autor + współpodpisujący ML-DSA-65), tlock, SHA3-256 |
12.3 Kompatybilność wprzód
Widz v1 napotykający QubEnvelope z nieznanymi kluczami opcjonalnej mapy CBOR (klucze nieobecne w kanonicznej kolejności z §3.2) POWINIEN zignorować te klucze i kontynuować weryfikację używając znanych pól. Pozwala to na przyszłe drobne dodatki (np. nowe metadane) bez wymagania głównego skoku wersji.
Widz v1 napotykający sig_alg = 0x01 (ML-DSA-65), ale pozbawiony wsparcia weryfikacji ML-DSA-65 POWINIEN wyświetlić treść qub z notą "podpis obecny, ale nieweryfikowalny", a nie odrzucać qub całkowicie. Implementacja referencyjna dzisiaj odrzuca każdą wartość sig_alg inną niż 0x00 i 0x01, ponieważ rejestr v1 nie zawiera innego ważnego algorytmu — ścisłe odrzucenie i miękka porażka są obserwacyjnie identyczne aż do zarejestrowania trzeciego algorytmu. Powyższe zachowanie miękkiej porażki staje się nośne, gdy §9.2 dopuści nowy wpis, a widz referencyjny zostanie zaktualizowany do miękkiej porażki w tym momencie.
12.4 Wersja zewnętrznej koperty
OuterWrapper opisany w §13 niesie własny bajt version, niezależny od SealedQub.version i QubEnvelope.version. Te dwie przestrzenie wersji ewoluują oddzielnie: przyszły postkwantowo bezpieczny zamiennik symetryczny zwiększa bajt opakowania bez dotykania wewnętrznej wersji protokołu, a przyszły dodatek warstwy protokołu (np. nowe pole koperty) zwiększa wewnętrzną wersję bez dotykania bajtu opakowania.
OUTER_WRAPPER_VERSION_* |
Wartość | Algorytm | Status |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM z liczbą jednorazową 12 bajtów, znacznikiem uwierzytelnienia 16 bajtów, AAD powiązanym z qub_id |
Domyślny v1 |
| — | 0x02–0xFF |
Zarezerwowane | Przyszłość |
Widzowie MUSZĄ odrzucać nieznane wersje opakowania z wyraźnym błędem. Protokół celowo utrzymuje przestrzeń wersji opakowania wąską aż do pojawienia się konkretnego sterownika migracji (np. wytyczne NIST faworyzujące inny AEAD); slot 0x02 zostanie przydzielony w tej samej rewizji, która wprowadza algorytm.
13. Zewnętrzna koperta szyfrowania
13.1 Uzasadnienie
Warstwy protokołu (QubEnvelope → tlock → SealedQub) sprawiają, że zapieczętowany qub jest zablokowany czasowo: ciało jest nieczytelne do unlock_at i opublikowania podpisu rundy drand. Jednak po odblokowaniu podpis rundy jest publiczny, a kanoniczny kształt CBOR SealedQub jest rozpoznawalny, więc zbieracz, który zaindeksował transakcje trwałego magazynu, mógłby masowo odszyfrować cały korpus qub.
Zewnętrzna koperta szyfrowania zamyka ten kanał, wstawiając dodatkową warstwę symetryczną AEAD między kanonicznym SealedQubCbor a bajtami wysłanymi do trwałego magazynu. 256-bitowy klucz K żyje tylko we fragmencie URL linku udostępniania i na urządzeniach użytkowników; przeglądarki nie transmitują fragmentów URL do serwerów, więc qub.social, każda brama magazynu i każdy CDN przed którymkolwiek z nich są obserwacyjnie ślepi na K. Każdy qub w trwałym magazynie jest zatem nieprzezroczystym tekstem zaszyfrowanym, którego tekst jawny jest nie do odzyskania bez URL, który twórca zdecydował się udostępnić.
Efekt netto:
- Domyślna odporność na enumerację. Zapakowane bajty w trwałym magazynie są bajtowo nierozróżnialne od dowolnego tekstu zaszyfrowanego. Strategia zbieracza "zapytaj GraphQL o uploady o kształcie qub, masowo odszyfruj publicznymi podpisami drand" nie kończy się tekstem jawnym.
- Postawa prywatności krypto-niszczenia. qub.social dosłownie nie może odszyfrować własnego korpusu. Wezwania sądowe sięgają tekstu zaszyfrowanego, nie tekstu jawnego.
- Dwupoziomowa drabina poufności. Domyślnie = dostęp kontrolowany linkiem (ta sekcja). Prywatne quby szyfrowane na odbiorcę (zarezerwowana funkcja Fazy 2, jeszcze nie wyspecyfikowana) warstwują się na wierzchu jako drugi poziom.
13.2 Warstwowanie
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)
Pieczętowanie i odblokowanie w warstwie protokołu (§7, §8) są niezmienione poniżej granicy opakowania; opakowanie podłącza się w miejscu wywołania seal() i odłącza w miejscu wywołania unlock().
13.3 Struktura danych 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
}
Niezmienniki pól.
versionMUSI równać się0x01dla bajtów opakowania v1.0.qub_idMUSI równać się poluqub_idSealedQub odzyskanego po rozpakowaniu. Krok rozpakowania nie egzekwuje tego bezpośrednio (powiązanie AEAD AAD czyni manipulację bajtową niemożliwą), ale warstwa odblokowania sprawdza relację tranzytywnie: jeśli twórca opakujeSealedQubCbor, którego wewnętrznyqub_idnie pasuje doqub_idopakowania, §8 krok 11 zawiedzie.nonceMUSI mieć 96 bitów (12 bajtów), generowane świeżo przez CSPRNG dla każdej operacji opakowania. Ponowne użycie liczby jednorazowej pod tym samym kluczem pozwala na ataki ponownego użycia liczby jednorazowej AEAD, które odzyskują tekst jawny; producenci MUSZĄ traktować pary (key,nonce) jako jednorazowe.ciphertextto wyjście AES-256-GCM: bajty tekstu zaszyfrowanego skonkatenowane z 16-bajtowym znacznikiem uwierzytelnienia.ciphertext.len() == SealedQubCbor.len() + 16dokładnie.
Kodowanie CBOR. Kanoniczny CBOR zgodnie z §3, z tą samą regułą uporządkowania kluczy (sortowane rosnąco po długości zakodowanych bajtów, następnie leksykograficznie). Cztery klucze to:
| Klucz | Zakodowane bajty | Kolejność |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
Pierwszy bajt CBOR OuterWrapper to zatem nagłówek mapy o określonej długości dla 4-wpisowej mapy (0xA4).
13.4 Powiązanie AAD z qub_id
Opakowanie wiąże qub_id jako dodatkowe dane uwierzytelnione AEAD. Jest to nośna strukturalna obrona przeciwko trzem klasom ataków:
| Atak | Obrona |
|---|---|
Przenieś tekst zaszyfrowany pod innym polem qub_id w opakowaniu |
Niezgodność AAD → uwierzytelnienie AEAD zawodzi |
| Wymieszaj fragment URL qub A z bajtami trwałego magazynu qub B | Niezgodność AAD → uwierzytelnienie AEAD zawodzi |
Manipuluj polem qub_id opakowania po uploadzie |
Niezgodność AAD → uwierzytelnienie AEAD zawodzi |
Niesienie qub_id w tekście jawnym opakowania nie osłabia istotnie odporności na enumerację — qub_id sam jest hashem SHA3-256 preimage z §4.1 bez odzyskiwalnego preimage ze skrótu, a enumerator, który już zebrał bajty opakowania, nie dowiaduje się z widocznego qub_id niczego, czego nie mógłby wywnioskować z samego istnienia uploadu.
13.5 Algorytmy opakowania i rozpakowania
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
Kolaps trybu porażki. Niewłaściwy K, niewłaściwa liczba jednorazowa, niezgodność AAD i sfałszowany tekst zaszyfrowany — wszystkie produkują ten sam błąd DECRYPT_FAILED. Jest to celowa właściwość AEAD: rozróżnianie trybu porażki tworzyłoby kanał boczny, który zdalny atakujący mógłby sondować, wysyłając zniekształcone opakowania i mierząc czas odpowiedzi. Implementacje referencyjne MUSZĄ zwijać wszystkie porażki AEAD do jednego kształtu błędu.
13.6 Materiał klucza i dystrybucja
Klucz opakowujący K to 256-bitowa wartość losowa jednostajna generowana per qub przez CSPRNG. Implementacje referencyjne pozyskują go z:
- Twórca WASM:
getrandom(WebCrypto pod backendemwasm_js). - Trasa pieczętowania po stronie Workera:
crypto.getRandomValues.
Dystrybucja: K MUSI być zakodowany jako URL-bezpieczny base64 (RFC 4648 §5, bez wypełnienia) i dołączony do linku udostępniania jako komponent fragmentu:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
Fragment nigdy nie jest transmitowany do żadnego serwera przez zgodną przeglądarkę. Kanały odzyskiwania (indeks historii po stronie serwera, opcjonalne automatyczne wysyłanie email) trwale zachowujące pełny link udostępniania — wraz z fragmentem — poza urządzeniem użytkownika są wyraźnym kompromisem względem domyślnej postawy krypto-niszczenia i MUSZĄ być bramkowane wyraźną zgodą użytkownika.
Utrata fragmentu. Jeśli użytkownik zgubi fragment URL i nie ma kanału odzyskiwania, qub jest nieczytelny. To jest nośny kompromis projektu i MUSI być ujawniony użytkownikowi w czasie pieczętowania. MVP wzmacnia ujawnienie w czasie pieczętowania wyraźnym tekstem "zachowaj ten URL" i kanałem odzyskiwania z weryfikowanym email dla użytkowników, którzy się zdecydują.
13.7 Poza zakresem dla tej sekcji
- Podpisywanie autorstwa (§9) jest niezmienione: podpisy są obliczane wewnątrz wewnętrznego
QubEnvelopei są odzyskiwane po rozpakowaniu → odszyfrowaniu tlock → parsowaniu CBOR. - Prywatne quby szyfrowane na odbiorcę (zarezerwowana funkcja Fazy 2, jeszcze nie wyspecyfikowana) komponują się na wierzchu tego opakowania jako drugi poziom poufności; oba poziomy mogą być aktywne jednocześnie.
- Pakty (§6, content_type
0x03) są opakowywane dokładnie jak quby tekstowe; opakowanie jest bajtowo ślepe na wewnętrzny typ treści.
13.8 Publiczne quby (pominięcie opakowania)
Zewnętrzne opakowanie jest opcjonalne w warstwie dostarczania. Twórca może zapieczętować qub jako publiczny — wtedy kanoniczny SealedQubCbor jest zapisywany do trwałego magazynu bezpośrednio, bez warstwy OuterWrapper i bez klucza K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
Publiczny qub jest zablokowany czasowo, ale nie bramkowany linkiem: pozostaje nieczytelny do opublikowania jego rundy drand (warstwa tlock jest niezmieniona), lecz po odblokowaniu każdy, kto ma arweave_tx_id, może go odszyfrować — żaden fragment URL nie jest wymagany, ponieważ nie ma K. To celowy kompromis dla powierzchni, którymi musi sterować serwer: e-maile z powiadomieniem o ujawnieniu, osadzenia stron trzecich oraz bogatsze SEO po ujawnieniu — wszystkie potrzebują linku działającego bez sekretu, którego serwer nigdy nie posiada (§13.6).
Konsekwencje, które producent MUSI uwzględnić:
- Brak odporności na enumerację. Publiczne quby z założenia rezygnują z właściwości odporności na enumerację z §13.1. Referencyjna usługa uploadu znakuje je (i tylko je) tagiem trwałego magazynu
Visibility: public, dzięki czemu są celowo wykrywalne; prywatne quby nie niosą takiego tagu i zachowują swoją bajtową nierozróżnialność. - Tytuł jawny ujawniony w czasie pieczętowania. Pole
titlez §3.2 jest tekstem jawnym wewnątrzSealedQubCbor. Pod opakowaniem pozostaje ukryte, dopóki widz nie dostarczyK; bez opakowania jest czytelne dla wszystkich w trwałym magazynie od momentu uploadu, przed odblokowaniem. Zgodne aplikacje twórcy MUSZĄ ujawnić to w czasie pieczętowania. - Wykrywanie jest strukturalne. Zgodny widz/osadzenie rozróżnia oba kształty przez parsowanie: bajty, które parsują się jako
OuterWrapper, idą ścieżką rozpakowania kluczemK; bajty, które parsują się jako gołySealedQubCbor, są przyjmowane bezpośrednio. Żadna flaga na drucie nie jest wymagana, aqub_idnie wiąże widoczności — ta sama treść jest bajtowo identyczna w warstwieSealedQub, niezależnie od tego, czy zapieczętowano ją jako publiczną, czy prywatną.
Prywatny (opakowany) pozostaje wartością domyślną; publiczny jest wyraźnym wyborem twórcy dla pojedynczego quba.
14. Wektory testowe
14.1 Derywacja 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
Implementacje MUSZĄ produkować identyczne wartości body_hash i qub_id dla tego wejścia. Ten wektor testowy POWINIEN być pierwszym napisanym testem jednostkowym. Kanoniczne wartości powyżej zostały obliczone przez implementację referencyjną i MUSZĄ pasować bit-w-bit. Historyczne układy preimage (przed uruchomieniem — żaden żywy qub nie zależał od tych wartości): qub_id V1.0 o długości 92 bajtów wynosił 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; qub_id V1.1 o długości 100 bajtów (po wpleceniu outcome_at_or_zero) wynosił b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 wplata drand_round i podbija separator domeny do QUB_ID_V2.
14.2 Mapowanie rundy odblokowania
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 Round-trip kanonicznego CBOR
Implementacje MUSZĄ zweryfikować, że serialize(parse(serialize(qub))) == serialize(qub) dla wszystkich prawidłowych wejść. To jest test właściwości, a nie pojedynczy wektor.
14.4 PactTerms CBOR (content_type 0x03)
Input:
pact_version = 1
title = "Scooter deposit"
terms = [
{ key: "Item", value: "Honda Metropolitan scooter" },
{ key: "Price", value: "$100" },
{ key: "Deposit", value: "$10" }
]
party_a = { label: "Alice" }
party_b = { label: "Bob", contact: "bob@example.com" }
notes = absent
Canonical CBOR key order (PactTerms):
"notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)
Canonical CBOR key order (PactTerm):
"key"(4) < "value"(6)
Canonical CBOR key order (PartyIdentifier):
"label"(6) < "contact"(8)
Kanoniczne bajty CBOR i body_hash SHA3-256 są obliczane przez implementację referencyjną. Implementacje MUSZĄ produkować bajtowo identyczny CBOR dla tego wejścia.
Implementacje MUSZĄ także zweryfikować, że serialize(parse(serialize(pact))) == serialize(pact) dla wszystkich prawidłowych wejść PactTerms (test właściwości).
14.5 Wektory wieloryzykowe zewnętrznej koperty
Zewnętrzna koperta (§13) ma osobną kanoniczną fikturę w crates/qub-core/tests/vectors/wrapper_v1.json. Każdy przypadek ustala krotkę (key, nonce, qub_id, sealed_cbor) jako nieprzezroczyste wejścia hex i potwierdza określone wyjście expected_wrapper_hex. Obie implementacje referencyjne konsumują ten sam plik 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).
Fikstura obecnie przypina trzy przypadki:
| Przypadek | Pokrycie |
|---|---|
basic-text-public |
Najmniejszy realistyczny kształt SealedQub; bez pól opcjonalnych. Ustanawia kanoniczny kształt opakowania dla typowego qub v1.0. |
with-recipient-pubkey |
SealedQub z ustawionym recipient_pubkey (ścieżka Fazy 2). Inny zbiór kluczy wewnętrznego CBOR, inny qub_id. |
longer-body |
Ciało ~4 KiB — ćwiczy wielobajtowe prefiksy długości CBOR wewnątrz zarówno wewnętrznej koperty, jak i zewnętrznego tekstu zaszyfrowanego. |
Implementacje MUSZĄ produkować bajtowo identyczny expected_wrapper_hex dla zapisanych wejść. Regeneracja fikstury wymaga QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors i jest zarezerwowana dla celowych zmian formatu.
15. Zarządzanie profilem kryptograficznym (przyszłość)
Ta sekcja jest informacyjna dla v1 i staje się normatywna przy pierwszym wprowadzeniu drugiego algorytmu do dowolnego z prymitywów kryptograficznych qub.
15.1 Obecna postawa
Protokół v1 wiąże dokładnie jeden algorytm na prymityw:
- Podpis: ML-DSA-65 (
sig_alg = 0x01; klucz publiczny 1952 bajtów, podpis 3309 bajtów) i niepodpisany (sig_alg = 0x00). Rejestr §9.2 nie definiuje żadnych innych wartości; weryfikator v1 MUSI odrzucić każdysig_algspoza{0x00, 0x01}. Przyszły wpis Ed25519 jest przewidywany (§15.3), ale nie jest przydzielony w v1. - Blokada czasowa: tylko drand quicknet — hash łańcucha, klucz publiczny, czas genesis i okres to stałe parametry sieci przenoszone przez referencyjny
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) iconfig/drand-endpoints.json. - Zewnętrzna koperta: tylko AES-256-GCM v1 (§13).
Weryfikatory obecnie hardcodują długości klucza i podpisu na prymityw. Format na drucie nie eksponuje żadnej powierzchni zwinności.
15.2 Zamierzony kształt
Gdy drugi algorytm wejdzie do protokołu, weryfikator zostanie skonfigurowany dla nazwanego CryptoProfile (np. ExqubV1) wymieniającego dokładny zbiór dozwolonych wartości na prymityw — sig_algs, łańcuchy drand, wersje opakowania, typy treści. Profil jest ustalony w czasie weryfikacji, nigdy negocjowany w paśmie. Każda wartość poza aktywnym profilem jest odrzucana.
Gwarantuje to, że dodanie ML-DSA-87 lub aktywacja Ed25519 nie może retroaktywnie osłabić istniejących konfiguracji weryfikatora: weryfikator v1 pozostaje weryfikatorem v1 nawet po opublikowaniu profilu v2.
15.3 Warunki wyzwalające
Promuj §15 do statusu normatywnego, gdy zaproponowane jest którekolwiek z poniższych:
- Drugi bajt
sig_alg(aktywacja Ed25519, ML-DSA-87 lub jakikolwiek nowy wpis w rejestrze §9). - Drugi łańcuch drand w użyciu produkcyjnym.
- Druga wersja zewnętrznego opakowania.
Do tego czasu §15 jest placeholderem, który ustala kształt migracji, aby przyszłe PR-y lądowały przy znanym celu, a nie ponownie spierały się o powierzchnię negocjacji od zera.