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:
- Ændring af et hvilket som helst felt i QubEnvelope (body, tidsstempler, content type, version) producerer et andet qub_id.
- qub_id beregnes før kryptering. Både QubEnvelope og SealedQub bærer samme qub_id. Læseren verificerer, at de matcher efter dekryptering.
- qub_id afhænger ikke af
sender_label,author_signatureellerauthor_pubkey. Det betyder, at det samme indhold forseglet på samme tidspunkt producerer samme qub_id, uanset hvem der signerer det. - Ændring af SealedQub-
title(med alt andet fast) ændrerqub_idviatitle_hash. En gateway kan derfor ikke udskifte den klartekst-titel, der vises på nedtællingen, uden at ugyldiggøre qub-identiteten. - Ændring af SealedQub-
outcome_at(med alt andet fast) ændrerqub_idvia preimage'et. En gateway kan ikke udskifte den verdict-on-dato før afsløring, der vises på nedtællingen, uden at ugyldiggøre qub-identiteten. - Ændring af
drand_round(med alt andet fast) ændrerqub_idvia preimage'et. En gateway kan ikke ombinde timelock-ciphertext'en til en anden runde uden at ugyldiggøre qub-identiteten; kombineret med §8-kontrollen af stanza-runden ved oplåsning er det visteunlock_atden runde, der faktisk gater dekrypteringen.
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:
- Kun HTTPS. Strengen MUST starte med byte-sekvensen
https://. Ethvert andet skema —http,ftp,javascript,data,fileosv. — afvises. - Længdeloft. ≤ 2.048 bytes (browserens praktiske URL-grænse).
- NFC + fjendtligt-kodepunkt-tjek. Samme regel som
titleogreflection— bidi-override- / nul-bredde- / tag-block- / BOM- / C0- / C1-kodepunkter afvises. Definitionen matcher Rustcrate::handle::contains_hostile_text_codepointog TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(hold dem synkroniseret). - 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. - 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.
- En part med skriveadgang til de lagrede bytes kunne udskifte
sender_label("Alice" → "Mallory") uden at ugyldiggøre forfattersignaturen.author_pubkeyinde i envelopen forbliver det sande identitetsanker — læsere MUST udlede vis-identiteten fraauthor_pubkey(via §9.5-attesteringslaget) i stedet for at stole påsender_label. - Et
reply_to-felt kan ligeledes redigeres efter signering. Fordiqub_ider indholdsadresseret, kan en angriber ikke pegereply_topå et ikke-eksisterende mål, men de kan stiltiende give et svar en anden eksisterende qub som forælder.
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:
cosigner_pubkey: ML-DSA-65 offentlig nøgle for medunderskriveren (Party B).cosigner_signature: Signatur over sammesig_inputsom forfatteren (§9.3).
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:
- Medunderskriveren signerer det identiske
sig_inputsom forfatteren — begge parter forpligter sig til sammequb_id,body_hashogunlock_at. qub_id-udledning (§4.1) inkluderer IKKE medunderskriverfelter. At tilføje en medunderskriver til en eksisterende envelope ændrer ikkequb_id.- En pagt kan være kun forfattersigneret (énsidet forpligtelse), kun medunderskriver (usædvanligt) eller begge (fuldt bilateralt bevis).
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
- Overskrifter:
#til og med####(ingen#####eller######) - Fremhævning: fed (
**), kursiv (*), gennemstreget (~~) - Lister: ordnet (
1.) og uordnet (-,*) - Citatblokke (
>) - Kode: inline-spans (```) og fenced-blokke (`````)
- Vandrette streger (
---) - Linjeskift (to afsluttende mellemrum eller blank linje)
- Afsnit
10.2 Forbudte elementer
| Element | Håndtering |
|---|---|
Rå HTML (<div>, <script> osv.) |
Fjernes helt. Ingen HTML kommer igennem. |
Billeder () |
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:
- Parse Markdown med
pulldown-cmark(eller tilsvarende). - Gennemløb AST'et og kassér enhver knude, der ikke er i allowlisten (§10.1).
- For link-knuder: udsend URL'en som synlig tekst, ikke som et klikbart
<a>-element. - 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. - Render fra den typed IR til mål-vislaget (f.eks. reaktive view-komponenter, DOM-knuder). Ingen HTML-strengsammenkædning eller
innerHTMLpå 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
- Maksimal renderet overskriftsdybde:
####(H4).#####og dybere rendres som fed tekst. - Ingen grænse for antal afsnit (body-størrelsesgrænserne i §6 er begrænsningen).
- Fenced kodeblokke: ingen syntax-highlighting i MVP. Rendres som monospace-præformateret tekst.
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_alg ≥ 0x01 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.
- Læsere MUST afvise ukendte større versioner med en tydelig fejl.
- Kendte større versioner MAY tolerere ukendte valgfrie felter, hvis fremad-kompatibilitetsregler tillader det (valgfrie felter, der er fraværende fra den kanoniske nøglerækkefølge, ignoreres).
- Content types (
content_type) og signaturordninger (sig_alg) er version-gated: nye værdier kan kun introduceres sammen med en ny protokolversion eller eksplicit registeropdatering.
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 |
| — | 0x02–0xFF |
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:
- Optællings-immunitet som standard. Wrappede bytes i permanent lager er byte-uskelnelige fra vilkårlig ciphertext. En harvester-strategi om at "GraphQL-spørge efter qub-formede uploads, masse-dekryptere med offentlige drand-signaturer" terminerer ikke med klartekst.
- Crypto-shredding-privatlivsposition. qub.social kan bogstaveligt talt ikke dekryptere sit eget korpus. Stævninger når ciphertext, ikke klartekst.
- To-trins-fortrolighedsstige. Standard = link-kontrolleret adgang (denne sektion). Modtager-krypterede private qubs (en reserveret Fase 2-funktion, endnu ikke specificeret) lægger sig ovenpå som det andet trin.
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.
versionMUST være lig med0x01for v1.0 wrapper-bytes.qub_idMUST være lig medqub_id-feltet af den SealedQub, der genskabes efter unwrap. Unwrap-trinnet håndhæver det ikke direkte (AEAD-AAD-bindingen gør byte-niveau-manipulation umulig), men oplåsningslaget kontrollerer forholdet transitivt: hvis en skaber wrapper enSealedQubCbor, hvis indrequb_idikke matcher wrapperensqub_id, fejler §8 trin 11.nonceMUST være 96 bits (12 bytes), frisk genereret af en CSPRNG ved hver wrap-operation. Genbrug af en nonce under samme nøgle tillader AEAD-nonce-reuse-angreb, der genskaber klarteksten; producenter MUST behandle (key,nonce)-par som engangs.ciphertexter AES-256-GCM-outputtet: ciphertext-bytes sammenkædet med den 16-byte authentication tag.ciphertext.len() == SealedQubCbor.len() + 16præcis.
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:
- WASM-skaber:
getrandom(WebCrypto underwasm_js-backend). - Worker server-side seal-rute:
crypto.getRandomValues.
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
- Forfatter-signering (§9) er uændret: signaturer beregnes inde i den indre
QubEnvelopeog genskabes efter unwrap → tlock-dekryptering → CBOR-parse. - Modtager-krypterede private qubs (en reserveret Fase 2-funktion, endnu ikke specificeret) komponerer ovenpå denne wrapper som et andet fortrolighedstrin; begge trin kan være aktive samtidigt.
- Pagter (§6, content_type
0x03) wrappes præcis som tekst-qubs; wrapperen er byte-blind for indre content type.
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:
- Ingen optællings-immunitet. Offentlige qubs giver per konstruktion afkald på §13.1-optællings-immunitetsegenskaben. Reference-upload-tjenesten stempler et
Visibility: public-tag på permanent lager på dem (og kun dem), så de bevidst er opdagelige; private qubs bærer intet sådant tag og bevarer deres byte-uskelnelighed. - Klartekst-titel eksponeret på forseglingstidspunktet. §3.2-
title-feltet er klartekst inde iSealedQubCbor. Under wrapperen er det skjult, indtil en læser levererK; uden wrapperen er det verdenslæsbart i permanent lager fra uploadøjeblikket, før oplåsning. Konforme skaber-apps MUST oplyse om dette på forseglingstidspunktet. - Detektion er strukturel. En konform læser/embed skelner de to former ved parse: bytes, der parser som
OuterWrapper, tager unwrap-med-K-vejen; bytes, der parser som en barSealedQubCbor, accepteres direkte. Der kræves intet wire-flag, ogqub_idbinder ikke synligheden — det samme indhold er byte-identisk påSealedQub-laget, uanset om det forsegles offentligt eller privat.
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:
- Rust:
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors). - TypeScript:
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test).
Fixturen 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:
- Signatur: ML-DSA-65 (
sig_alg = 0x01; 1952-byte offentlig nøgle, 3309-byte signatur) og usigneret (sig_alg = 0x00). §9.2-registret definerer ingen andre værdier; en v1-verifikator MUST afvise enhversig_alguden for{0x00, 0x01}. En fremtidig Ed25519-post forventes (§15.3), men er ikke allokeret i v1. - Timelock: kun drand quicknet — chain hash, offentlig nøgle, genesis-tid og periode er faste netværksparametre båret af reference-
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) ogconfig/drand-endpoints.json. - Ydre wrapper: kun AES-256-GCM v1 (§13).
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:
- En anden
sig_alg-byte (Ed25519-aktivering, ML-DSA-87 eller enhver ny post i §9-registret). - En anden drand-kæde i produktionsbrug.
- En anden ydre wrapper-version.
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.