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:

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:

  1. Endast HTTPS. Strängen MUST börja med bytesekvensen https://. Varje annat schema — http, ftp, javascript, data, file, etc. — avvisas.
  2. Längdtak. ≤ 2 048 byte (webbläsarens praktiska URL-gräns).
  3. NFC + kontroll av fientliga kodpunkter. Samma regel som för title och reflection — bidi-override / nollbredd / tag-block / BOM / C0 / C1-kodpunkter avvisas. Definitionen matchar Rust-crate::handle::contains_hostile_text_codepoint och TS-workers/api/src/utils/unicode.ts::isHostileCodepoint (håll i lås).
  4. Inga blanksteg, inga ASCII-styrtecken. Blanksteg / DEL / byte under 0x20 var som helst i URL:en avvisas — stänger \n/\t-injektionsvektorn som bidi-regeln inte täcker.
  5. 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.

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:

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:

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

10.2 Förbjudna element

Element Hantering
Rå HTML (<div>, <script>, etc.) Tas helt bort. Ingen HTML passerar igenom.
Bilder (![alt](url)) 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:

  1. Tolka Markdown med pulldown-cmark (eller motsvarande).
  2. Gå igenom AST:n och släng varje nod som inte finns i allowlist (§10.1).
  3. För länknoder: avge URL:en som synlig text, inte som ett klickbart <a>-element.
  4. 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.
  5. Rendera från den typade IR:n till mållagrets vy (t.ex. reaktiva vykomponenter, DOM-noder). Ingen HTML-strängkonkatenering eller innerHTML vid 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


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_alg0x01 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.

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
0x020xFF 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:

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.

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:

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

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:

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:

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:

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:

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.