qub Protokolspecifikation

qub er en protokol til kryptografiske tidsmæssige forpligtelser: et system til at forsegle ord til en fremtidig dato og bevise, når den dato indtræffer, præcis hvad der blev sagt og hvornår.

Tre primitiver får det til at fungere. drand er et decentraliseret tilfældigheds-beacon — afsløringsdatoen håndhæves af fysikken, ikke af nogen parts velvilje. Permanent offentligt lager er et manipulationssikret offentligt lager — ingen part kan redigere eller slette en qub, når den først er forseglet. ML-DSA-65 er en post-kvante digital signatur — hver qub er bundet til et nøglepar, hvis hemmelighed aldrig forlader forfatterens enhed.

Tilsammen producerer disse primitiver en erklæring, der er tidslåst, tampersikker og tilskrivelig — en kvittering, hvis værdi vokser i takt med, at verdens evne til at fabrikere fortiden forbedres.

Resten af dette dokument er den normative specifikation, der kræves til interoperable implementeringer.


qub-protokolspecifikation

Felt Værdi
Version 1.0 (protokolversion 0x01, ydre wrapper-version 0x01)
Dato 2026-05-01
Status Udkast
Gennemgået til og med 2026-05-01

Dette dokument er den normative protokolspecifikation for qub-systemet til tidsmæssige forpligtelser. Det definerer datastrukturer, serialiseringsregler, udledningsformler og verifikationsprocedurer, der kræves til interoperable implementeringer.

Anvendelsesområde: protokollaget er bevidst sprogneutralt — qub-bodyen er uigennemsigtig klartekst / markdown / pact-bytes, og lokalitetsbevidst rendering er læserens ansvar (qub.social-webappen, <qub-embed>-iframen, MCP-klienter osv.).


1. Notation og konventioner

