qub Protokollspesifikasjon
qub er en protokoll for kryptografiske temporale forpliktelser: et system for å forsegle ord til en framtidig dato og bevise, når den datoen kommer, nøyaktig hva som ble sagt og når.
Tre primitiver får det til å fungere. drand er en desentralisert tilfeldighetsbeacon — avsløringsdatoen håndheves av fysikk, ikke av noen parts velvilje. Permanent offentlig lagring er et manipulasjonssikkert offentlig 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 knyttet til et nøkkelpar der den hemmelige delen aldri forlater forfatterens enhet.
Sammen utgjør disse primitivene en uttalelse som er tidslåst, manipulasjonssikker og tilskrivbar — en kvittering hvis verdi vokser i takt med verdens evne til å fabrikere fortiden.
Resten av dette dokumentet er den normative spesifikasjonen som kreves for interoperable implementasjoner.
qub-protokollspesifikasjon
| Felt | Verdi |
|---|---|
| Versjon | 1.0 (protokollversjon 0x01, ytre omslagsversjon 0x01) |
| Dato | 2026-05-01 |
| Status | Utkast |
| Gjennomgått til | 2026-05-01 |
Dette dokumentet er den normative protokollspesifikasjonen for qub-systemet for tidsbestemte forpliktelser. Det definerer datastrukturer, serialiseringsregler, utledningsformler og verifikasjonsprosedyrer som kreves for interoperable implementasjoner.
Omfang: protokolllaget er bevisst språknøytralt — qub-kroppen er ugjennomsiktig klartekst / markdown / pakt-byte, og språkbevisst gjengivelse er leserens ansvar (qub.social-webapp, <qub-embed>-iframe, MCP-klienter, osv.).
1. Notasjon og konvensjoner
| Notasjon | Betydning |
|---|---|
u8, u64, i64 |
Usignerte/signerte heltall med spesifisert bitbredde |
[u8; N] |
Bytearray med fast lengde på N byte |
Vec<u8> |
Bytearray med variabel lengde |
Option<T> |
Verdi av type T, eller fraværende |
String |
UTF-8-tekststreng, NFC-normalisert |
| ` | |
SHA3-256(x) |
NIST SHA3-256-hash av bytestrengen x (FIPS 202) |
ceil(x) |
Takfunksjon: minste heltall ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Mest signifikante byte først |
Alle heltall i preimage-konstruksjoner kodes som big-endian bytearray med fast bredde (i64 → 8 byte, u8 → 1 byte) med mindre annet er spesifisert.
Alle tidsstempler er Unix-sekunder i UTC.
2. Datastrukturer
2.1 ComposeQub (skaperens tilstand i minnet)
Ikke serialisert til CBOR. Ikke skrevet til permanent lagring. Lokal for skaperens 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 (dekryptert nyttelast)
Serialisert med kanonisk CBOR (§3). Kryptert inne i SealedQub. Dette er strukturen som beviser innholdets integritet etter 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
}
Grunntilfelle (usignert tekst-qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, alle Option-felt fraværende.
Andre v1-konfigurasjoner: content_type = 0x03 (pakt-kropp, 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 medundertegnede pakter (se §9.7); reply_to satt til den overordnede qubens qub_id for svarkjede-qubs (se §9.3 for konsekvensene for signaturomfanget).
2.3 SealedQub (kanonisk wire-format)
Serialisert med kanonisk CBOR (§3). Skrevet til permanent lagring. Dette er on-chain-artefakten.
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 (leserens applikasjonstilstand)
Ikke serialisert til CBOR. Lokal for leserens app. Konstruert etter vellykket dekryptering og verifikasjon.
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 dom-vente-blokken på avslø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
All serialisering av SealedQub og QubEnvelope MUST samsvare med denne profilen. To implementasjoner gitt samme logiske struktur MUST produsere identiske byte.
3.1 Kodingsregler
| Regel | Spesifikasjon |
|---|---|
| Standard | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Sortering av nøkler i map | Sortert etter kodet bytelengde først (kortere før lengre), deretter leksikografisk (byte-for-byte for kodinger med samme lengde) |
| Heltallskoding | Korteste form: 0–23 i innledende byte; 24–255 i 2 byte; 256–65535 i 3 byte; osv. |
| Lengdekoding | Kun definitive lengder. Ingen ubestemt-lengde arrayer, maps, byte-strenger eller tekststrenger (additional info = 31 er forbudt). |
| Tags | Ingen CBOR-tags (major type 6 er forbudt). |
| Flyttall | Ingen flyttall (major type 7-verdier 0xF9–0xFB er forbudt). |
| Tekststrenger | UTF-8-kodet, NFC-normalisert (Unicode Normalization Form C). |
| Byte-strenger | Rå byte. Ingen base64-koding på CBOR-laget. |
| Dupliserte nøkler | Avvis med feil. Parsere MUST NOT stille godta dupliserte map-nøkler. |
| Enkle verdier | Kun true (0xF5), false (0xF4) og null (0xF6) er tillatt. |
| Valgfrie felt | Fraværende valgfrie felt utelates helt fra CBOR-mapen (ikke kodet som null). Til stede-felter inkluderes i sortert nøkkelrekkefølge. |
3.2 Verifiserte kanoniske nøkkelrekkefølger
Disse nøkkelrekkefølgene er normative. Implementasjoner MUST sende ut nøkler i nøyaktig denne rekkefølgen. Debug-asserter SHOULD verifisere rekkefølgen i ikke-release-bygg.
QubEnvelope (versjon 0x01, usignert, alle valgfrie felt 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)
Utledning av QubEnvelope-nøkkelrekkefølge: hver nøkkel er en CBOR-tekststreng. Kodet lengde = 1 byte header + strenglengde (for strenger under 24 byte). Sorter etter total kodet lengde først, deretter leksikografisk for nøkler med samme lengde.
SealedQub (versjon 0x01, offentlig, ingen mottaker):
"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 (pakt-kropp, content_type 0x03):
"notes" (6 encoded bytes) ← only if present
"terms" (6 encoded bytes)
"title" (6 encoded bytes)
"party_a" (8 encoded bytes)
"party_b" (8 encoded bytes)
"pact_version" (13 encoded bytes)
PactTerm (rad i terms-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-kodingsreferanse
| Type | CBOR-koding | Eksempel |
|---|---|---|
| SHA3-256-hash (32 byte) | 0x58 0x20 + 32 byte |
body_hash, qub_id |
| Tidsstempler (i64) | Major type 0 (positiv) eller 1 (negativ), korteste koding | Unix-sekunder |
| Versjon (u8, verdi 1) | 0x01 (én byte) |
|
| Content type (u8, verdi 1) | 0x01 (én byte) |
|
| sig_alg (u8, verdi 0) | 0x00 (én byte) |
|
| ML-DSA-65-signatur (3 309 byte) | 0x59 0x0C 0xED + 3 309 byte |
author_signature, cosigner_signature |
| ML-DSA-65 offentlig nøkkel (1 952 byte) | 0x59 0x07 0xA0 + 1 952 byte |
author_pubkey, cosigner_pubkey |
4. Normative utledninger
4.1 qub_id
qub_id identifiserer unikt en qub og binder QubEnvelope til SealedQub. Den utledes deterministisk fra envelope-innholdet.
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
Koding av domeneseparator: Strengen "QUB_ID_V2" er 9 ASCII-byte. Én enkelt 0x00-padding-byte legges til for å nå 10 byte for justering. Implementasjoner MUST bruke nøyaktig disse 10 byte: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
Koding av outcome_at: V1.1 utvidet preimagen fra 92 til 100 byte for å folde det valgfrie outcome_at-feltet inn i bindingen. Fraværende outcome_at kodes som 8 nullbyte; protokollvalidatorene avviser outcome_at <= 0 overalt, slik at denne sentinelen ikke kan kollidere med en legitim verdi. Se §3.2 (wire-format) og den interne tasks/verdict-uplift-plan.md for verdiktmekanikken som motiverer dette feltet.
Koding av drand_round: V1.2 utvidet preimagen fra 100 til 108 byte for å folde drand_round (mål-drand-runden, §4.3) inn i bindingen, og hevet domeneseparatoren til QUB_ID_V2. Dette binder tidslås-runden inn i qub-identiteten: en gateway kan ikke binde ciphertexten på nytt til en annen (f.eks. allerede passert) runde enn den viste unlock_at antyder. Opplåsingsprosedyren (§8) verifiserer i tillegg at runden som er bakt inn i tlock-ciphertext-stanzaen samsvarer med unlock_round(unlock_at), slik at den viste opplåsingstiden beviselig er runden som styrer dekryptering.
Egenskaper:
- Endring av et hvilket som helst felt i QubEnvelope (body, tidsstempler, content type, versjon) gir en annen qub_id.
- qub_id beregnes før kryptering. Både QubEnvelope og SealedQub bærer samme qub_id. Leseren verifiserer at de samsvarer etter dekryptering.
- qub_id avhenger ikke av
sender_label,author_signatureellerauthor_pubkey. Dette betyr at samme innhold forseglet på samme tidspunkt gir samme qub_id uavhengig av hvem som signerer det. - Endring av SealedQub
title(med alt annet fast) endrerqub_idviatitle_hash. En gateway kan derfor ikke bytte ut klartekst-tittelen som vises på nedtellingen uten å invalidere qub-identiteten. - Endring av SealedQub
outcome_at(med alt annet fast) endrerqub_idvia preimagen. En gateway kan ikke bytte ut datoen for verdikt-på som vises på nedtellingen før avsløring, uten å invalidere qub-identiteten. - Endring av
drand_round(med alt annet fast) endrerqub_idvia preimagen. En gateway kan ikke binde tidslås-ciphertexten på nytt til en annen runde uten å invalidere qub-identiteten; kombinert med §8-stanza-runde-sjekken ved opplåsingstid er den visteunlock_atrunden som faktisk styrer dekryptering.
4.2 body_hash
body_hash = SHA3-256(body)
Der body er den rå Vec<u8>-innholdsnyttelasten. For tekst-qubs er dette den UTF-8-kodede qub-kroppen.
4.2.1 title_hash
title_hash = SHA3-256(NFC(title).utf8_bytes) if title is present
title_hash = [0u8; 32] if title is absent
Der title er den valgfrie klartekst-tittelen som vises på leserens nedtelling før avsløring (se §3.2). NFC-normalisering kjøres ved hash-tid slik at digesten er stabil på tvers av visuelt ekvivalente kodepunkt-sekvenser. Sentinel-verdien med kun nuller er reservert for det fraværende tilfellet; en tom streng avvises ved den kanoniske CBOR-grensen som en ikke-kanonisk koding av «fraværende» (den kanoniske kodingen utelater feltet helt).
4.3 Mapping til opplåsingsrunde
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Parameter | Kilde | Eksempel |
|---|---|---|
unlock_at |
Brukervalgt 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()-operasjonen velger den første drand-runden hvis avsløringstid er ≥ unlock_at. Dette sikrer at qub-en ikke blir mulig å dekryptere før den valgte opplåsingstiden.
Grensetilfelle: hvis (unlock_at - chain_genesis_time) er nøyaktig delelig med chain_period_seconds, er resultatet nøyaktig den runden — qub-en låses opp presist ved den rundens avsløringstid.
Validering: unlock_at MUST være i framtiden ved forseglingstidspunkt. unlock_at MUST NOT være mer enn 10 år fra created_at (for å begrense risiko knyttet til drand-avhengighet med lang horisont; brukergrensesnittet SHOULD advare for opplåsingsdatoer utover 2 år).
5. Wire-format-newtypes
Wire-format-newtypes gir compile-time sikkerhet mot å forveksle CBOR-byte med JSON, rå klartekst eller andre byte-kodinger.
| Type | Inneholder | Produsert av | Konsumert av |
|---|---|---|---|
SealedQubCbor |
Kanonisk CBOR av SealedQub | serialize_sealed_qub() |
Opplasting til permanent lagring, leserens henting |
QubEnvelopeCbor |
Kanonisk CBOR av QubEnvelope | serialize_qub_envelope() |
tlock-krypteringsinngang, tlock-dekrypteringsutgang |
5.1 Konstruksjonsregler
// 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 konstruksjon
from_encoded() SHOULD validere at inndataen starter med en gyldig CBOR map-header. Full strukturell validering skjer ved parse-tid, ikke ved konstruksjonstid, for å unngå dobbel parsing.
6. Content type-register
| Verdi | Type | Maks kroppsstørrelse | Merknader |
|---|---|---|---|
0x00 |
Reservert (ugyldig) | — | MUST NOT brukes |
0x01 |
Rentekst (UTF-8, begrenset Markdown) | 50 KB betalt / 10 KB gratis | Se §10 for gjengivelsesregler. Splittet gratis/betalt håndheves av opplastingstjenesten; det harde taket på protokoll-laget er 50 KB. |
0x02 |
Reservert (fremtidig) | — | Tildelt for en fremtidig content type; ikke gyldig i v1. Lesere MUST avvise per regelen nedenfor. |
0x03 |
Pakt (bilateral avtale, CBOR-kropp) | 100 KB | Kroppen er kanonisk CBOR PactTerms (§6.1). Medundertegner-signering per §9.7. |
0x04 |
Dom (skaperens selvbedømmelse, CBOR-kropp) | 8 KB | Kroppen er kanonisk CBOR VerdictBody (§6.2). Sendes ut kun av system-side verdict-intensjonen. Moder-relasjonen ligger på Arweave-taggen Parent-Tx-Id, ikke i kroppen. Se verdict-uplift-plan §3.4. |
Lesere MUST avvise ukjente content types med en tydelig brukersynlig feil. Lesere MUST NOT forsøke å gjengi ukjente typer som tekst.
6.1 Pakt-kropp (content_type = 0x03)
En pakt-kropp er den kanoniske CBOR-kodingen av en PactTerms-verdi:
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økkelrekkefølger for alle tre mapene er gitt i §3.2. Total serialisert pakt-CBOR MUST NOT overstige 100 KB (samsvarer med §6).
Skjemadiskriminator. Den første raden i terms for en structured/v1-pakt MUST være { key: "pact_schema", value: "structured/v1" }. Rader uten denne markøren er «egendefinerte» pakter og får ingen strukturert validering eller skjemabevisst gjengivelse.
Frosne bekreftelses-slotter. structured/v1-pakter har nøyaktig fire bekreftelsesrader under disse nøklene:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
value for hver er én av åtte frosne engelske strenger valgt av paret (role, kind), der role ∈ { seller, buyer, provider, client } og kind ∈ { standard, capacity }. Selve strengene er normative protokolldata — begge parters ML-DSA-65-signaturer forplikter seg til de eksakte bytene via body_hash. De er IKKE lokaliserte; den signerte kroppen er språknøytral. Enhver formuleringsendring krever en ny skjemaversjon (structured/v2).
De åtte strengene, deres oppslag (acknowledgement_for(role, kind)), og begrunnelsen for hver er fastsatt av referanseimplementasjonen. Samsvarende implementasjoner MUST sende ut byte-identiske bekreftelsesverdier; gylne-fixture SHA3-256 body-hash-tester som dekker alle fire rollekombinasjoner fanger opp eventuell drift.
Visningsrekkefølge for leseren. Bekreftelsesstrengene inneholder fraser som «described above», som forutsetter at beskrivelses-/omfangsradene gjengis foran bekreftelsene. Lesere MUST gjengi terms-arrayet i CBOR-rekkefølge; omorganisering bryter prosa-semantikken.
Motpartskontakt. Når Part Bs contact er en gyldig e-postadresse, sender qub-opplastingstjenesten automatisk en gjennomgangs-/medsignaturinvitasjon på e-post ved iscenesettelsestidspunkt og binder den eventuelle medsignaturen til verifisering av samme adresse (§9.7). Pakter der Part Bs kontakt mangler kan fortsatt medundertegnes, men kun via en out-of-band-kanal — tjenesten avviser medsignaturforespørsler som ikke kan produsere en matchende 15-minutters e-postverifiseringsmarkør.
6.2 Dom-kropp (content_type = 0x04)
En dom-kropp er den kanoniske CBOR-kodingen av en VerdictBody-verdi:
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; «hva endret seg, hva lærte du»
evidence_url: Option<String>, // ≤ 2,048 bytes; bare HTTPS; nøkkelen er fraværende når den utelates
}
Kanonisk CBOR-nøkkelrekkefølge:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← bare hvis til stede
"evidence_url" (13 encoded bytes) ← bare hvis til stede
"verdict_version" (16 encoded bytes)
Total serialisert dom-CBOR MUST NOT overstige 8 KB (samsvarer med registerraden ovenfor).
Utfalls-enum. Wire-byten er intensjonsnøytral; de fire bøttene Right / Partial / Wrong / Unfalsifiable dekker utfallsrommet for enhver dom-bærende intensjon. Per-intensjons-etiketter («Traff blink» / «Holdt» / «Levert» / «Bekreftet» for Right, osv.) er en leser-side gjengivelses-anliggende som løses opp mot moder-qub-ens intensjon — wire forblir språk- og intensjonsnøytral. Verdier utenfor 1..=4 MUST avvises ved dekoding.
Moder-kobling. En dom-qub bærer IKKE moder-referansen i kroppen. Moder-qub-ens Arweave-transaksjons-ID sendes ut som lagrings-taggen Parent-Tx-Id ved opplastingstidspunktet (§7 lagrings-tagg-laget). Dette holder kroppen som et selvstendig signert utsagn om selvvurdering; revisjonskjeden («rett om hva?») etableres via Arweave-tagg-oppslag.
Beviselenke-sikkerhet (normativt). Når evidence_url er til stede, MUST validatorer (kompose-side, wire-side, Worker-edge) håndheve:
- Bare HTTPS. Strengen MUST starte med bytesekvensen
https://. Enhver annen skjema —http,ftp,javascript,data,file, osv. — avvises. - Lengdetak. ≤ 2 048 byte (praktisk grense for nettlesere-URL).
- NFC + sjekk på fiendtlige kodepunkter. Samme regel som
titleogreflection— bidi-override / nullbredde / tag-blokk / BOM / C0 / C1 kodepunkter avvises. Definisjonen samsvarer med Rustcrate::handle::contains_hostile_text_codepointog TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(hold i lås). - Ingen mellomrom, ingen ASCII-kontrolltegn. Mellomrom / DEL / sub-
0x20-byte hvor som helst i URL-en avvises — lukker injeksjonsvektoren\n/\tsom bidi-regelen ikke dekker. - Ikke-tomt vertssegment. Alt mellom
https://og første/,?eller#MUST være ikke-tomt.
Ingen server-side henting. Worker MUST NOT proxyere, hente eller forhåndsvise URL-en. Protokollen lagrer en streng; gjengivelsen skjer leser-side med rel="nofollow noopener noreferrer" target="_blank" og en synlig vert vist ved siden av lenketeksten.
Refleksjon. Valgfri skaper-skrevet refleksjonstekst («hva endret seg, hva lærte du»). Samme NFC + sjekk på fiendtlige kodepunkter som title. Tom / bare-mellomrom-inndata kollapser til fraværende ved konstruksjon.
Skjemaversjon. v1 støtter kun verdict_version = 0x01. Framtidige skjemarevisjoner bumper denne byten og lander sammen med en ny protokollversjon per §12.
7. Forseglingsprotokoll
Den komplette forseglingssekvensen. Hvert trinn 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.
Lagringstaggslag (out-of-band). qub-opplastingstjenesten knytter et bevisst lite sett med lagringstransaksjonstagger sammen med den innpakkede nyttelasten. Content-Type=application/octet-stream er normativt påkrevet. Referansetjenesten knytter i tillegg tre valgfrie tagger når skaperen velger å eksponere dem: Intent (allowlist-validert kompositt-intensjon — f.eks. quote, reply, commitment), Author (skaperens §9.3-pubkey-fingeravtrykk som 64-tegns små heksadesimal), og Parent-Tx-Id (overordnet qubs lagringstransaksjons-ID for svarkjeder, 43-tegns base64url).
Author-taggen er opt-in per qub: referanse-skaper-appen knytter den kun til når brukeren eksplisitt aktiverer offentlig tilskrivelse ved forseglingstidspunkt. Når veksleren er av — som er standard — skrives ingen Author-tagg og qub-en er utilskrevet på kjeden: ingenting i permanent lagring lenker opplastingen til en skapers handle, e-post eller andre qubs. Når veksleren er på, løser Author-fingeravtrykket til skaperens valgte @handle via §9.5-attesteringskjeden. Forhold i svarkjeder og Intent er ikke-identifiserende. Det ytre omslaget (§13) beskytter den indre kroppen mot ciphertext-korrelasjon — det hindrer en høster i å gjenkjenne og masse-dekryptere qub-formede opplastinger etter at deres drand-runde publiseres.
Referansetjenesten knytter bevisst IKKE App-Name, App-Version eller Type-tagger: ethvert slikt enkeltverdifilter ville returnert hele qub-korpuset til et GraphQL-spørring, noe som er inkonsistent med omslagets body-only-konfidensialitetsomfang.
En samsvarende verifikator MUST NOT avhenge av noen lagringstagg for §11 tredjeparts-verifikasjon; body-hashen / qub_id / signaturen forplikter seg kun til den indre CBOR, aldri til taggsettet.
8. Opplåsningsprotokoll
Den komplette opplåsingssekvensen. Hvert trinn 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. Forfattersignering
9.1 Begrunnelse
Alle qub-er lagres i permanent lagring. Forfattersignaturer må forbli uforfalskbare på ubestemt tid, og det er derfor v1.0 bruker det post-kvantale ML-DSA-65-skjemaet (FIPS 204) framfor et klassisk skjema hvis sikkerhet kan svekkes innenfor qub-ens permanente levetid.
9.2 Algoritmeregister
sig_alg |
Skjema | Nøkkelstørrelse | Signaturstørrelse |
|---|---|---|---|
0x00 |
Ingen signatur (usignert) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1 952 byte | 3 309 byte |
Lesere MUST avvise ukjente sig_alg-verdier.
9.3 Konstruksjon av signert 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)
Domeneseparator: "QUB_AUTHOR_SIG_V1" er 17 ASCII-byte: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Ingen padding.
Avsluttende byte: den 91. preimage-byten MUST være 0x00. Referanseimplementasjonen eksponerer dette som konstanten ORG_ID_PRESENT_INDIVIDUAL = 0x00 i crates/qub-core/src/signing.rs; lesere som rekonstruerer sig_input for verifikasjon MUST sende ut samme byte.
Signaturomfang — hva som er og ikke er dekket. sig_input forplikter seg til fire envelope-felt: version, qub_id, body_hash, unlock_at (i tillegg til den faste domeneseparatoren og org_id_present-byten). Tre av disse fire er strukturelle invarianter: qub_id er selv utledet fra version, content_type, created_at, unlock_at, outcome_at, drand_round og body_hash via §4.1-preimagen, så enhver endring av disse feltene gir en annen qub_id og invaliderer signaturen transitivt. Den direkte autentiserte flaten er derfor:
| Felt | Autentisert av 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 |
— (implisitt) | Nøkkelen som verifiserte signaturen er forfatteren, per definisjon |
sender_label |
✗ | Kun visningstekst; mutérbar uten signaturbrudd |
reply_to |
✗ | Trådpeker; mutérbar uten signaturbrudd |
cosigner_pubkey / cosigner_signature |
— | Uavhengig signert over samme sig_input (se §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Ytre SealedQub-felt, ikke inne i envelopen — dekket av sine egne strukturelle invarianter (runde-/kjedekonsistens) men ikke av forfattersignaturen. (drand_round er nå bundet transitivt via qub_id-preimagen — se over.) |
Sikkerhetsimplikasjoner av ikke-autentiserte felt.
- En part med skrivetilgang til de lagrede bytene kunne bytte
sender_label(«Alice» → «Mallory») uten å invalidere forfattersignaturen.author_pubkeyinne i envelopen forblir det sanne identitetsankeret — lesere MUST utlede vist identitet fraauthor_pubkey(via §9.5-attesteringslaget) i stedet for å stole påsender_label. - Et
reply_to-felt kan på samme måte redigeres etter signering. Fordiqub_ider innholdsadressert, kan en angriper ikke pekereply_tomot et ikke-eksisterende mål, men de kan stille re-foreldre et svar til en annen eksisterende qub.
Implementasjoner som viser sender_label eller reply_to til sluttbrukere MUST vise den autentiserte identiteten (pubkey-fingeravtrykk, attestering) som det primære identitetssignalet, ikke etiketten.
9.4 Verifikasjonsprosedyre
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."
Signaturverifikasjon er den dyreste operasjonen (spesielt ML-DSA-65). Den SHOULD utføres etter at alle billigere sjekker (hash, qub_id, unlock_at) har passert.
9.5 Identitetsattesteringer
Identitetsattesteringer — kartleggingen av author_pubkey til menneskelig gjenkjennelige identitetspåstander som et qub-handle, e-postadresse, sosial handle eller passkey-legitimasjon — er en leserside-progressiv forbedring og er ikke nødvendige for signaturverifikasjon. Lesere som løser attesteringer til en visningsidentitet MUST anvende presedensen:
handle > email > social > fingerprint
Fingeravtrykk-tilbakefallet er små heksadesimal av SHA3-256(author_pubkey); det er alltid tilgjengelig for enhver signert qub. Lesere MAY forkorte det for visning — referanseleseren gjengir qub: etterfulgt av de første og siste fire bytene (qub:<8 hex>…<8 hex>).
En samsvarende verifikator kan fullføre hver sjekk i §9.4 uten å kontakte qub-API-et, uten noe nettverk utover permanent lagring og drand, og uten noe oppslag på serversiden. Attesteringsoppløsning er et separat best-effort-trinn utført kun etter at signaturverifikasjon har lyktes.
9.6 Størrelsesinnvirkning
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Signatur | 64 byte | 3 309 byte |
| Offentlig nøkkel | 32 byte | 1 952 byte |
| Totalt per qub | 96 byte | 5 261 byte |
| Lagringskostnadsdelta (ved ~$5/MB) | ~$0,0005 | ~$0,026 |
For en tekst-qub på 500–2 000 byte tredobler ML-DSA-65 omtrent den lagrede størrelsen. Den absolutte kostnaden er ubetydelig.
9.7 Medsignaturverifikasjon (bilaterale paktavtaler)
For bilaterale avtaler (content_type = 0x03) beviser et andre signaturlag at begge parter samtykket til de samme betingelsene.
Envelope-felt:
cosigner_pubkey: ML-DSA-65 offentlig nøkkel til medundertegneren (Part B).cosigner_signature: Signatur over sammesig_inputsom forfatteren (§9.3).
Begge felt MUST være til stede sammen eller begge fraværende. Hvis nøyaktig ett er til stede, MUST lesere rapportere en integritetsfeil.
Verifikasjonsprosedyre:
1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3.
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."
Egenskaper:
- Medundertegneren signerer den identiske
sig_inputsom forfatteren — begge parter forplikter seg til sammequb_id,body_hashogunlock_at. qub_id-utledning (§4.1) inkluderer IKKE medsignatur-felt. Å legge til en medundertegner til en eksisterende envelope endrer ikkequb_id.- En pakt kan være kun forfattersignert (énsidig forpliktelse), kun medundertegnet (uvanlig) eller begge (fullt bilateralt bevis).
E-postbindingsport (operasjonell). Når en iscenesatt pakt har en Part B e-postkontakt (§6.1), MUST qub-opplastingstjenesten avvise medsignaturforespørselen med mindre en kortvarig e-postverifiseringsmarkør eksisterer som matcher både iscenesettelses-ID-en og den normaliserte e-posthashen til den kontakten. Markøren skrives av /api/v1/auth/verify når magic-link-tokenet bærer en staging_id og den verifiserte adressen samsvarer med SHA-256(normalise_email(party_b.contact)) — der normalise_email(addr) bevarer storingbruken i lokaldelen og gjør kun domenedelen til små bokstaver (per RFC 5321 §2.3.11), og SHA-256 her er NIST FIPS 180-4-hashen (forskjellig fra SHA3-256 brukt i §4-utledninger) — og utløper 900 sekunder (15 minutter) etter utstedelse. Dette er en operasjonell anti-imitasjonsport, IKKE en del av on-chain-qub-beviset — en tredjepartsverifikator som spiller av §11 trenger kun permanent lagring og drand, uten noe oppslag på serversiden. Markøren eksisterer kun på serversiden og er aldri en del av den signerte kroppen.
Størrelsesinnvirkning (ML-DSA-65 forfatter + medundertegner):
| Komponent | Størrelse |
|---|---|
| Forfattersignatur | 3 309 byte |
| Forfatterens offentlige nøkkel | 1 952 byte |
| Medundertegnerens signatur | 3 309 byte |
| Medundertegnerens offentlige nøkkel | 1 952 byte |
| Total krypto-overhead | 10 522 byte |
| Lagringskostnadsdelta | ~$0,05 |
10. Markdown-gjengivelse og -sanitisering
Denne seksjonen er sikkerhetskritisk. Leseren gjengir tekst-qubs (content_type = 0x01) ved å bruke et begrenset Markdown-delsett.
10.1 Tillatte elementer
- Overskrifter:
#til####(ingen#####eller######) - Utheving: fet (
**), kursiv (*), gjennomstreking (~~) - Lister: ordnede (
1.) og uordnede (-,*) - Sitatblokker (
>) - Kode: inline-spenn (```) og inngjerdede blokker (`````)
- Horisontale linjer (
---) - Linjeskift (to mellomrom på slutten eller blank linje)
- Avsnitt
10.2 Forbudte elementer
| Element | Håndtering |
|---|---|
Rå HTML (<div>, <script>, osv.) |
Fjernes helt. Ingen HTML slipper gjennom. |
Bilder () |
Fjernes. Bildesyntaks fjernes fra utdataen. |
Lenker ([text](url)) |
URL gjengis som synlig rentekst. Ikke autolenket. Ikke klikkbar uten eksplisitt brukerhandling. |
| Farlige URL-skjemaer | javascript:, data:, vbscript:, file: — fjernes. |
| Iframes, embeds, objects | Fjernes. |
| HTML-entiteter | Dekodes til visningstegn kun hvis trygge. |
10.3 Implementasjon
Implementasjoner MUST bruke en streng allowlist-parser, ikke en blocklist. Den anbefalte tilnærmingen:
- Parse Markdown med
pulldown-cmark(eller tilsvarende). - Gå gjennom AST-en og dropp enhver node som ikke er på allowlisten (§10.1).
- For lenkenoder: send ut URL-en som synlig tekst, ikke som et klikkbart
<a>-element. - Konverter den filtrerte AST-en til en typet mellomrepresentasjon (f.eks. en
MarkdownNode-enum med kun trygge varianter). Rå HTML er strukturelt urepresenterbar i denne IR-en. - Gjengi fra den typede IR-en til mål-visningslaget (f.eks. reaktive visningskomponenter, DOM-noder). Ingen HTML-strengkonkatenering eller
innerHTMLpå noe tidspunkt.
Blocklist-tilnærminger er sårbare fordi nye Markdown-utvidelser eller parser-særegenheter kan introdusere ufiltrerte elementer. Den typede AST-tilnærmingen gjør XSS strukturelt umulig — det finnes ingen variant som kan bære vilkårlig HTML.
10.4 Størrelses- og strukturgrenser
- Maksimal gjengivelsesdybde for overskrifter:
####(H4).#####og dypere gjengis som fet tekst. - Ingen grense for antall avsnitt (kroppsstørrelsesgrenser i §6 er begrensningen).
- Inngjerdede kodeblokker: ingen syntaksuthevning i MVP. Gjengis som monospace forhåndsformatert tekst.
11. Tredjepartsverifikasjon
Enhver tredjepart kan verifisere en offentlig qub uten samarbeid fra qub. Verifikasjonsprosedyren:
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.
Hva verifikasjonen beviser:
| Bevis | Hva det fastslår |
|---|---|
| Forpliktelse | Cipherteksten eksisterte ved lagringsblokkens tidsstempel. |
| Integritet | Klartekstkroppen samsvarer med den forpliktede hashen og har ikke blitt endret. |
| Tidspunkt | Innholdet var uleselig fram til drand-runden, som tilsvarer den valgte opplåsingstiden (forutsatt tlock- og drand-sikkerhetsantagelser). |
Hva verifikasjonen IKKE beviser:
| Ikke-bevis | Hvorfor |
|---|---|
| Forfatterskap | sender_label er dekorativ. Uten sig_alg ≥ 0x01 kunne hvem som helst ha forseglet dette innholdet. |
| Intensjon | qub-en beviser innhold og tidspunkt, ikke hva skaperen subjektivt mente. |
| Tidspunkt før hendelse | Lagringsblokk-inkludering kan ligge minutter etter faktisk opplasting. Forpliktelsestidsstempelet er blokk-tiden, ikke øyeblikket brukeren trykket «forsegle». |
12. Versjonering
12.1 Protokollversjon
version-feltet (u8) i både SealedQub og QubEnvelope identifiserer hovedprotokollversjonen.
- Lesere MUST avvise ukjente hovedversjoner med en tydelig feil.
- Kjente hovedversjoner MAY tolerere ukjente valgfrie felt hvis fremtidskompatibilitetsregler tillater det (valgfrie felt som mangler fra den kanoniske nøkkelrekkefølgen ignoreres).
- Content types (
content_type) og signaturskjemaer (sig_alg) er versjonsstyrte: nye verdier kan kun introduseres sammen med en ny protokollversjon eller eksplisitt registeroppdatering.
12.2 Versjonshistorikk
| Versjon | Verdi | Beskrivelse |
|---|---|---|
| v1 | 0x01 |
Offentlige tekst-qubs (content_type 0x01), bilaterale paktavtaler (0x03, structured/v1-skjema, ML-DSA-65 forfatter + medundertegner), tlock, SHA3-256 |
12.3 Fremtidskompatibilitet
En v1-leser som støter på en QubEnvelope med ukjente valgfrie CBOR-map-nøkler (nøkler som ikke er i §3.2 kanoniske rekkefølge) SHOULD ignorere disse nøklene og fortsette med verifikasjon ved bruk av kjente felt. Dette tillater fremtidige mindre tilføyelser (f.eks. nye metadata) uten å kreve et hovedversjonsopprykk.
En v1-leser som støter på sig_alg = 0x01 (ML-DSA-65) men mangler ML-DSA-65-verifikasjonsstøtte SHOULD vise qub-innholdet med et varsel om «signatur til stede, men ikke verifiserbar», ikke avvise qub-en helt. Referanseimplementasjonen avviser i dag enhver sig_alg-verdi annet enn 0x00 og 0x01 fordi v1-registeret ikke inneholder noen annen gyldig algoritme — streng avvisning og soft-fail er observasjonelt identiske inntil en tredje algoritme er registrert. Soft-fail-atferden ovenfor blir bærende når §9.2 tar opp en ny oppføring, og referanseleseren vil bli oppdatert til soft-fail på det tidspunktet.
12.4 Ytre omslagsversjon
OuterWrapper beskrevet i §13 bærer sin egen version-byte, uavhengig av SealedQub.version og QubEnvelope.version. De to versjonsrommene utvikler seg separat: en framtidig post-kvantesikker symmetrisk erstatning hever omslagsbyten uten å berøre den indre protokollversjonen, og en framtidig protokoll-lag-tilføyelse (f.eks. et nytt envelope-felt) hever den indre versjonen uten å berøre omslagsbyten.
OUTER_WRAPPER_VERSION_* |
Verdi | Algoritme | Status |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM med 12-byte nonce, 16-byte autentiseringstagg, AAD bundet til qub_id |
v1-standard |
| — | 0x02–0xFF |
Reservert | Framtidig |
Lesere MUST avvise ukjente omslagsversjoner med en tydelig feil. Protokollen holder bevisst omslagsversjonsrommet smalt inntil en konkret migrasjonsdriver dukker opp (f.eks. NIST-veiledning som favoriserer en annen AEAD); et 0x02-slot vil bli tildelt i samme revisjon som introduserer algoritmen.
13. Ytre krypteringsomslag
13.1 Begrunnelse
Protokoll-lagene (QubEnvelope → tlock → SealedQub) gjør en forseglet qub tidslåst: kroppen er uleselig fram til unlock_at og drand-rundesignaturen er publisert. Etter opplåsing er imidlertid rundesignaturen offentlig og den kanoniske CBOR-formen til SealedQub gjenkjennelig, så en høster som indekserte transaksjoner i permanent lagring kunne masse-dekryptere hele qub-korpuset.
Det ytre krypteringsomslaget lukker den kanalen ved å plassere et ekstra symmetrisk AEAD-lag mellom den kanoniske SealedQubCbor og bytene som lastes opp til permanent lagring. Den 256-biters nøkkelen K lever kun i URL-fragmentet til leveringslenken og på brukerenheter; nettlesere overfører ikke URL-fragmenter til servere, så qub.social, hver lagringsgateway, og hvert CDN foran dem er observasjonelt blinde for K. Hver qub i permanent lagring er derfor en ugjennomsiktig ciphertext hvis klartekst ikke kan gjenopprettes uten URL-en skaperen valgte å dele.
Netto effekt:
- Enumereringsimmunitet som standard. Innpakkede byte i permanent lagring er byte-uskillbare fra vilkårlig ciphertext. En høster-strategi om «GraphQL-spørring etter qub-formede opplastinger, masse-dekryptering med offentlige drand-signaturer» avsluttes ikke med klartekst.
- Krypto-makulerings-personvernholdning. qub.social kan bokstavelig talt ikke dekryptere sitt eget korpus. Innkallelser når ciphertext, ikke klartekst.
- To-trinns konfidensialitetsstige. Standard = lenkekontrollert tilgang (denne seksjonen). Mottaker-krypterte private qubs (en reservert Fase 2-funksjon, ennå ikke spesifisert) lag på toppen som andre nivå.
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 opplåsing på protokoll-laget (§7, §8) er uendret under omslagsgrensen; omslaget kobles til ved kallstedet for seal() og kobles fra ved kallstedet 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
}
Felt-invarianter.
versionMUST være lik0x01for v1.0-omslagsbyte.qub_idMUST være likqub_id-feltet til SealedQub gjenopprettet etter utpakking. Utpakkingstrinnet håndhever ikke dette direkte (AEAD AAD-binding gjør byte-nivå-tukling umulig), men opplåsingslaget sjekker forholdet transitivt: hvis en skaper pakker inn enSealedQubCborhvis indrequb_idikke samsvarer med omslagetsqub_id, mislykkes §8 trinn 11.nonceMUST være 96 bit (12 byte), generert friskt av en CSPRNG for hver innpakkingsoperasjon. Gjenbruk av en nonce under samme nøkkel tillater AEAD-nonce-gjenbruksangrep som gjenoppretter klarteksten; produsenter MUST behandle paret (key,nonce) som engangsbruk.ciphertexter AES-256-GCM-utdataen: ciphertext-byte konkatenert med den 16-byte autentiseringstaggen.ciphertext.len() == SealedQubCbor.len() + 16nøyaktig.
CBOR-koding. Kanonisk CBOR per §3, med samme nøkkelsorteringsregel (sortert etter kodet bytelengde stigende, deretter leksikografisk). De fire nøklene er:
| Nøkkel | Kodede byte | Rekkefølge |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
Den første byten til OuterWrapper-CBOR-en er derfor map-headeren med definitiv lengde for en 4-elements map (0xA4).
13.4 AAD-binding til qub_id
Omslaget binder qub_id som AEAD additional authenticated data. Dette er det bærende strukturelle forsvaret mot tre klasser av angrep:
| Angrep | Forsvar |
|---|---|
Flytt ciphertext under et annet qub_id-felt i omslaget |
AAD-uoverensstemmelse → AEAD-autentisering mislykkes |
| Bland URL-fragmentet til qub A med byte-ene i permanent lagring til qub B | AAD-uoverensstemmelse → AEAD-autentisering mislykkes |
Tukle med qub_id-feltet til omslaget etter opplasting |
AAD-uoverensstemmelse → AEAD-autentisering mislykkes |
Å bære qub_id i omslagets klartekst svekker ikke enumereringsimmunitet på en meningsfull måte — qub_id er selv en SHA3-256-hash av §4.1-preimagen uten noen gjenopprettbar preimage fra digesten, og en enumerator som allerede har høstet omslagets byte lærer ingenting fra det synlige qub_id som de ikke kunne utlede fra eksistensen av opplastingen selv.
13.5 Innpaknings- og utpakkingsalgoritmer
wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
require K.len() == 32 and N.len() == 12 and Q.len() == 32
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C includes the 16-byte authentication tag at the end
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
require K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
require O.version == 0x01 // §12.4
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
return P // P is the inner SealedQubCbor
Kollaps av feilmoduser. Feil K, feil nonce, AAD-uoverensstemmelse og tuklet ciphertext gir alle samme DECRYPT_FAILED-feil. Dette er en bevisst AEAD-egenskap: å skille feilmodusen ville skape en sidekanal en fjern angriper kunne sondere ved å sende defekte omslag og time responsen. Referanseimplementasjoner MUST kollapse alle AEAD-feil til én enkelt feilform.
13.6 Nøkkelmateriale og distribusjon
Innpakkingsnøkkelen K er en 256-biters uniform tilfeldig verdi generert per-qub av en CSPRNG. Referanseimplementasjonene henter den fra:
- WASM-skaper:
getrandom(WebCrypto underwasm_js-backend). - Worker-tjenerside-forseglingsrute:
crypto.getRandomValues.
Distribusjon: K MUST kodes som URL-sikker base64 (RFC 4648 §5, ingen padding) og legges til leveringslenken som fragmentkomponenten:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
Fragmentet overføres aldri til noen server av en samsvarende nettleser. Gjenopprettingskanaler (server-side historikk-indeks, opt-in e-post autosending) som beholder den fullstendige leveringslenken — inkludert fragmentet — utover brukerens enhet, er en eksplisitt avveining mot standard krypto-makulerings-holdningen og MUST betinges av eksplisitt brukersamtykke.
Tap av fragment. Hvis en bruker mister URL-fragmentet og ikke har noen gjenopprettingskanal, er qub-en uleselig. Dette er den bærende avveiningen i designet og MUST opplyses til brukeren ved forseglingstidspunkt. MVP-en styrker forseglings-tidspunkt-opplysningen med eksplisitt «lagre denne URL-en»-tekst og en verifisert-e-post-gjenopprettingskanal for brukere som velger inn.
13.7 Utenfor omfang for denne seksjonen
- Forfattersignering (§9) er uendret: signaturer beregnes inne i den indre
QubEnvelopeog gjenopprettes etter utpakking → tlock-dekryptering → CBOR-parsing. - Mottaker-krypterte private qubs (en reservert Fase 2-funksjon, ennå ikke spesifisert) komponerer på toppen av dette omslaget som et andre konfidensialitetsnivå; begge nivåer kan være aktive samtidig.
- Pakter (§6, content_type
0x03) pakkes inn nøyaktig som tekst-qubs; omslaget er byte-blindt for indre content type.
13.8 Offentlige qubs (utelatelse av omslag)
Det ytre omslaget er valgfritt på leveringslaget. En skaper kan forsegle en qub som offentlig, og i så fall skrives den kanoniske SealedQubCbor til permanent lagring direkte, uten noe OuterWrapper-lag og uten noen nøkkel 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 lenkekontrollert: den forblir uleselig fram til drand-runden publiseres (tlock-laget er uendret), men etter opplåsing kan hvem som helst med arweave_tx_id dekryptere den — det kreves ikke noe URL-fragment, fordi det ikke finnes noen K. Dette er den bevisste avveiningen for flater som serveren må drive: e-poster med avsløringsvarsel, tredjeparts-innbygginger og rikere SEO etter avsløring trenger alle en lenke som virker uten en hemmelighet serveren aldri holder (§13.6).
Konsekvenser en produsent MUST ta høyde for:
- Ingen enumereringsimmunitet. Offentlige qubs gir av konstruksjon avkall på enumereringsimmunitets-egenskapen i §13.1. Referanse-opplastingstjenesten stempler en
Visibility: public-tagg i permanent lagring på dem (og bare dem), slik at de med vilje er oppdagbare; private qubs bærer ingen slik tagg og beholder sin byte-uskillbarhet. - Klartekst-tittel eksponert ved forseglingstidspunkt.
title-feltet i §3.2 er klartekst inne iSealedQubCbor. Under omslaget er den skjult fram til en leser oppgirK; uten omslaget er den verdenslesbar i permanent lagring fra opplastingsøyeblikket, før opplåsing. Samsvarende skaper-apper MUST opplyse om dette ved forseglingstidspunkt. - Deteksjon er strukturell. En samsvarende leser/innbygging skiller de to formene ved parsing: byte som parser som
OuterWrappertar utpakk-med-K-veien; byte som parser som en barSealedQubCboraksepteres direkte. Det kreves ikke noe wire-flagg, ogqub_idbinder ikke synlighet — det samme innholdet er byte-identisk påSealedQub-laget enten det er forseglet offentlig eller privat.
Privat (innpakket) forblir standarden; offentlig er et eksplisitt per-qub-valg fra skaperen.
14. Testvektorer
14.1 qub_id-utledning
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
Implementasjoner MUST produsere identiske body_hash- og qub_id-verdier for denne inndataen. Denne testvektoren SHOULD være den første enhetstesten skrevet. De kanoniske verdiene over ble beregnet av referanseimplementasjonen og MUST stemme bit-for-bit. Historiske preimage-oppsett (før lansering — ingen aktive qubs var avhengige av disse): den 92-byte V1.0-qub_id var 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; den 100-byte V1.1-qub_id (etter folding av outcome_at_or_zero) var b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 folder drand_round inn og hever domeneseparatoren til QUB_ID_V2.
14.2 Mapping til opplåsingsrunde
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
Implementasjoner MUST verifisere at serialize(parse(serialize(qub))) == serialize(qub) for alle gyldige innputt. Dette er en egenskapstest, 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-byte-ene og SHA3-256 body_hash beregnes av referanseimplementasjonen. Implementasjoner MUST produsere byte-identisk CBOR for denne inndataen.
Implementasjoner MUST også verifisere at serialize(parse(serialize(pact))) == serialize(pact) for alle gyldige PactTerms-innputt (egenskapstest).
14.5 Tverr-språk-vektorer for ytre omslag
Det ytre omslaget (§13) har en separat kanonisk fixture på crates/qub-core/tests/vectors/wrapper_v1.json. Hvert tilfelle fester et (key, nonce, qub_id, sealed_cbor)-tuppel som ugjennomsiktige hex-innputt og hevder en spesifikk expected_wrapper_hex-utdata. Begge referanseimplementasjonene konsumerer den samme JSON-filen:
- 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 fester for øyeblikket tre tilfeller:
| Tilfelle | Dekning |
|---|---|
basic-text-public |
Minste realistiske SealedQub-form; ingen valgfrie felt. Etablerer den kanoniske omslagsformen for en v1.0-typisk qub. |
with-recipient-pubkey |
SealedQub med recipient_pubkey satt (Fase 2-sti). Annet indre CBOR-nøkkelsett, annen qub_id. |
longer-body |
~4 KiB kropp — utøver multi-byte CBOR-lengdeprefikser inne i både den indre envelopen og den ytre cipherteksten. |
Implementasjoner MUST produsere byte-identisk expected_wrapper_hex for de registrerte innputtene. Regenerering av fixturen krever QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors og er reservert for bevisste formatendringer.
15. Krypto-profil-styring (framtidig)
Denne seksjonen er informativ for v1 og blir normativ første gang en andre algoritme entrer noen av qubs kryptografiske primitiver.
15.1 Nåværende holdning
Protokoll v1 binder nøyaktig én algoritme per primitiv:
- Signatur: ML-DSA-65 (
sig_alg = 0x01; 1952-byte offentlig nøkkel, 3309-byte signatur) og usignert (sig_alg = 0x00). §9.2-registeret definerer ingen andre verdier; en v1-verifikator MUST avvise enhversig_algutenfor{0x00, 0x01}. En fremtidig Ed25519-oppføring er forventet (§15.3), men er ikke tildelt i v1. - Tidslås: kun drand quicknet — kjedehashen, den offentlige nøkkelen, genesis-tiden og perioden er faste nettverksparametere som bæres av referansen
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) ogconfig/drand-endpoints.json. - Ytre omslag: kun AES-256-GCM v1 (§13).
Verifikatorer hardkoder for øyeblikket nøkkel- og signaturlengder per primitiv. Ingen smidighetsflate er eksponert av wire-formatet.
15.2 Tiltenkt form
Når en andre algoritme entrer protokollen, vil verifikatoren bli konfigurert for en navngitt CryptoProfile (f.eks. ExqubV1) som lister opp det nøyaktige settet med tillatte verdier per primitiv — sig_algs, drand-kjeder, omslagsversjoner, content types. Profilen er fast ved verifiseringstidspunkt, aldri forhandlet in-band. Enhver verdi utenfor den aktive profilen avvises.
Dette garanterer at å legge til ML-DSA-87 eller aktivere Ed25519 ikke kan retroaktivt svekke eksisterende verifikator-konfigurasjoner: en v1-verifikator forblir en v1-verifikator selv etter at en v2-profil er publisert.
15.3 Utløsningsbetingelser
Forfremme §15 til normativ status når noe av følgende foreslås:
- En andre
sig_alg-byte (Ed25519-aktivering, ML-DSA-87, eller enhver ny oppføring i §9-registeret). - En andre drand-kjede i produksjonsbruk.
- En andre ytre-omslagsversjon.
Inntil da er §15 en plassholder som fester migrasjonsformen slik at framtidige PR-er lander mot et kjent mål framfor å re-prosessere forhandlingsflaten fra bunnen av.