qub-protokollspecifikation
qub är ett protokoll för kryptografiska temporala åtaganden: ett system för att försegla ord till ett framtida datum och bevisa, när det datumet kommer, exakt vad som sades och när.
Tre primitiver får det att fungera. drand är en decentraliserad slumpbaken — avslöjandedatumet upprätthålls av fysik, inte av någon parts goda vilja. Permanent offentlig lagring är ett manipuleringssäkert offentligt arkiv — ingen part kan redigera eller radera en qub när den har förseglats. ML-DSA-65 är en postkvantsignatur — varje qub är knuten till ett nyckelpar vars hemliga del aldrig lämnar författarens enhet.
Tillsammans gör dessa primitiver ett uttalande som är tidslåst, manipuleringsuppenbart och tillskrivbart — ett kvitto vars värde växer i takt med att världens förmåga att fabricera det förflutna förbättras.
Resten av detta dokument är den normativa specifikationen som krävs för interoperabla implementationer.
qub-protokollspecifikation
| Fält | Värde |
|---|---|
| Version | 1.0 (protokollversion 0x01, yttre omslag version 0x01) |
| Datum | 2026-05-01 |
| Status | Utkast |
| Granskat till | 2026-05-01 |
Detta dokument är den normativa protokollspecifikationen för qub-systemet för tidsbestämda åtaganden. Det definierar datastrukturer, serialiseringsregler, härledningsformler och verifieringsprocedurer som krävs för interoperabla implementationer.
Omfattning: protokollskiktet är avsiktligt språkneutralt — qub-kroppen är ogenomskinlig klartext / markdown / paktbyte, och språkmedveten rendering är läsarens ansvar (qub.social-webbapp, <qub-embed>-iframe, MCP-klienter, etc.).
1. Notation och konventioner
| Notation | Betydelse |
|---|---|
u8, u64, i64 |
Osignerade/signerade heltal med specificerad bitbredd |
[u8; N] |
Bytearray med fast längd om N byte |
Vec<u8> |
Bytearray med variabel längd |
Option<T> |
Värde av typ T, eller frånvarande |
String |
UTF-8-textsträng, NFC-normaliserad |
| ` | |
SHA3-256(x) |
NIST SHA3-256-hash av bytesträngen x (FIPS 202) |
ceil(x) |
Takfunktion: minsta heltal ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Mest signifikant byte först |
Alla heltal i pre-image-konstruktioner kodas som big-endian bytearrayer med fast bredd (i64 → 8 byte, u8 → 1 byte) om inte annat anges.
Alla tidsstämplar är Unix-sekunder i UTC.
2. Datastrukturer
2.1 ComposeQub (skaparens tillstånd i minnet)
Serialiseras inte till CBOR. Skrivs inte till permanent lagring. Lokal för skaparappen.
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 (dekrypterad nyttolast)
Serialiseras med kanonisk CBOR (§3). Krypterad inuti SealedQub. Detta är strukturen som bevisar innehållsintegritet efter dekryptering.
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 — när verkligheten avkunnar sin dom (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
}
Baslinje (osignerad text-qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, alla Option-fält frånvarande.
Andra v1-konfigurationer: content_type = 0x03 (paktkropp, se §6.1); sig_alg = 0x01 (ML-DSA-65) med author_signature och author_pubkey närvarande (se §9.3); cosigner_pubkey och cosigner_signature närvarande tillsammans för motundertecknade pakter (se §9.7); reply_to satt till föräldra-qubens qub_id för svarskedjor (se §9.3 för signaturomfångskonsekvenserna).
2.3 SealedQub (kanoniskt trådformat)
Serialiseras med kanonisk CBOR (§3). Skrivs till permanent lagring. Detta är artefakten på kedjan.
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 — visas på verdict-watch-CTA
// före avslöjande; speglar QubEnvelope.outcome_at;
// bundet till qub_id via §4.1-pre-imagen.
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 (läsarens applikationstillstånd)
Serialiseras inte till CBOR. Lokal för läsarappen. Konstrueras efter framgångsrik dekryptering och verifiering.
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 — bärs vidare från QubEnvelope.outcome_at / SealedQub.outcome_at; driver avslöjandesidans verdiktbevakningsblock (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. Kanonisk CBOR-profil
All serialisering av SealedQub och QubEnvelope MUST följa denna profil. Två implementationer som ges samma logiska struktur MUST producera identiska byte.
3.1 Kodningsregler
| Regel | Specifikation |
|---|---|
| Standard | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Nyckelordning i kartor | Sorterade efter kodad bytelängd först (kortare före längre), sedan lexikografiskt (byte-för-byte för kodningar av samma längd) |
| Heltalskodning | Kortaste form: 0–23 i initial byte; 24–255 i 2 byte; 256–65535 i 3 byte; etc. |
| Längdkodning | Endast bestämda längder. Inga arrayer, kartor, bytestrings eller textstrings av obestämd längd (additional info = 31 är förbjudet). |
| Taggar | Inga CBOR-taggar (major-typ 6 är förbjudet). |
| Flyttal | Inga flyttal (major-typ 7 värden 0xF9–0xFB är förbjudna). |
| Textstrings | UTF-8-kodade, NFC-normaliserade (Unicode Normalization Form C). |
| Bytestrings | Råa byte. Ingen base64-kodning i CBOR-skiktet. |
| Dubblettnycklar | Avvisa med fel. Tolkare MUST NOT tyst acceptera dubblettnycklar i kartor. |
| Enkla värden | Endast true (0xF5), false (0xF4) och null (0xF6) är tillåtna. |
| Frivilliga fält | Frånvarande frivilliga fält utelämnas helt från CBOR-kartan (kodas inte som null). Närvarande frivilliga fält inkluderas i sorterad nyckelordning. |
3.2 Verifierade kanoniska nyckelordningar
Dessa nyckelordningar är normativa. Implementationer MUST avge nycklar i exakt denna ordning. Debug-assertioner SHOULD verifiera ordning i icke-release-byggen.
QubEnvelope (version 0x01, osignerad, alla frivilliga fält frånvarande):
"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)
Härledning av QubEnvelopes nyckelordning: varje nyckel är en CBOR-textsträng. Kodad längd = 1 byte header + stränglängd (för strängar under 24 byte). Sortera först efter total kodad längd, sedan lexikografiskt för nycklar av samma längd.
SealedQub (version 0x01, publik, ingen mottagare):
"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 (paktkropp, 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 (rad i terms-arrayen):
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier (party_a / party_b-karta):
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 Referens för bytekodning
| Typ | CBOR-kodning | Exempel |
|---|---|---|
| SHA3-256-hash (32 byte) | 0x58 0x20 + 32 byte |
body_hash, qub_id |
| Tidsstämplar (i64) | Major-typ 0 (positiv) eller 1 (negativ), kortaste kodning | Unix-sekunder |
| Version (u8, värde 1) | 0x01 (enskild byte) |
|
| Innehållstyp (u8, värde 1) | 0x01 (enskild byte) |
|
| sig_alg (u8, värde 0) | 0x00 (enskild byte) |
|
| ML-DSA-65-signatur (3 309 byte) | 0x59 0x0C 0xED + 3 309 byte |
author_signature, cosigner_signature |
| ML-DSA-65 publik nyckel (1 952 byte) | 0x59 0x07 0xA0 + 1 952 byte |
author_pubkey, cosigner_pubkey |
4. Normativa härledningar
4.1 qub_id
qub_id identifierar unikt en qub och binder QubEnvelope till SealedQub. Den härleds deterministiskt från envelopinnehåll.
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
Kodning av domänseparator: Strängen "QUB_ID_V2" är 9 ASCII-byte. En enskild 0x00-utfyllnadsbyte läggs till för att nå 10 byte för justering. Implementationer MUST använda exakt dessa 10 byte: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
Kodning av outcome_at: V1.1 utvidgade pre-imagen från 92 till 100 byte för att vika in det frivilliga outcome_at-fältet i bindningen. Frånvarande outcome_at kodas som 8 nollbyte; protokollvalidatorerna avvisar outcome_at <= 0 överallt så att detta sentinelvärde inte kan kollidera med ett legitimt värde. Se §3.2 (trådformat) och den interna tasks/verdict-uplift-plan.md för verdiktmekaniken som motiverar detta fält.
Kodning av drand_round: V1.2 utvidgade pre-imagen från 100 till 108 byte för att vika in drand_round (mål-drand-rundan, §4.3) i bindningen, och höjde domänseparatorn till QUB_ID_V2. Detta binder tidslåsrundan in i qub-identiteten: en gateway kan inte binda om chiffertexten till en annan runda (t.ex. en redan passerad) än den som det visade unlock_at antyder. Upplåsningsproceduren (§8) verifierar dessutom att rundan som är inbakad i tlock-chiffertextens stanza matchar unlock_round(unlock_at), så att den visade upplåsningstiden bevisligen är den runda som styr dekrypteringen.
Egenskaper:
- En ändring i något fält i QubEnvelope (body, tidsstämplar, innehållstyp, version) producerar ett annat qub_id.
- qub_id beräknas före kryptering. Både QubEnvelope och SealedQub bär samma qub_id. Läsaren verifierar att de matchar efter dekryptering.
- qub_id beror inte på
sender_label,author_signatureellerauthor_pubkey. Detta innebär att samma innehåll förseglat vid samma tid producerar samma qub_id oavsett vem som signerar det. - Ändring av SealedQubs
title(med allt annat oförändrat) ändrarqub_idviatitle_hash. En gateway kan därför inte byta ut klartexttiteln som visas på nedräkningen utan att ogiltigförklara qub-identiteten. - Ändring av SealedQubs
outcome_at(med allt annat oförändrat) ändrarqub_idvia pre-imagen. En gateway kan inte byta ut det verdict-on-datum som visas på nedräkningen före avslöjande utan att ogiltigförklara qub-identiteten. - Ändring av
drand_round(med allt annat oförändrat) ändrarqub_idvia pre-imagen. En gateway kan inte binda om tidslås-chiffertexten till en annan runda utan att ogiltigförklara qub-identiteten; i kombination med stanza-runda-kontrollen vid upplåsning i §8 är det visadeunlock_atden runda som faktiskt styr dekrypteringen.
4.2 body_hash
body_hash = SHA3-256(body)
Där body är den råa Vec<u8>-innehållsnyttolasten. För text-qubs är detta den UTF-8-kodade qub-kroppen.
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
Där title är den frivilliga klartexttiteln som visas på läsarens nedräkning före avslöjande (se §3.2). NFC-normalisering körs vid hashtid så att sammanfattningen är stabil över visuellt likvärdiga kodpunktssekvenser. Sentinelvärdet med alla nollor är reserverat för det frånvarande fallet; en tom sträng avvisas vid den kanoniska CBOR-gränsen som en icke-kanonisk kodning av "frånvarande" (den kanoniska kodningen utelämnar fältet helt).
4.3 Mappning av upplåsningsrunda
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Parameter | Källa | Exempel |
|---|---|---|
unlock_at |
Användarvald Unix-sekund UTC | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
drand chain info (genesis_time) |
1595431050 |
chain_period_seconds |
drand chain info (period) |
30 |
ceil()-operationen väljer första drand-rundan vars avslöjandetid är ≥ unlock_at. Detta säkerställer att quben inte blir dekrypterbar före den valda upplåsningstiden.
Gränsfall: om (unlock_at - chain_genesis_time) är exakt jämnt delbart med chain_period_seconds är resultatet exakt den rundan — quben låses upp precis vid den rundans avslöjandetid.
Validering: unlock_at MUST ligga i framtiden vid förseglingstid. unlock_at MUST NOT vara mer än 10 år från created_at (för att begränsa risken av långhorisont-drand-beroende; gränssnittet SHOULD varna för upplåsningsdatum bortom 2 år).
5. Newtypes för trådformat
Newtypes för trådformat ger kompileringstidssäkerhet mot att förväxla CBOR-byte med JSON, rå klartext eller andra bytekodningar.
| Typ | Innehåller | Producerad av | Konsumerad av |
|---|---|---|---|
SealedQubCbor |
Kanonisk CBOR av SealedQub | serialize_sealed_qub() |
Uppladdning till permanent lagring, läsarhämtning |
QubEnvelopeCbor |
Kanonisk CBOR av QubEnvelope | serialize_qub_envelope() |
tlock-krypteringsinmatning, tlock-dekrypteringsutmatning |
5.1 Konstruktionsregler
// 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 Validering vid konstruktion
from_encoded() SHOULD validera att indata börjar med ett giltigt CBOR-kartheader. Fullständig strukturell validering sker vid tolkningstid, inte konstruktionstid, för att undvika dubbeltolkning.
6. Register för innehållstyper
| Värde | Typ | Max kroppsstorlek | Anmärkningar |
|---|---|---|---|
0x00 |
Reserverad (ogiltig) | — | MUST NOT användas |
0x01 |
Klartext (UTF-8, begränsad Markdown) | 50 KB betald / 10 KB gratis | Se §10 för renderingsregler. Uppdelningen gratis / betald upprätthålls av uppladdningstjänsten; protokollskiktets hårda tak är 50 KB. |
0x02 |
Reserverad (framtida) | — | Tilldelad för en framtida innehållstyp; ogiltig i v1. Läsare MUST avvisa enligt regeln nedan. |
0x03 |
Pakt (bilateralt avtal, CBOR-kropp) | 100 KB | Kroppen är kanonisk CBOR PactTerms (§6.1). Motundertecknarsignering enligt §9.7. |
0x04 |
Verdikt (skaparens självbetygsättning, CBOR-kropp) | 8 KB | Kroppen är kanonisk CBOR VerdictBody (§6.2). Avges endast av den systemsidiga verdict-intentionen. Föräldrarelationen ligger på Parent-Tx-Id-Arweave-taggen, inte på kroppen. Se verdict-uplift-plan §3.4. |
Läsare MUST avvisa okända innehållstyper med ett tydligt användarsynligt felmeddelande. Läsare MUST NOT försöka rendera okända typer som text.
6.1 Paktkropp (content_type = 0x03)
En paktkropp är den kanoniska CBOR-kodningen av ett PactTerms-värde:
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)> }
Kanoniska CBOR-nyckelordningar för alla tre kartorna anges i §3.2. Totalt serialiserad pakt-CBOR MUST NOT överskrida 100 KB (matchar §6).
Schemadiskriminator. Den första raden i terms för en structured/v1-pakt MUST vara { key: "pact_schema", value: "structured/v1" }. Rader utan denna markör är "custom"-pakter och får ingen strukturerad validering eller schemamedveten rendering.
Frusna bekräftelseplatser. structured/v1-pakter bär exakt fyra bekräftelserader under dessa nycklar:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
value för var och en är en av åtta frusna engelska strängar valda av paret (role, kind), där role ∈ { seller, buyer, provider, client } och kind ∈ { standard, capacity }. Strängarna själva är normativa protokolldata — båda parternas ML-DSA-65-signaturer förbinder sig till de exakta bytena via body_hash. De är INTE lokaliserade; den signerade kroppen är språkneutral. Varje formuleringsändring kräver en ny schemaversion (structured/v2).
De åtta strängarna, deras uppslagning (acknowledgement_for(role, kind)) och motiveringen för var och en är fastställda av referensimplementationen. Konforma implementationer MUST avge byte-identiska bekräftelsevärden; golden-fixture SHA3-256 body-hash-tester som täcker alla fyra rollkombinationer fångar all drift.
Visningsordning för läsare. Bekräftelsesträngarna innehåller fraser såsom "described above", som förutsätter att beskrivning / omfattning-raderna renderas före bekräftelserna. Läsare MUST rendera terms-arrayen i CBOR-ordning; omordning bryter prosasemantiken.
Motpartskontakt. När Part B:s contact är en giltig e-postadress skickar qub-uppladdningstjänsten automatiskt en granskning / motundertecknings-inbjudan via e-post vid stage-tid och binder den slutliga motunderskriften till verifiering av samma adress (§9.7). Pakter vars Part B-kontakt är frånvarande kan fortfarande motundertecknas, men endast via en kanal utanför bandet — tjänsten avvisar motunderskriftsförfrågningar som inte kan producera en matchande 15-minuters e-postverifieringsmarkör.
6.2 Verdiktkropp (content_type = 0x04)
En verdiktkropp är den kanoniska CBOR-kodningen av ett VerdictBody-värde:
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
}
Kanonisk CBOR-nyckelordning:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
Totalt serialiserad verdikt-CBOR MUST NOT överskrida 8 KB (matchar registerraden ovan).
Utfallsenum. Wire-byten är intentionsneutral; de fyra kategorierna Right / Partial / Wrong / Unfalsifiable täcker varje verdiktbärande intentions utfallsrymd. Per-intentionsetiketter ("Rätt sagt" / "Höll det" / "Levererat" / "Bekräftad" för Right, etc.) är en läsarsidig renderingsfråga som löses mot föräldra-qubbens intention — wire förblir språk- och intentionsneutral. Värden utanför 1..=4 MUST avvisas vid avkodning.
Föräldralänkning. En verdikt-qub bär INTE föräldrareferensen i sin kropp. Föräldra-qubbens Arweave-transaktions-id avges som Parent-Tx-Id-lagringstaggen vid uppladdning (§7 lagringstaggsskikt). Detta håller kroppen som ett självständigt signerat uttalande av självbedömning; granskningskedjan ("rätt om vad?") etableras via Arweave-taggsökningen.
Säkerhet för bevis-URL (normativt). När evidence_url är närvarande MUST validerare (kompositionssidan, wire-sidan, Worker-kanten) upprätthålla:
- Endast HTTPS. Strängen MUST börja med bytesekvensen
https://. Varje annat schema —http,ftp,javascript,data,file, etc. — avvisas. - Längdtak. ≤ 2 048 byte (webbläsarens praktiska URL-gräns).
- NFC + kontroll av fientliga kodpunkter. Samma regel som för
titleochreflection— bidi-override / nollbredd / tag-block / BOM / C0 / C1-kodpunkter avvisas. Definitionen matchar Rust-crate::handle::contains_hostile_text_codepointoch TS-workers/api/src/utils/unicode.ts::isHostileCodepoint(håll i lås). - Inga blanksteg, inga ASCII-styrtecken. Blanksteg / DEL / byte under
0x20var som helst i URL:en avvisas — stänger\n/\t-injektionsvektorn som bidi-regeln inte täcker. - Icke-tomt värdsegment. Allt mellan
https://och första/,?eller#MUST vara icke-tomt.
Ingen serversidig hämtning. Workern MUST NOT proxyera, hämta eller förhandsgranska URL:en. Protokollet lagrar en sträng; renderingen sker läsarsidigt med rel="nofollow noopener noreferrer" target="_blank" och en synlig värd visad bredvid länktexten.
Reflektion. Frivillig skaparskriven reflektionstext ("vad förändrades, vad lärde du dig"). Samma NFC + kontroll av fientliga kodpunkter som för title. Tom / endast-blanksteg-inmatning faller till frånvarande vid konstruktion.
Schemaversion. v1 stöder endast verdict_version = 0x01. Framtida schemarevisioner bumpar denna byte och landar tillsammans med en ny protokollversion enligt §12.
7. Förseglingsprotokoll
Den fullständiga förseglingssekvensen. Varje steg är normativt.
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.
Lagringstaggskikt (utanför bandet). qub-uppladdningstjänsten fäster en avsiktligt liten uppsättning lagringstransaktionstaggar tillsammans med den inslagna nyttolasten. Content-Type=application/octet-stream krävs normativt. Referenstjänsten fäster dessutom tre frivilliga taggar när skaparen väljer att lyfta fram dem: Intent (allowlist-validerad kompositionsavsikt — t.ex. quote, reply, commitment), Author (skaparens §9.3-pubkey-fingeravtryck som 64-teckens gemen hex) och Parent-Tx-Id (föräldra-qubens lagringstransaktions-ID för svarskedjor, 43-teckens base64url).
Author-taggen är opt-in per qub: referensskaparappen fäster den endast när användaren uttryckligen aktiverar offentlig tillskrivning vid förseglingstid. När växeln är av — standardläget — skrivs ingen Author-tagg och quben är otillskriven på kedjan: inget i permanent lagring länkar uppladdningen till en skapares handtag, e-post eller andra qubs. När växeln är på löses Author-fingeravtrycket till skaparens valda @handle via §9.5-attesteringskedjan. Svarskedjerelationer och Intent är icke-identifierande. Det yttre omslaget (§13) skyddar den inre kroppen från chiffertextkorrelation — och förhindrar att en skördare känner igen och massdekrypterar qub-formade uppladdningar efter att deras drand-runda publiceras.
Referenstjänsten fäster avsiktligt INTE App-Name-, App-Version- eller Type-taggar: varje sådant ensamvärdesfilter skulle returnera hela qub-korpusen till en GraphQL-fråga, vilket är inkonsekvent med omslagets kroppsbara konfidentialitetsomfång.
En konform verifierare MUST NOT vara beroende av någon lagringstagg för §11 tredjepartsverifiering; body-hashen / qub_id / signaturen förbinder sig endast till den inre CBOR:en, aldrig till taggsetet.
8. Upplåsningsprotokoll
Den fullständiga upplåsningssekvensen. Varje steg är normativt.
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. Signering av författarskap
9.1 Motivering
qubs lagras i permanent lagring. Signaturer för författarskap måste förbli oförfalskbara på obestämd tid, varför v1.0 använder det postkvantsäkra schemat ML-DSA-65 (FIPS 204) snarare än ett klassiskt schema vars säkerhet kan försämras inom qubens permanenta livstid.
9.2 Algoritmregister
sig_alg |
Schema | Nyckelstorlek | Signaturstorlek |
|---|---|---|---|
0x00 |
Ingen signatur (osignerad) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1 952 byte | 3 309 byte |
Läsare MUST avvisa okända sig_alg-värden.
9.3 Konstruktion av signerad pre-image
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)
Domänseparator: "QUB_AUTHOR_SIG_V1" är 17 ASCII-byte: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Ingen utfyllnad.
Avslutande byte: den 91:a pre-image-byten MUST vara 0x00. Referensimplementationen exponerar detta som konstanten ORG_ID_PRESENT_INDIVIDUAL = 0x00 i crates/qub-core/src/signing.rs; läsare som rekonstruerar sig_input för verifiering MUST avge samma byte.
Signaturomfång — vad som är och inte är täckt. sig_input förbinder sig till fyra envelopfält: version, qub_id, body_hash, unlock_at (plus den fasta domänseparatorn och org_id_present-byten). Tre av dessa fyra är strukturella invarianter: qub_id härleds själv från version, content_type, created_at, unlock_at och body_hash via §4.1 pre-image, så varje ändring av content_type eller created_at producerar ett annat qub_id och ogiltigförklarar signaturen transitivt. Den direkt autentiserade ytan är därför:
| Fält | Autentiserad av signatur | Hur |
|---|---|---|
version |
✓ | Direkt indata till sig_input |
qub_id |
✓ | Direkt indata |
body_hash |
✓ | Direkt indata |
unlock_at |
✓ | Direkt indata |
content_type |
✓ | Transitivt, via qub_id pre-image |
created_at |
✓ | Transitivt, via qub_id pre-image |
outcome_at |
✓ | Transitivt, via qub_id pre-image |
drand_round |
✓ | Transitivt, via qub_id pre-image (V1.2) |
body |
✓ | Transitivt, via body_hash = SHA3-256(body) |
author_pubkey |
— (implicit) | Nyckeln som verifierade signaturen är författaren per definition |
sender_label |
✗ | Endast visningstext; mutabel utan signaturbrytning |
reply_to |
✗ | Trådpekare; mutabel utan signaturbrytning |
cosigner_pubkey / cosigner_signature |
— | Oberoende signerad över samma sig_input (se §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Yttre SealedQub-fält, inte inuti envelopen — täckta av sina egna strukturella invarianter (runda / kedjekonsistens) men inte av författarsignaturen. (drand_round binds nu transitivt via qub_id-pre-imagen — se ovan.) |
Säkerhetsimplikationer av icke-autentiserade fält.
- En part med skrivåtkomst till de lagrade bytena kan byta
sender_label("Alice" → "Mallory") utan att ogiltigförklara författarsignaturen.author_pubkeyinuti envelopen förblir det sanna identitetsankaret — läsare MUST härleda visningsidentiteten frånauthor_pubkey(via §9.5-attesteringsskiktet) snarare än att lita påsender_label. - Ett
reply_to-fält kan likaledes redigeras efter signering. Eftersomqub_idär innehållsadresserat kan en angripare inte riktareply_tomot ett mål som inte finns, men de kan tyst om-förälder ett svar till en annan befintlig qub.
Implementationer som visar sender_label eller reply_to för slutanvändare MUST lyfta fram den autentiserade identiteten (pubkey-fingeravtryck, attestering) som den primära identitetssignalen, inte etiketten.
9.4 Verifieringsprocedur
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."
Signaturverifiering är den dyraste operationen (särskilt ML-DSA-65). Den SHOULD utföras efter att alla billigare kontroller (hash, qub_id, unlock_at) har passerat.
9.5 Identitetsattesteringar
Identitetsattesteringar — mappningen av author_pubkey till mänskligt igenkännbara identitetspåståenden såsom ett qub-handtag, en e-postadress, ett socialt handtag eller en passkey-uppgift — är en progressiv förbättring på läsarsidan och är inte obligatoriska för signaturverifiering. Läsare som löser attesteringar till en visningsidentitet MUST tillämpa företrädet:
handle > email > social > fingerprint
Fingeravtrycks-fallbacken är gemen hex av SHA3-256(author_pubkey); den är alltid tillgänglig för varje signerad qub. Läsare MAY förkorta den för visning — referensläsaren renderar qub: följt av de första och sista fyra byten (qub:<8 hex>…<8 hex>).
En konform verifierare kan slutföra varje kontroll i §9.4 utan att kontakta qub-API:et, utan något nätverk utöver permanent lagring och drand, och utan någon serversideuppslagning. Attesteringsupplösning är ett separat steg på bästa möjliga sätt som utförs endast efter att signaturverifiering har lyckats.
9.6 Storlekspåverkan
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Signatur | 64 byte | 3 309 byte |
| Publik nyckel | 32 byte | 1 952 byte |
| Totalt per qub | 96 byte | 5 261 byte |
| Lagringskostnadsdelta (vid ~5 USD/MB) | ~0,0005 USD | ~0,026 USD |
För en text-qub på 500–2 000 byte tredubblar ML-DSA-65 ungefär den lagrade storleken. Den absoluta kostnaden är försumbar.
9.7 Motundertecknarverifiering (bilaterala paktavtal)
För bilaterala avtal (content_type = 0x03) bevisar ett andra signaturskikt att båda parter samtyckte till samma villkor.
Envelopfält:
cosigner_pubkey: ML-DSA-65 publik nyckel för motundertecknaren (Part B).cosigner_signature: Signatur över sammasig_inputsom författaren (§9.3).
Båda fält MUST vara närvarande tillsammans eller båda frånvarande. Om exakt ett är närvarande MUST läsare rapportera ett integritetsfel.
Verifieringsprocedur:
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."
Egenskaper:
- Motundertecknaren signerar det identiska
sig_inputsom författaren — båda parter förbinder sig till sammaqub_id,body_hashochunlock_at. qub_id-härledning (§4.1) inkluderar INTE motundertecknarfält. Att lägga till en motundertecknare till en befintlig envelop ändrar intequb_id.- En pakt kan vara endast författarsignerad (ensidigt åtagande), endast motundertecknad (ovanligt) eller båda (fullt bilateralt bevis).
Grind för e-postbindning (operativt). När en staged pakt bär en Part B-e-postkontakt (§6.1) MUST qub-uppladdningstjänsten avvisa motundertecknarförfrågan om inte en kortlivad e-postverifieringsmarkör finns som matchar både staging-ID:t och den normaliserade e-posthashen av den kontakten. Markören skrivs av /api/v1/auth/verify när magic-link-token bär en staging_id och den verifierade adressen matchar SHA-256(normalise_email(party_b.contact)) — där normalise_email(addr) bevarar lokaldelens skiftläge och gemener endast domändelen (enligt RFC 5321 §2.3.11), och SHA-256 här är NIST FIPS 180-4-hashen (skild från SHA3-256 som används i §4-härledningar) — och förfaller 900 sekunder (15 minuter) efter utfärdande. Detta är en operativ anti-imitations-grind, INTE en del av qub-beviset på kedjan — en tredjepartsverifierare som spelar upp §11 behöver endast permanent lagring och drand, utan någon serversideuppslagning. Markören finns endast på serversidan och är aldrig en del av den signerade kroppen.
Storlekspåverkan (ML-DSA-65 författare + motundertecknare):
| Komponent | Storlek |
|---|---|
| Författarsignatur | 3 309 byte |
| Författares publika nyckel | 1 952 byte |
| Motundertecknarsignatur | 3 309 byte |
| Motundertecknares publika nyckel | 1 952 byte |
| Totalt kryptoöverhang | 10 522 byte |
| Lagringskostnadsdelta | ~0,05 USD |
10. Markdown-rendering och sanering
Denna sektion är säkerhetskritisk. Läsaren renderar text-qubs (content_type = 0x01) med en begränsad Markdown-delmängd.
10.1 Tillåtna element
- Rubriker:
#till####(inte#####eller######) - Betoning: fet (
**), kursiv (*), genomstrykning (~~) - Listor: ordnade (
1.) och oordnade (-,*) - Blockcitat (
>) - Kod: inline-spans (```) och fenced blocks (`````)
- Horisontella linjer (
---) - Radbrytningar (två avslutande mellanslag eller blank rad)
- Stycken
10.2 Förbjudna element
| Element | Hantering |
|---|---|
Rå HTML (<div>, <script>, etc.) |
Tas helt bort. Ingen HTML passerar igenom. |
Bilder () |
Tas bort. Bildsyntax tas bort från utdata. |
Länkar ([text](url)) |
URL renderas som synlig klartext. Inte auto-länkad. Inte klickbar utan explicit användaråtgärd. |
| Farliga URL-scheman | javascript:, data:, vbscript:, file: — tas bort. |
| Iframes, embeds, objects | Tas bort. |
| HTML-entiteter | Avkodas till visningstecken endast om de är säkra. |
10.3 Implementation
Implementationer MUST använda en strikt allowlist-tolkare, inte en blocklist. Den rekommenderade metoden:
- Tolka Markdown med
pulldown-cmark(eller motsvarande). - Gå igenom AST:n och släng varje nod som inte finns i allowlist (§10.1).
- För länknoder: avge URL:en som synlig text, inte som ett klickbart
<a>-element. - Konvertera den filtrerade AST:n till en typad mellanrepresentation (t.ex. en
MarkdownNode-enum med endast säkra varianter). Rå HTML är strukturellt icke-representerbar i denna IR. - Rendera från den typade IR:n till mållagrets vy (t.ex. reaktiva vykomponenter, DOM-noder). Ingen HTML-strängkonkatenering eller
innerHTMLvid någon punkt.
Blocklist-metoder är ömtåliga eftersom nya Markdown-utvidgningar eller tolkningskvistar kan introducera ofiltrerade element. Den typade AST-metoden gör XSS strukturellt omöjligt — det finns ingen variant som kan bära godtycklig HTML.
10.4 Storleks- och strukturgränser
- Maximalt renderad rubrikdjup:
####(H4).#####och djupare renderas som fet text. - Ingen gräns på antal stycken (kroppsstorleksgränser i §6 är begränsningen).
- Fenced kodblock: ingen syntaxmarkering i MVP. Renderas som monospace preformaterad text.
11. Tredjepartsverifiering
Vilken tredje part som helst kan verifiera en publik qub utan qubs medverkan. Verifieringsproceduren:
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.
Vad verifiering bevisar:
| Bevis | Vad det fastställer |
|---|---|
| Åtagande | Chiffertexten existerade vid lagringsblockets tidsstämpel. |
| Integritet | Klartextkroppen matchar den åtagna hashen och har inte ändrats. |
| Tidsbestämning | Innehållet var oläsbart fram till drand-rundan, som motsvarar den valda upplåsningstiden (med förbehåll för tlock- och drand-säkerhetsantaganden). |
Vad verifiering INTE bevisar:
| Icke-bevis | Varför |
|---|---|
| Författarskap | sender_label är dekorativ. Utan sig_alg ≥ 0x01 kan vem som helst ha förseglat detta innehåll. |
| Avsikt | Quben bevisar innehåll och tidsbestämning, inte vad skaparen subjektivt menade. |
| Före-händelsetidsbestämning | Lagringsblockinkludering kan släpa efter faktisk uppladdning med minuter. Åtagandetidsstämpeln är blocktiden, inte det ögonblick användaren tryckte "försegla". |
12. Versionshantering
12.1 Protokollversion
version-fältet (u8) i både SealedQub och QubEnvelope identifierar huvudprotokollversionen.
- Läsare MUST avvisa okända huvudversioner med ett tydligt felmeddelande.
- Kända huvudversioner MAY tolerera okända frivilliga fält om regler för framåtkompatibilitet tillåter (frivilliga fält som saknas i den kanoniska nyckelordningen ignoreras).
- Innehållstyper (
content_type) och signaturscheman (sig_alg) är versionsgrindade: nya värden får endast införas tillsammans med en ny protokollversion eller en explicit registeruppdatering.
12.2 Versionshistorik
| Version | Värde | Beskrivning |
|---|---|---|
| v1 | 0x01 |
Publika text-qubs (content_type 0x01), bilaterala paktavtal (0x03, schemat structured/v1, ML-DSA-65 författare + motundertecknare), tlock, SHA3-256 |
12.3 Framåtkompatibilitet
En v1-läsare som stöter på en QubEnvelope med okända frivilliga CBOR-kartnycklar (nycklar som inte finns i §3.2-kanonisk ordning) SHOULD ignorera dessa nycklar och fortsätta verifieringen med kända fält. Detta tillåter framtida mindre tillägg (t.ex. nya metadata) utan att kräva en huvudversionsökning.
En v1-läsare som stöter på sig_alg = 0x01 (ML-DSA-65) men saknar ML-DSA-65-verifieringsstöd SHOULD visa qub-innehållet med ett meddelande om "signatur närvarande men inte verifierbar", inte avvisa quben helt. Referensimplementationen idag avvisar varje sig_alg-värde annat än 0x00 och 0x01 eftersom v1-registret inte innehåller någon annan giltig algoritm — strikt avvisande och soft-fail är observationellt identiska tills en tredje algoritm registreras. Soft-fail-beteendet ovan blir bärande när §9.2 inför en ny post, och referensläsaren kommer att uppdateras för att soft-faila vid den tidpunkten.
12.4 Yttre omslagsversion
OuterWrappern som beskrivs i §13 bär sin egen version-byte, oberoende av SealedQub.version och QubEnvelope.version. De två versionsutrymmena utvecklas separat: en framtida postkvantsäker symmetrisk ersättning ökar omslagsbyten utan att röra den inre protokollversionen, och ett framtida protokollskiktstillägg (t.ex. ett nytt envelopfält) ökar den inre versionen utan att röra omslagsbyten.
OUTER_WRAPPER_VERSION_* |
Värde | Algoritm | Status |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM med 12-byte nonce, 16-byte autentiseringstagg, AAD bunden till qub_id |
v1-standard |
| — | 0x02–0xFF |
Reserverad | Framtid |
Läsare MUST avvisa okända omslagsversioner med ett tydligt felmeddelande. Protokollet håller avsiktligt omslagsversionsutrymmet smalt tills en konkret migrationsdrivare dyker upp (t.ex. NIST-vägledning som föredrar en annan AEAD); en 0x02-plats kommer att tilldelas i samma revision som introducerar algoritmen.
13. Yttre krypteringsomslag
13.1 Motivering
Protokollskikten (QubEnvelope → tlock → SealedQub) gör en förseglad qub tidslåst: kroppen är oläsbar fram till unlock_at och drand-rundsignaturen har publicerats. Efter upplåsning är emellertid rundsignaturen offentlig och den kanoniska CBOR-formen för SealedQub är igenkännbar, så en skördare som indexerade transaktioner i permanent lagring skulle kunna massdekryptera hela qub-korpusen.
Det yttre krypteringsomslaget stänger den kanalen genom att lägga in ett ytterligare symmetriskt AEAD-skikt mellan den kanoniska SealedQubCbor och de byte som skrivs till permanent lagring. Den 256-bitars nyckeln K lever endast i URL-fragmentet på leveranslänken och på användarenheter; webbläsare överför inte URL-fragment till servrar, så qub.social, varje lagringsgateway och varje CDN framför båda är observationellt blind för K. Varje qub i permanent lagring är därför en ogenomskinlig chiffertext vars klartext är oåterhämtningsbar utan URL:en som skaparen valde att dela.
Nettoeffekt:
- Uppräkningsimmunitet som standard. Inslagna byte i permanent lagring är byte-omöjliga att skilja från godtycklig chiffertext. En skördarstrategi av "GraphQL-fråga efter qub-formade uppladdningar, massdekryptera med offentliga drand-signaturer" avslutas inte med klartext.
- Krypto-strimling-integritetshållning. qub.social kan bokstavligen inte dekryptera sin egen korpus. Stämningar når chiffertext, inte klartext.
- Tvåskiktad konfidentialitetstrappa. Standard = länkstyrd åtkomst (denna sektion). Mottagarkrypterade privata qubs (en reserverad Fas 2-funktion, ännu inte specificerad) lager ovanpå som det andra skiktet.
13.2 Skiktning
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)
Försegling och upplåsning vid protokollskiktet (§7, §8) är oförändrade under omslagsgränsen; omslaget fästs vid anropsplatsen för seal() och tas bort vid anropsplatsen för unlock().
13.3 Datastruktur för 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
}
Fältinvarianter.
versionMUST vara lika med0x01för v1.0-omslagsbyte.qub_idMUST vara lika medqub_id-fältet i den SealedQub som återställs efter uppackning. Uppackningssteget upprätthåller inte detta direkt (AEAD AAD-bindningen gör manipulation på byte-nivå omöjlig), men upplåsningsskiktet kontrollerar relationen transitivt: om en skapare slår in enSealedQubCborvars inrequb_idinte matchar omslagetsqub_idmisslyckas §8 steg 11.nonceMUST vara 96 bitar (12 byte), genererad fräsch av en CSPRNG för varje inslagningsoperation. Återanvändning av en nonce under samma nyckel tillåter AEAD-nonce-återanvändningsattacker som återhämtar klartexten; producenter MUST behandla (key,nonce)-par som engångs.ciphertextär AES-256-GCM-utdata: chiffertextbyte konkatenerat med 16-byte autentiseringstaggen.ciphertext.len() == SealedQubCbor.len() + 16exakt.
CBOR-kodning. Kanonisk CBOR enligt §3, med samma nyckelordningsregel (sorterad efter kodad bytelängd stigande, sedan lexikografiskt). De fyra nycklarna är:
| Nyckel | Kodade byte | Ordning |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
Den första byten av OuterWrapper-CBOR:en är därför det bestämda kart-headern för en 4-posters karta (0xA4).
13.4 AAD-bindning till qub_id
Omslaget binder qub_id som AEAD ytterligare autentiserade data. Detta är det bärande strukturella försvaret mot tre attackklasser:
| Attack | Försvar |
|---|---|
Flytta chiffertext under ett annat qub_id-fält i omslaget |
AAD-omatchning → AEAD-autentisering misslyckas |
| Blanda URL-fragmentet för qub A med byten i permanent lagring för qub B | AAD-omatchning → AEAD-autentisering misslyckas |
Manipulera qub_id-fältet i omslaget efter uppladdning |
AAD-omatchning → AEAD-autentisering misslyckas |
Att bära qub_id i omslagets klartext försvagar inte uppräkningsimmuniteten meningsfullt — qub_id är själv en SHA3-256-hash av §4.1-pre-imagen utan återställbar pre-image från sammanfattningen, och en uppräknare som redan skördade omslagsbyten lär sig inget från det synliga qub_id som de inte kunde sluta sig till från själva uppladdningens existens.
13.5 Inslagnings- och uppackningsalgoritmer
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
Kollaps av felläge. Fel K, fel nonce, AAD-omatchning och manipulerad chiffertext producerar alla samma DECRYPT_FAILED-fel. Detta är en avsiktlig AEAD-egenskap: att skilja felläget skulle skapa en sidokanal som en fjärrangripare kunde sondera genom att skicka felformade omslag och tida svaret. Referensimplementationer MUST kollapsa alla AEAD-fel till en enskild felform.
13.6 Nyckelmaterial och distribution
Inslagningsnyckeln K är ett 256-bitars enhetligt slumpmässigt värde som genereras per qub av en CSPRNG. Referensimplementationerna hämtar det från:
- WASM-skapare:
getrandom(WebCrypto underwasm_js-backenden). - Workerns serversideförseglingsrutt:
crypto.getRandomValues.
Distribution: K MUST kodas som URL-säker base64 (RFC 4648 §5, ingen utfyllnad) och läggas till leveranslänken som fragmentkomponenten:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
Fragmentet överförs aldrig till någon server av en konform webbläsare. Återställningskanaler (serverside-historikindex, opt-in automatisk e-postsändning) som persisterar den fullständiga leveranslänken — inklusive fragmentet — bortom användarens enhet är en uttrycklig avvägning mot den standardmässiga krypto-strimlings-hållningen och MUST gating på explicit användarsamtycke.
Fragmentförlust. Om en användare förlorar URL-fragmentet och inte har någon återställningskanal är quben oläsbar. Detta är den bärande avvägningen i designen och MUST avslöjas för användaren vid förseglingstid. MVP:n förstärker förseglingstidsavslöjandet med uttrycklig "spara denna URL"-text och en verifierad-e-poståterställningskanal för användare som väljer att delta.
13.7 Utanför omfattning för denna sektion
- Författarskapssignering (§9) är oförändrad: signaturer beräknas inuti den inre
QubEnvelopeoch återställs efter uppackning → tlock-dekryptering → CBOR-tolkning. - Mottagarkrypterade privata qubs (en reserverad Fas 2-funktion, ännu inte specificerad) komponerar ovanpå detta omslag som ett andra konfidentialitetsskikt; båda skikten kan vara aktiva samtidigt.
- Pakter (§6, content_type
0x03) slås in exakt som text-qubs; omslaget är byte-blind för inre innehållstyp.
13.8 Offentliga qubs (utelämnande av omslag)
Det yttre omslaget är valfritt på leveransskiktet. En skapare kan försegla en qub som offentlig, varvid den kanoniska SealedQubCbor skrivs till permanent lagring direkt, utan något OuterWrapper-skikt och utan någon nyckel K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
En offentlig qub är tidslåst men inte länkstyrd: den förblir oläsbar fram till att dess drand-runda publiceras (tlock-skiktet är oförändrat), men efter upplåsning kan vem som helst som har arweave_tx_id dekryptera den — inget URL-fragment krävs, eftersom det inte finns någon K. Detta är den avsiktliga avvägningen för ytor som servern måste driva: avslöjandenotifierings-e-post, tredjepartsinbäddningar och rikare SEO efter avslöjande behöver alla en länk som fungerar utan en hemlighet som servern aldrig håller (§13.6).
Följder som en producent MÅSTE ta hänsyn till:
- Ingen uppräkningsimmunitet. Offentliga qubs avstår per konstruktion från §13.1-egenskapen uppräkningsimmunitet. Referensuppladdningstjänsten stämplar en
Visibility: public-tagg i permanent lagring på dem (och endast dem) så att de är avsiktligt upptäckbara; privata qubs bär ingen sådan tagg och behåller sin byte-omöjlighet att skilja. - Klartexttitel exponerad vid förseglingstid. Fältet
titlei §3.2 är klartext inutiSealedQubCbor. Under omslaget är det dolt fram till att en läsare tillhandahållerK; utan omslaget är det världsläsbart i permanent lagring från uppladdningsögonblicket, före upplåsning. Konforma skapar-appar MÅSTE avslöja detta vid förseglingstid. - Detektion är strukturell. En konform läsare/inbäddning skiljer de två formerna genom tolkning: byte som tolkas som
OuterWrappertar uppackning-med-K-vägen; byte som tolkas som en barSealedQubCboraccepteras direkt. Ingen wire-flagga krävs, ochqub_idbinder inte synlighet — samma innehåll är byte-identiskt påSealedQub-skiktet vare sig det förseglas offentligt eller privat.
Privat (inslaget) förblir standard; offentligt är ett uttryckligt skaparval per qub.
14. Testvektorer
14.1 qub_id-härledning
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
Implementationer MUST producera identiska body_hash- och qub_id-värden för denna indata. Denna testvektor SHOULD vara den första enhetstest som skrivs. De kanoniska värdena ovan beräknades av referensimplementationen och MUST matcha bit-för-bit. Historiska pre-image-layouter (före lansering — inga aktiva qubs berodde på dessa): det 92-byte stora V1.0-qub_id var 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; det 100-byte stora V1.1-qub_id (efter invikning av outcome_at_or_zero) var b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 viker in drand_round och höjer domänseparatorn till QUB_ID_V2.
14.2 Mappning av upplåsningsrunda
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 Kanonisk CBOR-rundresa
Implementationer MUST verifiera att serialize(parse(serialize(qub))) == serialize(qub) för alla giltiga indata. Detta är ett egenskapstest, inte en enskild vektor.
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)
De kanoniska CBOR-byten och SHA3-256 body_hash beräknas av referensimplementationen. Implementationer MUST producera byte-identisk CBOR för denna indata.
Implementationer MUST också verifiera att serialize(parse(serialize(pact))) == serialize(pact) för alla giltiga PactTerms-indata (egenskapstest).
14.5 Tvärspråkliga vektorer för yttre omslag
Det yttre omslaget (§13) har en separat kanonisk fixtur i crates/qub-core/tests/vectors/wrapper_v1.json. Varje fall fixerar en tupel (key, nonce, qub_id, sealed_cbor) som ogenomskinlig hex-indata och hävdar en specifik expected_wrapper_hex-utdata. Båda referensimplementationerna konsumerar samma JSON-fil:
- 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).
Fixturen nålar för närvarande tre fall:
| Fall | Täckning |
|---|---|
basic-text-public |
Minsta realistiska SealedQub-form; inga frivilliga fält. Etablerar den kanoniska omslagsformen för en v1.0-typisk qub. |
with-recipient-pubkey |
SealedQub med recipient_pubkey satt (Fas 2-väg). Annan inre CBOR-nyckeluppsättning, annat qub_id. |
longer-body |
~4 KiB kropp — utövar fleruppdelade CBOR-längdsprefix inuti både den inre envelopen och den yttre chiffertexten. |
Implementationer MUST producera byte-identiskt expected_wrapper_hex för de registrerade indata. Att regenerera fixturen kräver QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors och är reserverat för avsiktliga formatändringar.
15. Styrning av kryptoprofil (framtid)
Denna sektion är informativ för v1 och blir normativ första gången en andra algoritm träder in i någon av qubs kryptografiska primitiver.
15.1 Nuvarande hållning
Protokoll v1 binder exakt en algoritm per primitiv:
- Signatur: ML-DSA-65 (
sig_alg = 0x01; 1952-byte publik nyckel, 3309-byte signatur) och osignerad (sig_alg = 0x00). §9.2-registret definierar inga andra värden; en v1-verifierare MUST avvisa varjesig_algutanför{0x00, 0x01}. En framtida Ed25519-post är förutsedd (§15.3) men är inte tilldelad i v1. - Tidslås: Endast drand quicknet — kedjehashen, den publika nyckeln, genesistiden och perioden är fasta nätverksparametrar som bärs av referensens
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) ochconfig/drand-endpoints.json. - Yttre omslag: Endast AES-256-GCM v1 (§13).
Verifierare hårdkodar för närvarande nyckel- och signaturlängder per primitiv. Ingen agilitetsyta exponeras av trådformatet.
15.2 Avsedd form
När en andra algoritm träder in i protokollet kommer verifieraren att konfigureras för en namngiven CryptoProfile (t.ex. ExqubV1) som listar den exakta uppsättningen tillåtna värden per primitiv — sig_algs, drand-kedjor, omslagsversioner, innehållstyper. Profilen fastställs vid verifieringstid, aldrig förhandlas i bandet. Varje värde utanför den aktiva profilen avvisas.
Detta garanterar att tillägg av ML-DSA-87 eller aktivering av Ed25519 inte retroaktivt kan försvaga befintliga verifierarkonfigurationer: en v1-verifierare förblir en v1-verifierare även efter att en v2-profil publiceras.
15.3 Utlösningsvillkor
Befordra §15 till normativ status när något av följande föreslås:
- En andra
sig_alg-byte (Ed25519-aktivering, ML-DSA-87 eller någon ny post i §9-registret). - En andra drand-kedja i produktionsanvändning.
- En andra yttre omslagsversion.
Tills dess är §15 en platshållare som fastställer migrationsformen så att framtida PR:er landar mot ett känt mål snarare än att åter-förhandla förhandlingsytan från grunden.