Notation Betydning
u8, u64, i64 Heltal uden/med fortegn af angivet bitbredde
[u8; N] Byte-array med fast længde på N bytes
Vec<u8> Byte-array med variabel længde
Option<T> Værdi af typen T, eller fraværende
String UTF-8-tekststreng, NFC-normaliseret
`
SHA3-256(x) NIST SHA3-256-hash af bytestrengen x (FIPS 202)
ceil(x) Loftfunktion: mindste heltal ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Mest betydende byte først

Alle heltal i preimage-konstruktioner kodes som big-endian-byte-arrays med fast bredde (i64 → 8 bytes, u8 → 1 byte), medmindre andet er angivet.

Alle tidsstempler er Unix-sekunder i UTC.


2. Datastrukturer

2.1 ComposeQub (skaberens in-memory-tilstand)

Ikke serialiseret til CBOR. Ikke skrevet til permanent lager. Lokalt i skaberens app.

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 (dekrypteret payload)

Serialiseret med kanonisk CBOR (§3). Krypteret inde i SealedQub. Dette er strukturen, der beviser indholdsintegritet 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 — when reality renders judgment (verdict-uplift-plan §3.1)
    sender_label:        Option<String>,  // Decorative; not authenticated in MVP
    reply_to:            Option<[u8; 32]>,// Parent qub_id for reply chains; not in qub_id preimage; not signed (see §9.3)
    body:                Vec<u8>,         // Content payload (UTF-8 for text, CBOR for pact)
    body_hash:           [u8; 32],        // SHA3-256(body) (see §4.2)
    sig_alg:             u8,              // Signature algorithm (see §9.2)
    author_signature:    Option<Vec<u8>>, // Set when sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Set when sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
    cosigner_signature:  Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}

Baseline (usigneret tekst-qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, alle Option-felter fraværende.

Andre v1-konfigurationer: content_type = 0x03 (pact-body, se §6.1); sig_alg = 0x01 (ML-DSA-65) med author_signature og author_pubkey til stede (se §9.3); cosigner_pubkey og cosigner_signature til stede sammen for medunderskrevne pagter (se §9.7); reply_to sat til den overordnede qubs qub_id for svarkæde-qubs (se §9.3 for konsekvenserne af signaturomfanget).

2.3 SealedQub (kanonisk wire-format)

Serialiseret med kanonisk CBOR (§3). Skrevet til permanent lager. Dette er artefaktet on-chain.

SealedQub {
    version:           u8,              // Protocol major version (0x01 for v1)
    qub_id:            [u8; 32],        // Same as QubEnvelope.qub_id
    visibility:        u8,              // 0x01 = public; v1 viewers reject other values
    unlock_at:         i64,             // Unix seconds UTC
    outcome_at:        Option<i64>,     // V1.1 — surfaced on the verdict-watch CTA
                                        //   before reveal; mirrors QubEnvelope.outcome_at;
                                        //   bound to qub_id via the §4.1 preimage.
    drand_chain_id:    String,          // drand chain hash (hex string)
    drand_round:       u64,             // Target drand round number
    tlock_ciphertext:  Vec<u8>,         // tlock-encrypted QubEnvelope CBOR bytes
    recipient_pubkey:  Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
                                        //   but not interpreted by the v1 reference viewer
    title:             Option<String>,  // Plaintext title surfaced on the viewer
                                        //   countdown before reveal. Bound to qub_id
                                        //   via title_hash (§4.1). 1..=100 NFC code
                                        //   points, no control characters.
}

2.4 RevealedQub (læserens applikationstilstand)

Ikke serialiseret til CBOR. Lokalt i læserens app. Konstrueret efter succesfuld dekryptering og verifikation.

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 — videreført fra QubEnvelope.outcome_at / SealedQub.outcome_at; driver verdict-watch-blokken på afsløringssiden (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

Al serialisering af SealedQub og QubEnvelope MUST overholde denne profil. To implementeringer, der får den samme logiske struktur, MUST producere identiske bytes.

3.1 Kodningsregler

Regel Specifikation
Standard RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Map-nøglerækkefølge Sorteret efter kodet bytelængde først (kortere før længere), derefter leksikografisk (byte for byte for kodninger med samme længde)
Heltalskodning Korteste form: 0–23 i startbyte; 24–255 i 2 bytes; 256–65535 i 3 bytes; osv.
Længdekodning Kun definerede længder. Ingen arrays, maps, byte-strenge eller tekststrenge af ubestemt længde (additional info = 31 er forbudt).
Tags Ingen CBOR-tags (major type 6 er forbudt).
Flydende komma Ingen floats (major type 7-værdierne 0xF9–0xFB er forbudt).
Tekststrenge UTF-8-kodede, NFC-normaliserede (Unicode Normalization Form C).
Byte-strenge Rå bytes. Ingen base64-kodning på CBOR-laget.
Duplikerede nøgler Afvis med fejl. Parsere MUST NOT stiltiende acceptere duplikerede map-nøgler.
Simple værdier Kun true (0xF5), false (0xF4) og null (0xF6) er tilladt.
Valgfrie felter Fraværende valgfrie felter udelades helt fra CBOR-mappet (kodes ikke som null). Tilstedeværende valgfrie felter inkluderes i sorteret nøglerækkefølge.

3.2 Verificerede kanoniske nøglerækkefølger

Disse nøglerækkefølger er normative. Implementeringer MUST udsende nøgler i præcis denne rækkefølge. Debug-assertions SHOULD verificere rækkefølgen i ikke-release-builds.

QubEnvelope (version 0x01, usigneret, alle valgfrie felter fraværende):

"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)

Udledning af QubEnvelope-nøglerækkefølge: hver nøgle er en CBOR-tekststreng. Kodet længde = 1-byte-header + strenglængde (for strenge under 24 bytes). Sortér først efter samlet kodet længde, derefter leksikografisk for nøgler med samme længde.

SealedQub (version 0x01, offentlig, ingen modtager):

"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 (pact-body, 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 (række i terms-arrayet):

"key"    (4 encoded bytes)
"value"  (6 encoded bytes)

PartyIdentifier (party_a / party_b-map):

"label"    (6 encoded bytes)
"contact"  (8 encoded bytes)  ← only if present

3.3 Byte-kodningsreference

Type CBOR-kodning Eksempel
SHA3-256-hash (32 bytes) 0x58 0x20 + 32 bytes body_hash, qub_id
Tidsstempler (i64) Major type 0 (positiv) eller 1 (negativ), korteste kodning Unix-sekunder
Version (u8, værdi 1) 0x01 (enkelt byte)
Content type (u8, værdi 1) 0x01 (enkelt byte)
sig_alg (u8, værdi 0) 0x00 (enkelt byte)
ML-DSA-65-signatur (3.309 bytes) 0x59 0x0C 0xED + 3.309 bytes author_signature, cosigner_signature
ML-DSA-65 offentlig nøgle (1.952 bytes) 0x59 0x07 0xA0 + 1.952 bytes author_pubkey, cosigner_pubkey

4. Normative udledninger

4.1 qub_id

qub_id identificerer entydigt en qub og binder QubEnvelope til SealedQub. Den udledes deterministisk fra envelope-indholdet.

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 af domæneseparator: Strengen "QUB_ID_V2" er 9 ASCII-bytes. En enkelt 0x00-padding-byte tilføjes for at nå 10 bytes til justering. Implementeringer MUST bruge præcis disse 10 bytes: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Kodning af outcome_at: V1.1 udvidede preimage'et fra 92 til 100 bytes for at folde det valgfrie outcome_at-felt ind i bindingen. Fraværende outcome_at kodes som 8 nul-bytes; protokollens validatorer afviser outcome_at <= 0 overalt, så denne sentinel ikke kan kollidere med en legitim værdi. Se §3.2 (wire-format) og den in-tree tasks/verdict-uplift-plan.md for den verdict-mekanik, der motiverer dette felt.

Kodning af drand_round: V1.2 udvidede preimage'et fra 100 til 108 bytes for at folde drand_round (den mål-drand-runde, §4.3) ind i bindingen og hævede domæneseparatoren til QUB_ID_V2. Dette binder timelock-runden ind i qub-identiteten: en gateway kan ikke ombinde ciphertext'en til en anden (f.eks. allerede passeret) runde end den, som det viste unlock_at indebærer. Oplåsningsproceduren (§8) verificerer derudover, at den runde, der er indbagt i tlock-ciphertext-stanzaen, matcher unlock_round(unlock_at), så det viste oplåsningstidspunkt påviseligt er den runde, der gater dekrypteringen.

Egenskaber:

4.2 body_hash

body_hash = SHA3-256(body)

Hvor body er den rå Vec<u8>-indholds-payload. For tekst-qubs er dette den UTF-8-kodede qub-body.

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

Hvor title er den valgfrie klartekst-titel, der vises på læserens nedtælling før afsløring (se §3.2). NFC-normalisering køres på hash-tidspunktet, så digesten er stabil på tværs af visuelt ækvivalente kodepunktsekvenser. Sentinel-værdien med alle nuller er reserveret til det fraværende tilfælde; en tom streng afvises ved den kanoniske CBOR-grænse som en ikke-kanonisk kodning af "fraværende" (den kanoniske kodning udelader feltet helt).

4.3 Mapping fra oplåsning til runde

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
Parameter Kilde Eksempel
unlock_at Brugervalgte Unix-sekunder 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ælger den første drand-runde, hvis afsløringstid er ≥ unlock_at. Dette sikrer, at qubben ikke bliver dekrypterbar før det valgte oplåsningstidspunkt.

Grænsetilfælde: hvis (unlock_at - chain_genesis_time) er præcis deleligt med chain_period_seconds, er resultatet netop denne runde — qubben låses op præcis ved den rundes afsløringstid.

Validering: unlock_at MUST være i fremtiden på forseglingstidspunktet. unlock_at MUST NOT være mere end 10 år fra created_at (for at begrænse risikoen ved langtidshorisont-drand-afhængighed; UI'et SHOULD advare for oplåsningsdatoer ud over 2 år).


5. Wire-format-newtypes

Wire-format-newtypes giver compile-time-sikkerhed mod at forveksle CBOR-bytes med JSON, rå klartekst eller andre byte-kodninger.

Type Indeholder Produceret af Forbrugt af
SealedQubCbor Kanonisk CBOR af SealedQub serialize_sealed_qub() Permanent-lager-upload, læser-fetch
QubEnvelopeCbor Kanonisk CBOR af QubEnvelope serialize_qub_envelope() tlock encrypt-input, tlock decrypt-output

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 ved konstruktion

from_encoded() SHOULD validere, at input begynder med en gyldig CBOR-map-header. Fuld strukturel validering sker ved parse-tid, ikke konstruktionstid, for at undgå dobbelt-parsing.


6. Content-Type-register

Værdi Type Maks. body-størrelse Noter
0x00 Reserveret (ugyldig) MUST NOT bruges
0x01 Almindelig tekst (UTF-8, begrænset Markdown) 50 KB betalt / 10 KB gratis Se §10 for rendering-regler. Opdelingen gratis / betalt håndhæves af upload-tjenesten; det hårde protokol-loft er 50 KB.
0x02 Reserveret (fremtidig) Allokeret til en fremtidig content type; ikke gyldig i v1. Læsere MUST afvise pr. reglen nedenfor.
0x03 Pagt (bilateral aftale, CBOR-body) 100 KB Body er kanonisk CBOR PactTerms (§6.1). Medunderskriver-signering pr. §9.7.
0x04 Dom (skaberens selvbedømmelse, CBOR-body) 8 KB Body er kanonisk CBOR VerdictBody (§6.2). Udsendes kun af system-siden verdict-intent. Den overordnede relation ligger på Parent-Tx-Id-Arweave-tagget, ikke på body'en. Se verdict-uplift-plan §3.4.

Læsere MUST afvise ukendte content types med en tydelig brugersynlig fejl. Læsere MUST NOT forsøge at rendere ukendte typer som tekst.

6.1 Pact-body (content_type = 0x03)

En pact-body er den kanoniske CBOR-kodning af en PactTerms-værdi:

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)> }

Kanoniske CBOR-nøglerækkefølger for alle tre maps findes i §3.2. Den samlede serialiserede pact-CBOR MUST NOT overstige 100 KB (matcher §6).

Skema-diskriminator. Den første række i terms for en structured/v1-pagt MUST være { key: "pact_schema", value: "structured/v1" }. Rækker uden denne markør er "custom"-pagter og modtager ingen struktureret validering eller skema-bevidst rendering.

Indefrosne anerkendelsespladser. structured/v1-pagter bærer præcis fire anerkendelsesrækker under disse nøgler:

"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"

value for hver er én af otte indefrosne engelske strenge, valgt af (role, kind)-parret, hvor role ∈ { seller, buyer, provider, client } og kind ∈ { standard, capacity }. Selve strengene er normative protokoldata — begge parters ML-DSA-65-signaturer forpligter sig til de præcise bytes via body_hash. De er IKKE lokaliseret; den signerede body er sprogneutral. Enhver ordlydsændring kræver en ny skemaversion (structured/v2).

De otte strenge, deres opslag (acknowledgement_for(role, kind)) og rationalet for hver er fastlåst af referenceimplementeringen. Konforme implementeringer MUST udsende byte-identiske anerkendelsesværdier; golden-fixture SHA3-256-body-hash-tests, der dækker alle fire role-kombinationer, fanger enhver drift.

Læsernes visningsrækkefølge. Anerkendelsesstrengene indeholder formuleringer som "described above", der forudsætter, at beskrivelses-/omfangsrækkerne rendres før anerkendelserne. Læsere MUST rendre terms-arrayet i CBOR-rækkefølge; omorganisering bryder prosa-semantikken.

Modpartens kontakt. Når Party B's contact er en gyldig e-mailadresse, sender qub-upload-tjenesten automatisk en gennemgang/medunderskrivnings-invitation pr. e-mail på stage-tidspunktet og binder den endelige medunderskrift til verifikation af samme adresse (§9.7). Pagter, hvis Party B-kontakt er fraværende, kan stadig medunderskrives, men kun gennem en out-of-band-kanal — tjenesten afviser medunderskrivningsanmodninger, der ikke kan producere en matchende 15-minutters e-mail-verifikationsmarkør.

6.2 Verdict-body (content_type = 0x04)

En verdict-body er den kanoniske CBOR-kodning af en VerdictBody-værdi:

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-nøglerækkefølge:

"outcome"          (8 encoded bytes)
"reflection"       (11 encoded bytes)  ← only if present
"evidence_url"     (13 encoded bytes)  ← only if present
"verdict_version"  (16 encoded bytes)

Den samlede serialiserede verdict-CBOR MUST NOT overstige 8 KB (matcher registret ovenfor).

Outcome-enum. Wire-byten er intent-neutral; de fire kategorier Right / Partial / Wrong / Unfalsifiable dækker resultatrummet for ethvert verdict-bærende intent. Per-intent-etiketter ("Ramte plet" / "Holdt det" / "Leveret" / "Bekræftet" for Right osv.) er en læser-side renderingssag, der opløses mod den overordnede qubs intent — wire-formatet forbliver sprog- og intent-neutralt. Værdier uden for 1..=4 MUST afvises ved dekodning.

Overordnet linkning. En verdict-qub bærer IKKE den overordnede reference i sin body. Den overordnede qubs Arweave-transaktions-id udsendes som Parent-Tx-Id-storage-tagget ved upload (§7 storage-tag-laget). Det holder body'en som en selvstændig signeret erklæring om selvbedømmelse; revisionskæden ("ret om hvad?") etableres via Arweave-tag-opslaget.

Sikkerhed for evidence-URL (normativ). Når evidence_url er til stede, MUST validatorer (compose-siden, wire-siden, Worker-edge) håndhæve:

  1. Kun HTTPS. Strengen MUST starte med byte-sekvensen https://. Ethvert andet skema — http, ftp, javascript, data, file osv. — afvises.
  2. Længdeloft. ≤ 2.048 bytes (browserens praktiske URL-grænse).
  3. NFC + fjendtligt-kodepunkt-tjek. Samme regel som title og reflection — bidi-override- / nul-bredde- / tag-block- / BOM- / C0- / C1-kodepunkter afvises. Definitionen matcher Rust crate::handle::contains_hostile_text_codepoint og TS workers/api/src/utils/unicode.ts::isHostileCodepoint (hold dem synkroniseret).
  4. Ingen mellemrum, ingen ASCII-kontroller. Mellemrum / DEL / sub-0x20-bytes hvor som helst i URL'en afvises — lukker \n/\t-injektionsvektoren, som bidi-reglen ikke dækker.
  5. Ikke-tomt hostsegment. Alt mellem https:// og første /, ? eller # MUST være ikke-tomt.

Ingen server-side fetching. Workeren MUST NOT proxy, fetche eller forhåndsvise URL'en. Protokollen gemmer en streng; rendering sker læser-side med rel="nofollow noopener noreferrer" target="_blank" og en synlig vært vist sammen med linkteksten.

Reflektion. Valgfri skaber-skrevet refleksionstekst ("hvad ændrede sig, hvad lærte du"). Samme NFC + fjendtligt-kodepunkt-validering som title. Tomt / kun-mellemrum-input falder sammen til fraværende ved konstruktion.

Skemaversion. v1 understøtter kun verdict_version = 0x01. Fremtidige skemarevisioner bumper denne byte og lander sammen med en ny protokolversion pr. §12.


7. Forseglingsprotokol

Den komplette forseglingssekvens. Hvert trin er 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.

Lager-tag-laget (out-of-band). qub-upload-tjenesten tilknytter et bevidst lille sæt lager-transaktionstags ved siden af den wrappede payload. Content-Type=application/octet-stream er normativt påkrævet. Referencetjenesten tilknytter desuden tre valgfrie tags, når skaberen vælger at vise dem: Intent (allowlist-valideret komposeringshensigt — f.eks. quote, reply, commitment), Author (skaberens §9.3-pubkey-fingeraftryk som 64-tegns lowercase hex) og Parent-Tx-Id (det overordnede qubs lager-transaktions-id for svarkæder, 43-tegns base64url).

Author-tagget er opt-in pr. qub: reference-skaber-appen tilknytter det kun, når brugeren eksplicit aktiverer offentlig tilskrivning på forseglingstidspunktet. Når kontakten er slået fra — standarden — skrives intet Author-tag, og qubben er utilskrevet på kæden: intet i permanent lager forbinder uploaden til en skabers handle, e-mail eller andre qubs. Når kontakten er slået til, opløses Author-fingeraftrykket til skaberens valgte @handle via §9.5-attesteringskæden. Svarkæde-relationer og Intent er ikke-identificerende. Den ydre wrapper (§13) beskytter den indre body mod ciphertext-korrelation — og forhindrer en harvester i at genkende og masse-dekryptere qub-formede uploads, efter at deres drand-runde er udgivet.

Referencetjenesten tilknytter bevidst IKKE App-Name-, App-Version- eller Type-tags: ethvert sådant enkeltværdifilter ville returnere hele qub-korpusset til en GraphQL-forespørgsel, hvilket er uforeneligt med wrapperens body-only-fortrolighedsomfang.

En konform verifikator MUST NOT være afhængig af noget lager-tag til §11-tredjeparts-verifikation; body-hashet / qub_id / signatur forpligter kun til den indre CBOR, aldrig til tag-sættet.


8. Oplåsningsprotokol

Den komplette oplåsningssekvens. Hvert trin er 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. Forfatter-signering

9.1 Rationale

qubs opbevares i permanent lager. Forfatter-signaturer skal forblive uforfalskelige på ubestemt tid, hvilket er grunden til, at v1.0 bruger den post-kvante ML-DSA-65-ordning (FIPS 204) frem for en klassisk ordning, hvis sikkerhed kan degradere inden for qubbens permanente levetid.

9.2 Algoritmeregister

sig_alg Ordning Nøglestørrelse Signaturstørrelse
0x00 Ingen signatur (usigneret)
0x01 ML-DSA-65 (FIPS 204) 1.952 bytes 3.309 bytes

Læsere MUST afvise ukendte sig_alg-værdier.

9.3 Konstruktion af signeret preimage

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V1"  ||    // domain separator (17 bytes)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 bytes)
    body_hash            ||    // [u8; 32] (32 bytes)
    unlock_at            ||    // i64 big-endian (8 bytes)
    0x00                       // u8 (1 byte): MUST be 0x00 in v1.0
)

// Total preimage: 91 bytes → 32-byte hash

signature = Sign(author_secret_key, sig_input)

Domæneseparator: "QUB_AUTHOR_SIG_V1" er 17 ASCII-bytes: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Ingen padding.

Afsluttende byte: den 91. preimage-byte MUST være 0x00. Referenceimplementeringen eksponerer denne som konstanten ORG_ID_PRESENT_INDIVIDUAL = 0x00 i crates/qub-core/src/signing.rs; læsere, der rekonstruerer sig_input til verifikation, MUST udsende samme byte.

Signaturomfang — hvad der er og ikke er dækket. sig_input forpligter sig til fire envelope-felter: version, qub_id, body_hash, unlock_at (plus den faste domæneseparator og org_id_present-byten). Tre af de fire er strukturelle invarianter: qub_id er selv udledt af version, content_type, created_at, unlock_at, outcome_at, drand_round og body_hash via §4.1-preimagen, så enhver ændring til disse felter producerer et andet qub_id og ugyldiggør signaturen transitivt. Den direkte autentificerede overflade er derfor:

Felt Autentificeret af signatur Hvordan
version Direkte input til sig_input
qub_id Direkte input
body_hash Direkte input
unlock_at Direkte input
content_type Transitivt, via qub_id-preimage
created_at Transitivt, via qub_id-preimage
outcome_at Transitivt, via qub_id-preimage
drand_round Transitivt, via qub_id-preimage (V1.2)
body Transitivt, via body_hash = SHA3-256(body)
author_pubkey — (implicit) Nøglen, der verificerede signaturen, er forfatteren pr. definition
sender_label Display-only-tekst; mutérbar uden at bryde signaturen
reply_to Trådnings-pointer; mutérbar uden at bryde signaturen
cosigner_pubkey / cosigner_signature Uafhængigt signeret over samme sig_input (se §9.7)
drand_chain_id, tlock_ciphertext, visibility Ydre SealedQub-felter, ikke inde i envelopen — dækket af deres egne strukturelle invarianter (runde-/kæde-konsistens), men ikke af forfattersignaturen. (drand_round er nu bundet transitivt via qub_id-preimage'et — se ovenfor.)

Sikkerhedsimplikationer af ikke-autentificerede felter.

Implementeringer, der viser sender_label eller reply_to til slutbrugere, MUST fremhæve den autentificerede identitet (pubkey-fingeraftryk, attestering) som det primære identitetssignal, ikke etiketten.

9.4 Verifikationsprocedure

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

Signaturverifikation er den dyreste operation (især ML-DSA-65). Den SHOULD udføres, efter alle billigere kontroller (hash, qub_id, unlock_at) er bestået.

9.5 Identitetsattesteringer

Identitetsattesteringer — mappingen af author_pubkey til menneskeligt genkendelige identitetspåstande som et qub-handle, en e-mailadresse, et socialt handle eller en passkey-legitimation — er en læser-side progressiv forbedring og er ikke påkrævet til signaturverifikation. Læsere, der opløser attesteringer til en vis-identitet, MUST anvende præcedensen:

handle > email > social > fingerprint

Fingeraftryks-fallbacken er lowercase-hex af SHA3-256(author_pubkey); den er altid tilgængelig for enhver signeret qub. Læsere MAY forkorte den til visning — reference-læseren renderer qub: efterfulgt af de første og sidste fire bytes (qub:<8 hex>…<8 hex>).

En konform verifikator kan gennemføre hver kontrol i §9.4 uden at kontakte qub-API'en, uden noget netværk ud over permanent lager og drand og uden noget server-side opslag. Attesteringsopløsning er et separat best-effort-trin, der kun udføres, efter signaturverifikation er lykkedes.

9.6 Størrelsesindvirkning

Ed25519 ML-DSA-65
Signatur 64 bytes 3.309 bytes
Offentlig nøgle 32 bytes 1.952 bytes
I alt pr. qub 96 bytes 5.261 bytes
Lager-omkostningsdelta (ved ~$5/MB) ~$0,0005 ~$0,026

For en tekst-qub på 500–2.000 bytes tredobler ML-DSA-65 nogenlunde den lagrede størrelse. Den absolutte omkostning er ubetydelig.

9.7 Verifikation af medunderskriver (pact bilaterale aftaler)

For bilaterale aftaler (content_type = 0x03) beviser et andet signaturlag, at begge parter har samtykket til de samme vilkår.

Envelope-felter:

Begge felter MUST være til stede sammen eller begge fraværende. Hvis præcis ét er til stede, MUST læsere rapportere en integritetsfejl.

Verifikationsprocedure:

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

Egenskaber:

E-mail-bindings-gate (operationel). Når en staget pagt bærer en Party B-e-mailkontakt (§6.1), MUST qub-upload-tjenesten afvise medunderskrivningsanmodningen, medmindre der eksisterer en kortvarig e-mail-verifikationsmarkør, der matcher både staging-id'et og den normaliserede e-mail-hash af denne kontakt. Markøren skrives af /api/v1/auth/verify, når magic-link-tokenet bærer et staging_id, og den verificerede adresse matcher SHA-256(normalise_email(party_b.contact)) — hvor normalise_email(addr) bevarer local-part-store/små-bogstaver og kun gør domænedelen lowercase (pr. RFC 5321 §2.3.11), og SHA-256 her er NIST FIPS 180-4-hashen (distinkt fra SHA3-256, der bruges i §4-udledninger) — og udløber 900 sekunder (15 minutter) efter udstedelse. Dette er en operationel anti-impersonation-gate, IKKE en del af det on-chain qub-bevis — en tredjeparts-verifikator, der genafspiller §11, behøver kun permanent lager og drand, uden noget server-side opslag. Markøren findes kun server-side og er aldrig en del af den signerede body.

Størrelsesindvirkning (ML-DSA-65 forfatter + medunderskriver):

Komponent Størrelse
Forfattersignatur 3.309 bytes
Forfatterens offentlige nøgle 1.952 bytes
Medunderskriversignatur 3.309 bytes
Medunderskriverens offentlige nøgle 1.952 bytes
Samlet krypto-overhead 10.522 bytes
Lager-omkostningsdelta ~$0,05

10. Markdown-rendering og sanering

Denne sektion er sikkerhedskritisk. Læseren renderer tekst-qubs (content_type = 0x01) ved hjælp af et begrænset Markdown-undersæt.

10.1 Tilladte elementer

10.2 Forbudte elementer

Element Håndtering
Rå HTML (<div>, <script> osv.) Fjernes helt. Ingen HTML kommer igennem.
Billeder (![alt](url)) Fjernes. Billede-syntaks fjernes fra output.
Links ([text](url)) URL'en gengives som synlig klartekst. Ikke auto-linket. Ikke klikbar uden eksplicit brugerhandling.
Farlige URL-skemaer javascript:, data:, vbscript:, file: — fjernes.
Iframes, embeds, objects Fjernes.
HTML-entiteter Afkodes til vis-tegn, kun hvis de er sikre.

10.3 Implementering

Implementeringer MUST bruge en streng allowlist-parser, ikke en blocklist. Den anbefalede tilgang:

  1. Parse Markdown med pulldown-cmark (eller tilsvarende).
  2. Gennemløb AST'et og kassér enhver knude, der ikke er i allowlisten (§10.1).
  3. For link-knuder: udsend URL'en som synlig tekst, ikke som et klikbart <a>-element.
  4. Konverter det filtrerede AST til en typed mellemrepræsentation (f.eks. en MarkdownNode-enum med kun sikre varianter). Rå HTML er strukturelt ikke-repræsenterbar i denne IR.
  5. Render fra den typed IR til mål-vislaget (f.eks. reaktive view-komponenter, DOM-knuder). Ingen HTML-strengsammenkædning eller innerHTML på noget tidspunkt.

Blocklist-tilgange er skrøbelige, fordi nye Markdown-udvidelser eller parser-særheder kan introducere ufiltrerede elementer. Den typed-AST-tilgang gør XSS strukturelt umulig — der er ingen variant, der kan bære vilkårlig HTML.

10.4 Størrelses- og strukturgrænser


11. Tredjeparts-verifikation

Enhver tredjepart kan verificere en offentlig qub uden qubs samarbejde. Verifikationsproceduren:

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.

Hvad verifikation beviser:

Bevis Hvad det fastslår
Forpligtelse Ciphertexten eksisterede senest ved lager-blokkens tidsstempel.
Integritet Klartekst-bodyen matcher den forpligtede hash og er ikke blevet ændret.
Timing Indholdet var ulæseligt indtil drand-runden, der svarer til det valgte oplåsningstidspunkt (under tlock- og drand-sikkerhedsantagelser).

Hvad verifikation IKKE beviser:

Ikke-bevis Hvorfor
Forfatterskab sender_label er dekorativt. Uden sig_alg0x01 kunne hvem som helst have forseglet dette indhold.
Hensigt qubben beviser indhold og timing, ikke hvad skaberen subjektivt mente.
Pre-event-timing Lager-blokinklusion kan lægge minutter efter den faktiske upload. Forpligtelsens tidsstempel er bloktiden, ikke det øjeblik, brugeren trykkede "forsegl".

12. Versionering

12.1 Protokolversion

version-feltet (u8) i både SealedQub og QubEnvelope identificerer den større protokolversion.

12.2 Versionshistorik

Version Værdi Beskrivelse
v1 0x01 Offentlige tekst-qubs (content_type 0x01), pact bilaterale aftaler (0x03, structured/v1-skema, ML-DSA-65 forfatter + medunderskriver), tlock, SHA3-256

12.3 Fremad-kompatibilitet

En v1-læser, der støder på en QubEnvelope med ukendte valgfrie CBOR-map-nøgler (nøgler, der ikke er i §3.2-kanonisk rækkefølge), SHOULD ignorere disse nøgler og fortsætte med verifikation ved hjælp af kendte felter. Dette tillader fremtidige mindre tilføjelser (f.eks. nye metadata) uden at kræve et større versionsspring.

En v1-læser, der støder på sig_alg = 0x01 (ML-DSA-65), men som mangler ML-DSA-65-verifikationsunderstøttelse, SHOULD vise qub-indholdet med en "signatur til stede, men ikke verificerbar"-notits, ikke afvise qubben helt. Reference-implementeringen i dag afviser enhver sig_alg-værdi ud over 0x00 og 0x01, fordi v1-registret ikke indeholder nogen anden gyldig algoritme — streng afvisning og soft-fail er observationelt identiske, indtil en tredje algoritme registreres. Soft-fail-adfærden ovenfor bliver bærende, så snart §9.2 optager en ny post, og reference-læseren vil blive opdateret til soft-fail på det tidspunkt.

12.4 Ydre wrapper-version

OuterWrapperen beskrevet i §13 bærer sin egen version-byte, uafhængig af SealedQub.version og QubEnvelope.version. De to versionsrum udvikler sig separat: en fremtidig post-kvante-sikker symmetrisk erstatning hæver wrapper-byten uden at røre den indre protokolversion, og en fremtidig protokollag-tilføjelse (f.eks. et nyt envelope-felt) hæver den indre version uden at røre wrapper-byten.

OUTER_WRAPPER_VERSION_* Værdi Algoritme Status
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM med 12-byte nonce, 16-byte authentication tag, AAD bundet til qub_id v1-standard
0x020xFF Reserveret Fremtidig

Læsere MUST afvise ukendte wrapper-versioner med en tydelig fejl. Protokollen holder bevidst wrapper-versionsrummet smalt, indtil en konkret migrationsdriver dukker op (f.eks. NIST-vejledning, der favoriserer en anden AEAD); en 0x02-plads vil blive allokeret i samme revision, der introducerer algoritmen.


13. Ydre krypteringswrapper

13.1 Rationale

Protokollagene (QubEnvelope → tlock → SealedQub) gør en forseglet qub tidslåst: bodyen er ulæselig indtil unlock_at, og drand-runde-signaturen er blevet udgivet. Efter oplåsning er runde-signaturen imidlertid offentlig, og den kanoniske CBOR-form af SealedQub er genkendelig, så en harvester, der indekserede permanent-lager-transaktioner, kunne masse-dekryptere hele qub-korpusset.

Den ydre krypteringswrapper lukker den kanal ved at indsætte et yderligere symmetrisk AEAD-lag mellem den kanoniske SealedQubCbor og de bytes, der skrives til permanent lager. Den 256-bit nøgle K lever kun i URL-fragmentet af leveringslinket og på brugerenheder; browsere transmitterer ikke URL-fragmenter til servere, så qub.social, hver lager-gateway og hver CDN foran begge er observationelt blinde for K. Hver qub i permanent lager er derfor en uigennemsigtig ciphertext, hvis klartekst ikke kan genskabes uden den URL, skaberen valgte at dele.

Nettoeffekt:

13.2 Lagdeling

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)

Forsegling og oplåsning på protokollaget (§7, §8) er uændrede under wrapper-grænsen; wrapperen sættes på ved kaldstedet for seal() og fjernes ved kaldstedet for unlock().

13.3 OuterWrapper-datastruktur

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
}

Feltinvarianter.

CBOR-kodning. Kanonisk CBOR pr. §3, med samme nøgle-ordens-regel (sorteret efter kodet bytelængde stigende, derefter leksikografisk). De fire nøgler er:

Nøgle Kodede bytes Rækkefølge
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

Den første byte af OuterWrapper-CBOR'en er derfor den definite-length-map-header for en 4-entry-map (0xA4).

13.4 AAD-binding til qub_id

Wrapperen binder qub_id som AEAD additional authenticated data. Dette er det bærende strukturelle forsvar mod tre angrebsklasser:

Angreb Forsvar
Flytte ciphertext under et andet qub_id-felt i wrapperen AAD-mismatch → AEAD-autentificering fejler
Blande URL-fragmentet af qub A med permanent-lager-bytes af qub B AAD-mismatch → AEAD-autentificering fejler
Manipulere qub_id-feltet af wrapperen efter upload AAD-mismatch → AEAD-autentificering fejler

At bære qub_id i wrapperens klartekst svækker ikke optællings-immuniteten på meningsfuld måde — qub_id er selv en SHA3-256-hash af §4.1-preimagen uden gendannelig preimage fra digesten, og en optæller, der allerede har høstet wrapper-bytene, lærer intet af det synlige qub_id, som de ikke kunne udlede af selve uploadens eksistens.

13.5 Wrap- og unwrap-algoritmer

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

Fejl-tilstands-kollaps. Forkert K, forkert nonce, AAD-mismatch og manipuleret ciphertext producerer alle samme DECRYPT_FAILED-fejl. Dette er en bevidst AEAD-egenskab: at skelne fejl-tilstanden ville skabe en sidekanal, som en fjern-angriber kunne undersøge ved at sende deforme wrappere og time svaret. Reference-implementeringer MUST kollapse alle AEAD-fejl til en enkelt fejlform.

13.6 Nøglemateriale og distribution

Wrap-nøglen K er en 256-bit ensartet tilfældig værdi genereret pr. qub af en CSPRNG. Reference-implementeringerne henter den fra:

Distribution: K MUST kodes som URL-sikker base64 (RFC 4648 §5, ingen padding) og tilføjes leveringslinket som fragment-komponenten:

delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>

Fragmentet transmitteres aldrig til nogen server af en konform browser. Genoprettelseskanaler (server-side historik-indeks, opt-in e-mail-auto-send), der persisterer det fulde leveringslink — inklusive fragmentet — ud over brugerens enhed, er et eksplicit kompromis mod standard-crypto-shredding-positionen og MUST gates ved eksplicit brugersamtykke.

Tab af fragment. Hvis en bruger mister URL-fragmentet og ikke har nogen genoprettelseskanal, er qubben ulæselig. Dette er designets bærende kompromis og MUST oplyses brugeren på forseglingstidspunktet. MVP'en styrker forseglingstids-oplysningen med eksplicit "gem dette link"-tekst og en verificeret-e-mail-genoprettelseskanal for brugere, der vælger det til.

13.7 Uden for denne sektions omfang

13.8 Offentlige qubs (udeladelse af wrapper)

Den ydre wrapper er valgfri på leveringslaget. En skaber kan forsegle en qub som offentlig, og i så fald skrives den kanoniske SealedQubCbor til permanent lager direkte, uden noget OuterWrapper-lag og uden nogen nøgle K:

SealedQubCbor bytes  ──(public)──▶  uploaded to permanent storage as-is
SealedQubCbor bytes  ──(private)─▶  AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded

En offentlig qub er tidslåst, men ikke link-gated: den forbliver ulæselig, indtil dens drand-runde udgives (tlock-laget er uændret), men efter oplåsning kan enhver, der har arweave_tx_id, dekryptere den — der kræves intet URL-fragment, fordi der ikke findes nogen K. Dette er det bevidste kompromis for overflader, som serveren skal drive: afsløringsnotifikations-e-mails, tredjeparts-embeds og rigere SEO efter afsløring har alle brug for et link, der virker uden en hemmelighed, serveren aldrig besidder (§13.6).

Konsekvenser, en producent MUST tage højde for:

Privat (wrappet) forbliver standarden; offentlig er et eksplicit pr.-qub-skabervalg.


14. Testvektorer

14.1 qub_id-udledning

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

Implementeringer MUST producere identiske body_hash- og qub_id-værdier for dette input. Denne testvektor SHOULD være den første enhedstest, der skrives. De kanoniske værdier ovenfor blev beregnet af reference-implementeringen og MUST matche bit-for-bit. Historiske preimage-layouts (før-lancering — ingen live qubs afhang af disse): det 92-byte V1.0 qub_id var 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; det 100-byte V1.1 qub_id (efter at have foldet outcome_at_or_zero ind) var b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 folder drand_round ind og hæver domæneseparatoren til QUB_ID_V2.

14.2 Mapping fra oplåsning til runde

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-rundtur

Implementeringer MUST verificere, at serialize(parse(serialize(qub))) == serialize(qub) for alle gyldige input. Dette er en property-test, ikke en enkelt 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 kanoniske CBOR-bytes og SHA3-256 body_hash beregnes af reference-implementeringen. Implementeringer MUST producere byte-identisk CBOR for dette input.

Implementeringer MUST også verificere, at serialize(parse(serialize(pact))) == serialize(pact) for alle gyldige PactTerms-input (property-test).

14.5 Krydssprogs-vektorer for ydre wrapper

Den ydre wrapper (§13) har en separat kanonisk fixture på crates/qub-core/tests/vectors/wrapper_v1.json. Hvert tilfælde fastsætter en (key, nonce, qub_id, sealed_cbor)-tuple som uigennemsigtige hex-input og asserter et bestemt expected_wrapper_hex-output. Begge reference-implementeringer forbruger samme JSON-fil:

Fixturen fastlåser i øjeblikket tre tilfælde:

Tilfælde Dækning
basic-text-public Mindste realistiske SealedQub-form; ingen valgfrie felter. Etablerer den kanoniske wrapper-form for en v1.0-typisk qub.
with-recipient-pubkey SealedQub med recipient_pubkey sat (Phase 2-sti). Andet indre CBOR-nøglesæt, andet qub_id.
longer-body ~4 KiB body — udøver multi-byte CBOR-længde-præfikser inde i både den indre envelope og den ydre ciphertext.

Implementeringer MUST producere byte-identisk expected_wrapper_hex for de optagne input. Regenerering af fixturen kræver QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors og er forbeholdt bevidste formatændringer.


15. Krypto-profil-governance (fremtid)

Denne sektion er informativ for v1 og bliver normativ første gang en anden algoritme træder ind i nogen af qubs kryptografiske primitiver.

15.1 Nuværende stilling

Protokol v1 binder præcis én algoritme pr. primitiv:

Verifikatorer hårdkoder i øjeblikket nøgle- og signaturlængder pr. primitiv. Ingen agility-overflade er eksponeret af wire-formatet.

15.2 Tilsigtet form

Når en anden algoritme træder ind i protokollen, vil verifikatoren blive konfigureret til en navngivet CryptoProfile (f.eks. ExqubV1), der opregner det præcise sæt af tilladte værdier pr. primitiv — sig_algs, drand-kæder, wrapper-versioner, content types. Profilen er fastsat ved verifikationstid, aldrig forhandlet in-band. Enhver værdi uden for den aktive profil afvises.

Dette garanterer, at tilføjelse af ML-DSA-87 eller aktivering af Ed25519 ikke retroaktivt kan svække eksisterende verifikator-konfigurationer: en v1-verifikator forbliver en v1-verifikator, selv efter at en v2-profil er udgivet.

15.3 Udløsningsbetingelser

Forfremme §15 til normativ status, når et af følgende foreslås:

Indtil da er §15 en pladsholder, der fastsætter migrationsformen, så fremtidige PR'er lander mod et kendt mål i stedet for at gen-litigere forhandlingsoverfladen fra bunden.