qub-protocolspecificatie
qub is een protocol voor cryptografische temporele toezeggingen: een systeem om woorden te verzegelen tot een toekomstige datum en, wanneer die datum aanbreekt, te bewijzen wat er precies werd gezegd en wanneer.
Drie primitieven maken dit mogelijk. drand is een gedecentraliseerd willekeursignaal — de onthullingsdatum wordt afgedwongen door de fysica, niet door de welwillendheid van een betrokken partij. Permanente openbare opslag is een manipulatiebestendige openbare opslag — geen enkele partij kan een qub bewerken of verwijderen zodra deze is verzegeld. ML-DSA-65 is een post-quantum digitale handtekening — elke qub is verbonden aan een sleutelpaar waarvan het geheim het apparaat van de auteur nooit verlaat.
Samen produceren deze primitieven een verklaring die tijdvergrendeld, manipulatiebestendig en toewijsbaar is — een ontvangstbewijs waarvan de waarde groeit naarmate het vermogen van de wereld om het verleden te vervalsen toeneemt.
De rest van dit document is de normatieve specificatie die nodig is voor interoperabele implementaties.
qub-protocolspecificatie
| Veld | Waarde |
|---|---|
| Versie | 1.0 (protocolversie 0x01, buitenste wrapper-versie 0x01) |
| Datum | 2026-05-01 |
| Status | Concept |
| Beoordeeld tot | 2026-05-01 |
Dit document is de normatieve protocolspecificatie voor het qub-systeem voor temporele toezeggingen. Het definieert datastructuren, serialisatieregels, afleidingsformules en verificatieprocedures die nodig zijn voor interoperabele implementaties.
Reikwijdte: de protocollaag is opzettelijk taalneutraal — de qub-body is ondoorzichtige platte tekst / markdown / pact-bytes, en locale-afhankelijke rendering is de verantwoordelijkheid van de lezer (qub.social-webapp, <qub-embed>-iframe, MCP-clients, enz.).
1. Notatie en conventies
| Notatie | Betekenis |
|---|---|
u8, u64, i64 |
Niet-getekende / getekende gehele getallen van de aangegeven bitbreedte |
[u8; N] |
Byte-array met vaste lengte van N bytes |
Vec<u8> |
Byte-array met variabele lengte |
Option<T> |
Waarde van type T, of afwezig |
String |
UTF-8-tekenreeks, NFC-genormaliseerd |
| ` | |
SHA3-256(x) |
NIST SHA3-256-hash van bytereeks x (FIPS 202) |
ceil(x) |
Afrondingsfunctie naar boven: kleinste geheel getal ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Meest significante byte eerst |
Alle gehele getallen in preimage-constructies worden gecodeerd als big-endian-byte-arrays met vaste breedte (i64 → 8 bytes, u8 → 1 byte), tenzij anders aangegeven.
Alle tijdstempels zijn Unix-seconden in UTC.
2. Datastructuren
2.1 ComposeQub (in-memory-status van de maker)
Niet geserialiseerd naar CBOR. Niet naar permanente opslag geschreven. Lokaal in de maker-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 (ontsleutelde payload)
Geserialiseerd met canonieke CBOR (§3). Versleuteld binnen de SealedQub. Dit is de structuur die de integriteit van de inhoud bewijst na ontsleuteling.
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
}
Basis (onondertekende tekst-qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, alle Option-velden afwezig.
Andere v1-configuraties: content_type = 0x03 (pact-body, zie §6.1); sig_alg = 0x01 (ML-DSA-65) met author_signature en author_pubkey aanwezig (zie §9.3); cosigner_pubkey en cosigner_signature samen aanwezig voor medeondertekende pacts (zie §9.7); reply_to ingesteld op het qub_id van de bovenliggende qub voor qubs in antwoordketens (zie §9.3 voor de implicaties voor het handtekeningbereik).
2.3 SealedQub (canoniek wire-formaat)
Geserialiseerd met canonieke CBOR (§3). Naar permanente opslag geschreven. Dit is het on-chain-artefact.
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 (toepassingsstatus van de lezer)
Niet geserialiseerd naar CBOR. Lokaal in de lezer-app. Geconstrueerd na geslaagde ontsleuteling en verificatie.
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 — overgenomen uit QubEnvelope.outcome_at / SealedQub.outcome_at; voedt het verdict-watch-blok op de onthullingspagina (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. Canoniek CBOR-profiel
Alle SealedQub- en QubEnvelope-serialisatie MOET voldoen aan dit profiel. Twee implementaties moeten, gegeven dezelfde logische structuur, identieke bytes produceren.
3.1 Coderingsregels
| Regel | Specificatie |
|---|---|
| Standaard | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Volgorde van mapsleutels | Gesorteerd op gecodeerde bytelengte eerst (korter vóór langer), daarna lexicografisch (byte-voor-byte voor coderingen van dezelfde lengte) |
| Gehele-getalcodering | Kortste vorm: 0–23 in initiële byte; 24–255 in 2 bytes; 256–65535 in 3 bytes; enz. |
| Lengtecodering | Alleen bepaalde lengten. Geen arrays, maps, byte-strings of tekst-strings met onbepaalde lengte (additional info = 31 is verboden). |
| Tags | Geen CBOR-tags (major type 6 is verboden). |
| Drijvende komma | Geen floats (major type 7 waarden 0xF9–0xFB zijn verboden). |
| Tekst-strings | UTF-8-gecodeerd, NFC-genormaliseerd (Unicode Normalization Form C). |
| Byte-strings | Ruwe bytes. Geen base64-codering op de CBOR-laag. |
| Dubbele sleutels | Weigeren met fout. Parsers MOGEN GEEN dubbele mapsleutels stilzwijgend accepteren. |
| Onbekende sleutels | Weigeren met fout. Parsers MOGEN GEEN mapsleutels buiten de canonieke sleutelset van het type tolereren — twee verschillende canonieke byte-strings mogen nooit naar dezelfde waarde decoderen (encode(decode(x)) == x), en voor ondertekende payloads zou een extra sleutel verborgen inhoud zijn waaraan beide handtekeningen zich vastleggen. Schema-evolutie verloopt via version, nooit via extra sleutels. |
| Eenvoudige waarden | Alleen true (0xF5), false (0xF4) en null (0xF6) zijn toegestaan. |
| Optionele velden | Afwezige optionele velden worden volledig uit de CBOR-map weggelaten (niet gecodeerd als null). Aanwezige optionele velden worden opgenomen in gesorteerde sleutelvolgorde. |
3.2 Geverifieerde canonieke sleutelvolgordes
Deze sleutelvolgordes zijn normatief. Implementaties MOETEN sleutels in exact deze volgorde uitvoeren. Debug-asserties ZOUDEN de volgorde MOETEN verifiëren in niet-release-builds.
QubEnvelope (versie 0x01, niet ondertekend, alle optionele velden afwezig):
"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)
Afleiding van de QubEnvelope-sleutelvolgorde: elke sleutel is een CBOR-tekst-string. Gecodeerde lengte = 1 byte header + stringlengte (voor strings korter dan 24 bytes). Sorteer eerst op totale gecodeerde lengte, daarna lexicografisch voor sleutels van dezelfde lengte.
SealedQub (versie 0x01, openbaar, geen ontvanger):
"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 (rij van de terms-array):
"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 Bytecoderingsreferentie
| Type | CBOR-codering | Voorbeeld |
|---|---|---|
| SHA3-256-hash (32 bytes) | 0x58 0x20 + 32 bytes |
body_hash, qub_id |
| Tijdstempels (i64) | Major type 0 (positief) of 1 (negatief), kortste codering | Unix-seconden |
| Versie (u8, waarde 1) | 0x01 (enkele byte) |
|
| Inhoudstype (u8, waarde 1) | 0x01 (enkele byte) |
|
| sig_alg (u8, waarde 0) | 0x00 (enkele byte) |
|
| ML-DSA-65-handtekening (3.309 bytes) | 0x59 0x0C 0xED + 3.309 bytes |
author_signature, cosigner_signature |
| ML-DSA-65-publieke sleutel (1.952 bytes) | 0x59 0x07 0xA0 + 1.952 bytes |
author_pubkey, cosigner_pubkey |
4. Normatieve afleidingen
4.1 qub_id
De qub_id identificeert een qub op unieke wijze en koppelt de QubEnvelope aan de SealedQub. Hij wordt deterministisch afgeleid uit de envelope-inhoud.
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
Codering van de domeinseparator: de string "QUB_ID_V2" bestaat uit 9 ASCII-bytes. Eén 0x00-padding-byte wordt toegevoegd om 10 bytes te bereiken voor uitlijning. Implementaties MOETEN exact deze 10 bytes gebruiken: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
Codering van outcome_at: V1.1 heeft het preimage uitgebreid van 92 naar 100 bytes om het optionele outcome_at-veld in de binding op te nemen. Een afwezige outcome_at wordt gecodeerd als 8 nul-bytes; de protocolvalidators weigeren outcome_at <= 0 overal, zodat deze sentinel niet kan botsen met een legitieme waarde. Zie §3.2 (wire-formaat) en het tasks/verdict-uplift-plan.md-document in de repo voor de oordeel-mechanica die dit veld motiveert.
Codering van drand_round: V1.2 heeft het preimage uitgebreid van 100 naar 108 bytes om drand_round (de doel-drand-ronde, §4.3) in de binding op te nemen, en heeft de domeinseparator verhoogd naar QUB_ID_V2. Dit bindt de tijdslot-ronde aan de identiteit van de qub: een gateway kan de ciphertext niet aan een andere (bijv. al verstreken) ronde binden dan de getoonde unlock_at impliceert. De ontsleutel-procedure (§8) verifieert daarnaast dat de ronde die in de tlock-ciphertext-stanza is ingebakken overeenkomt met unlock_round(unlock_at), zodat de getoonde ontgrendelingstijd aantoonbaar de ronde is die de ontsleuteling afschermt.
Eigenschappen:
- Het wijzigen van een veld in de QubEnvelope (body, tijdstempels, inhoudstype, versie) produceert een andere qub_id.
- De qub_id wordt berekend vóór versleuteling. Zowel QubEnvelope als SealedQub dragen dezelfde qub_id. De lezer verifieert dat ze overeenkomen na ontsleuteling.
- qub_id is niet afhankelijk van
sender_label,author_signatureofauthor_pubkey. Dit betekent dat dezelfde inhoud, verzegeld op hetzelfde moment, dezelfde qub_id oplevert ongeacht wie er ondertekent. - Het wijzigen van de
titlevan de SealedQub (met alle andere velden vast) verandertqub_idviatitle_hash. Een gateway kan daarom de platte-tekst-titel die op de aftelling wordt getoond niet vervangen zonder de identiteit van de qub ongeldig te maken. - Het wijzigen van de
outcome_atvan de SealedQub (met alle andere velden vast) verandertqub_idvia het preimage. Een gateway kan de pre-onthullings-oordeel-op-datum die op de aftelling wordt getoond niet vervangen zonder de identiteit van de qub ongeldig te maken. - Het wijzigen van
drand_round(met alle andere velden vast) verandertqub_idvia het preimage. Een gateway kan de tijdslot-ciphertext niet aan een andere ronde binden zonder de identiteit van de qub ongeldig te maken; gecombineerd met de stanza-ronde-controle bij ontgrendeling uit §8 is de getoondeunlock_atde ronde die de ontsleuteling daadwerkelijk afschermt.
4.2 body_hash
body_hash = SHA3-256(body)
Waarbij body de ruwe Vec<u8>-inhoudspayload is. Voor tekst-qubs is dit de UTF-8-gecodeerde 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
Waarbij title de optionele platte-tekst-titel is die wordt getoond op de aftelling van de lezer vóór onthulling (zie §3.2). NFC-normalisatie wordt op hashtijd uitgevoerd zodat de digest stabiel is voor visueel gelijkwaardige code-puntreeksen. De all-zeros-sentinel is gereserveerd voor het afwezige geval; een lege string wordt geweigerd op de canonieke CBOR-grens als een niet-canonieke codering van "afwezig" (de canonieke codering laat het veld volledig weg).
4.3 Unlock-ronde-mapping
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Parameter | Bron | Voorbeeld |
|---|---|---|
unlock_at |
Door gebruiker gekozen Unix-seconden 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 |
De ceil()-bewerking selecteert de eerste drand-ronde waarvan de onthullingstijd ≥ unlock_at is. Dit zorgt ervoor dat de qub niet ontsleutelbaar wordt vóór de gekozen ontgrendelingstijd.
Randgeval: als (unlock_at - chain_genesis_time) exact deelbaar is door chain_period_seconds, is het resultaat precies die ronde — de qub wordt ontgrendeld op exact de onthullingstijd van die ronde.
Validatie: unlock_at MOET in de toekomst liggen op het moment van verzegelen. unlock_at MAG NIET meer dan 10 jaar na created_at liggen (om langetermijn-drand-afhankelijkheidsrisico te beperken; de UI ZOU MOETEN waarschuwen voor ontgrendelingsdata buiten 2 jaar).
5. Wire-formaat-newtypes
Wire-formaat-newtypes bieden veiligheid tijdens compilatie tegen het verwarren van CBOR-bytes met JSON, ruwe platte tekst of andere bytecoderingen.
| Type | Bevat | Geproduceerd door | Geconsumeerd door |
|---|---|---|---|
SealedQubCbor |
Canonieke CBOR van SealedQub | serialize_sealed_qub() |
Permanente-opslag-upload, lezerophalen |
QubEnvelopeCbor |
Canonieke CBOR van QubEnvelope | serialize_qub_envelope() |
tlock-versleutelingsinvoer, tlock-ontsleutelingsuitvoer |
5.1 Constructieregels
// 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 Validatie bij constructie
from_encoded() ZOU MOETEN valideren dat de invoer begint met een geldige CBOR-map-header. Volledige structurele validatie gebeurt op parse-tijd, niet op constructietijd, om dubbel parseren te vermijden.
6. Inhoudstype-register
| Waarde | Type | Max body-grootte | Opmerkingen |
|---|---|---|---|
0x00 |
Gereserveerd (ongeldig) | — | MAG NIET worden gebruikt |
0x01 |
Platte tekst (UTF-8, beperkte Markdown) | 50 KB betaald / 10 KB gratis | Zie §10 voor renderregels. De splitsing tussen gratis en betaald wordt afgedwongen door de uploadservice; het harde plafond op de protocollaag is 50 KB. |
0x02 |
Gereserveerd (toekomstig) | — | Toegewezen aan een toekomstig inhoudstype; niet geldig in v1. Lezers MOETEN weigeren volgens de regel hieronder. |
0x03 |
Pact (bilaterale overeenkomst, CBOR-body) | 100 KB | Body is canonieke CBOR PactTerms (§6.1). Medeondertekenaarsondertekening volgens §9.7. |
0x04 |
Verdict (zelfbeoordeling door de maker, CBOR-body) | 8 KB | Body is canonieke CBOR VerdictBody (§6.2). Wordt alleen uitgezonden door de systeemzijdige verdict-intentie. De relatie met de bovenliggende qub staat op de Arweave-tag Parent-Tx-Id, niet in de body. Zie verdict-uplift-plan §3.4. |
Lezers MOETEN onbekende inhoudstypen weigeren met een duidelijke voor de gebruiker zichtbare fout. Lezers MOGEN GEEN poging doen onbekende typen als tekst te renderen.
6.1 Pact-body (content_type = 0x03)
Een pact-body is de canonieke CBOR-codering van een PactTerms-waarde:
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)> }
Canonieke CBOR-sleutelvolgordes voor alle drie de maps staan in §3.2. De totale geserialiseerde pact-CBOR MAG NIET groter zijn dan 100 KB (komt overeen met §6).
Schema-discriminator. De eerste rij in terms voor een structured/v1-pact MOET zijn: { key: "pact_schema", value: "structured/v1" }. Rijen zonder deze marker zijn "custom"-pacts en ontvangen geen gestructureerde validatie of schemabewuste rendering.
Bevroren bevestigingsslots. structured/v1-pacts bevatten precies vier bevestigingsrijen onder deze sleutels:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
De value voor elk is een van acht bevroren Engelse strings, gekozen op basis van het paar (role, kind), waarbij role ∈ { seller, buyer, provider, client } en kind ∈ { standard, capacity }. De strings zelf zijn normatieve protocoldata — de ML-DSA-65-handtekeningen van beide partijen leggen zich vast op de exacte bytes via body_hash. Ze worden NIET gelokaliseerd; de ondertekende body is taalneutraal. Iedere wijziging van de bewoording vereist een nieuwe schemaversie (structured/v2).
De acht strings, hun opzoeking (acknowledgement_for(role, kind)) en de rationale voor elk zijn vastgepind door de referentie-implementatie. Conforme implementaties MOETEN byte-identieke bevestigingswaarden produceren; golden-fixture-SHA3-256-body-hashtests die alle vier de rolcombinaties dekken, vangen elke drift op.
Weergavevolgorde voor lezers. De bevestigingsstrings bevatten zinsneden zoals "described above", die veronderstellen dat de beschrijvings- / scope-rijen vóór de bevestigingen worden gerenderd. Lezers MOETEN de terms-array in CBOR-volgorde renderen; herordening breekt de prozasemantiek.
Contact van de tegenpartij. Wanneer de contact van Partij B een geldig e-mailadres is, verzendt de qub-uploadservice automatisch een review- / medeondertekeningsuitnodigingsmail op stage-tijd en bindt de uiteindelijke medeondertekening aan verificatie van datzelfde adres (§9.7). Pacts waarbij het contact van Partij B afwezig is, kunnen nog steeds worden medeondertekend, maar alleen via een out-of-band-kanaal — de service weigert medeondertekeningsverzoeken die geen overeenkomstige 15-minuten-e-mailverificatiemarker kunnen produceren.
6.2 Verdict-body (content_type = 0x04)
Een verdict-body is de canonieke CBOR-codering van een VerdictBody-waarde:
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
}
Canonieke CBOR-sleutelvolgorde:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
De totale geserialiseerde verdict-CBOR MAG NIET groter zijn dan 8 KB (komt overeen met de registerrij hierboven).
Outcome-enum. De wire-byte is intentieneutraal; de vier categorieën Right / Partial / Wrong / Unfalsifiable dekken de uitkomstruimte van elke verdict-dragende intentie. Per-intentielabels ("Goed voorspeld" / "Gehouden" / "Geleverd" / "Bevestigd" voor Right, enz.) zijn een renderzorg aan de lezerskant die wordt opgelost aan de hand van de intentie van de bovenliggende qub — de wire blijft taal- en intentieneutraal. Waarden buiten 1..=4 MOETEN bij decoderen worden geweigerd.
Koppeling met bovenliggende qub. Een verdict-qub draagt de verwijzing naar de bovenliggende qub NIET in zijn body. Het Arweave-transactie-id van de bovenliggende qub wordt bij upload-tijd uitgezonden als de opslagtag Parent-Tx-Id (§7 opslagtaglaag). Dit houdt de body een op zichzelf staande ondertekende verklaring van zelfbeoordeling; de audit-keten ("waarover gelijk?") wordt vastgesteld via de opzoeking op Arweave-tag.
Veiligheid van de bewijslink (normatief). Wanneer evidence_url aanwezig is, MOETEN validators (compose-zijde, wire-zijde, Worker-edge) afdwingen:
- Alleen HTTPS. De string MOET beginnen met de bytesequentie
https://. Elk ander schema —http,ftp,javascript,data,file, enz. — wordt geweigerd. - Lengtelimiet. ≤ 2.048 bytes (praktische browser-URL-limiet).
- NFC + controle op vijandige codepunten. Zelfde regel als
titleenreflection— bidi-override- / nulbreedte- / tag-blok- / BOM- / C0- / C1-codepunten worden geweigerd. De definitie komt overeen met de Rustcrate::handle::contains_hostile_text_codepointen de TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(houd ze gelijklopend). - Geen witruimte, geen ASCII-stuurtekens. Witruimte / DEL / bytes onder
0x20waar dan ook in de URL worden geweigerd — sluit de\n/\t-injectievector die de bidi-regel niet dekt. - Niet-leeg host-segment. Alles tussen
https://en de eerste/,?of#MOET niet leeg zijn.
Geen server-side fetch. De Worker MAG de URL NIET proxyen, ophalen of een preview tonen. Het protocol slaat een string op; rendering gebeurt aan de lezerszijde met rel="nofollow noopener noreferrer" target="_blank" en een zichtbare host die naast de linktekst wordt getoond.
Reflectie. Optionele door de maker geschreven reflectietekst ("wat is er veranderd, wat heb je geleerd"). Zelfde NFC + controle op vijandige codepunten als title. Lege invoer / invoer met alleen witruimte klapt bij constructie samen tot afwezig.
Schemaversie. v1 ondersteunt alleen verdict_version = 0x01. Toekomstige schema-revisies verhogen deze byte en landen samen met een nieuwe protocolversie volgens §12.
7. Verzegelingsprotocol
De volledige verzegelingssequentie. Elke stap is normatief.
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.
Opslagtaglaag (out-of-band). De qub-uploadservice voegt een opzettelijk kleine set opslagtransactietags toe naast de ingepakte payload. Content-Type=application/octet-stream is normatief vereist. De referentie-service voegt daarnaast drie optionele tags toe wanneer de maker ervoor kiest deze zichtbaar te maken: Intent (allowlist-gevalideerde compose-intentie — bijv. quote, reply, commitment), Author (vingerafdruk van de §9.3-publieke sleutel van de maker als 64 tekens kleine letters hex) en Parent-Tx-Id (opslagtransactie-ID van de bovenliggende qub voor antwoordketens, 43 tekens base64url).
De Author-tag is opt-in per qub: de referentie-maker-app voegt deze alleen toe wanneer de gebruiker expliciet publieke toeschrijving inschakelt op het moment van verzegelen. Wanneer de schakelaar uit staat — de standaard — wordt er geen Author-tag geschreven en is de qub niet toegeschreven op de chain: niets in de permanente opslag koppelt de upload aan de handle, e-mail of andere qubs van een maker. Wanneer de schakelaar aan staat, lost de Author-vingerafdruk op naar de door de maker gekozen @handle via de §9.5-attestatieketen. Relaties in antwoordketens en Intent zijn niet-identificerend. De buitenste wrapper (§13) beschermt de inner body tegen ciphertext-correlatie — dit voorkomt dat een verzamelaar qub-vormige uploads herkent en in bulk ontsleutelt nadat hun drand-ronde is gepubliceerd.
De referentie-service voegt opzettelijk GEEN App-Name-, App-Version- of Type-tags toe: zo'n filter met één waarde zou het hele qub-corpus teruggeven bij een GraphQL-query, wat onverenigbaar is met de body-only-vertrouwelijkheidsreikwijdte van de wrapper.
Een conforme verifier MAG NIET afhankelijk zijn van een opslagtag voor §11 derde-partijverificatie; de body-hash / qub_id / handtekening leggen zich alleen vast op de inner CBOR, nooit op de tagset.
8. Ontgrendelingsprotocol
De volledige ontgrendelingssequentie. Elke stap is normatief.
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. Auteurschapsondertekening
9.1 Rationale
qubs worden in permanente opslag bewaard. Auteurschapshandtekeningen moeten onvervalsbaar blijven voor onbepaalde tijd, en daarom gebruikt v1.0 het post-quantum-schema ML-DSA-65 (FIPS 204) in plaats van een klassiek schema waarvan de beveiliging binnen de permanente levensduur van de qub kan afnemen.
9.2 Algoritmeregister
sig_alg |
Schema | Sleutelgrootte | Handtekeninggrootte |
|---|---|---|---|
0x00 |
Geen handtekening (niet ondertekend) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1.952 bytes | 3.309 bytes |
Lezers MOETEN onbekende sig_alg-waarden weigeren.
9.3 Constructie van het ondertekende preimage
Er hebben twee preimage-versies bestaan. Alle handtekeningen MOETEN V2 gebruiken, en verifiers MOETEN alleen V2 accepteren. Het verouderde V1-preimage (hieronder gedocumenteerd ter historische referentie) werd tijdens de V2-migratie geaccepteerd als alleen-verificatie-fallback; die fallback is uitgefaseerd en een handtekening die alleen tegen V1 verifieert wordt nu geweigerd.
V2 (huidig — geproduceerd door alle nieuwe auteursondertekening, en door beide handtekeningen van de pact-staging-/medeondertekeningsflow):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V2" || // 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.x
sender_label_hash || // [u8; 32]: SHA3-256(NFC(sender_label)),
// or 32 zero bytes when absent
reply_to_or_zero // [u8; 32]: parent qub_id, or 32 zero
// bytes when absent
)
// Total preimage: 155 bytes → 32-byte hash
signature = Sign(author_secret_key, sig_input)
sender_label_hash volgt dezelfde afwezigheids-sentinelconventie als title_hash (§4.2.1): 32 nulbytes zijn geen geldige SHA3-256-uitvoer, dus 'afwezig' kan nooit botsen met een aanwezig label. Alle velden hebben een vaste breedte, dus het preimage is ondubbelzinnig zonder lengteprefixen.
V1 (verouderd — UITGEFASEERD; wordt niet meer geproduceerd en niet meer geaccepteerd bij verificatie):
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
Het V1-preimage liet sender_label en reply_to weg. Het werd tijdens de migratie naar V2 geaccepteerd als alleen-verificatie-fallback; die fallback is sindsdien uitgefaseerd — verifiers MOETEN alleen het V2-preimage accepteren. De definitie wordt hier bewaard ter historische referentie en om de domeinseparator hieronder toe te lichten. Een handtekening die alleen tegen V1 verifieert MOET worden behandeld als een verificatiefout.
Domeinseparatoren: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" bestaan elk uit 17 ASCII-bytes ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Geen padding. De verschillende separator scheidt de twee constructies domeinmatig, zodat een handtekening over het ene preimage nooit als het andere kan verifiëren.
org_id_present-byte: de byte na unlock_at MOET 0x00 zijn. De referentie-implementatie stelt deze beschikbaar als de constante ORG_ID_PRESENT_INDIVIDUAL = 0x00 in crates/qub-core/src/signing.rs; lezers die sig_input reconstrueren voor verificatie MOETEN dezelfde byte uitzenden.
Handtekeningreikwijdte — wat wel en niet wordt gedekt. Het V2-sig_input legt zich direct vast op version, qub_id, body_hash, unlock_at, sender_label en reply_to (plus de vaste domeinseparator en org_id_present-byte). qub_id wordt zelf afgeleid uit version, content_type, created_at, unlock_at, outcome_at, drand_round en body_hash via het §4.1-preimage, dus elke wijziging van die velden produceert een andere qub_id en maakt de handtekening transitief ongeldig. Het geauthenticeerde oppervlak is daarom:
| Veld | Geauthenticeerd door handtekening | Hoe |
|---|---|---|
version |
✓ | Directe input voor sig_input |
qub_id |
✓ | Directe input |
body_hash |
✓ | Directe input |
unlock_at |
✓ | Directe input |
content_type |
✓ | Transitief, via qub_id-preimage |
created_at |
✓ | Transitief, via qub_id-preimage |
outcome_at |
✓ | Transitief, via qub_id-preimage |
drand_round |
✓ | Transitief, via qub_id-preimage (V1.2) |
body |
✓ | Transitief, via body_hash = SHA3-256(body) |
author_pubkey |
— (impliciet) | Sleutel die de handtekening heeft geverifieerd is per definitie de auteur |
sender_label |
✓ | Directe input via sender_label_hash (V2-preimage — de enige geaccepteerde vorm) |
reply_to |
✓ | Directe input via reply_to_or_zero (V2-preimage — de enige geaccepteerde vorm) |
cosigner_pubkey / cosigner_signature |
— | Onafhankelijk ondertekend over dezelfde sig_input (zie §9.7) |
drand_round, drand_chain_id, tlock_ciphertext, visibility |
— | Buitenste SealedQub-velden, niet binnen de envelope — gedekt door hun eigen structurele invarianten (ronde- / chain-consistentie) maar niet door de auteurshandtekening |
Waarom V2 het enige geaccepteerde preimage is.
- Onder het uitgefaseerde V1-preimage kon een partij met schrijftoegang tot de opgeslagen bytes
sender_labelverwisselen ("Alice" → "Mallory") ofreply_toherouderen — en na de ronde opnieuw versleutelen — zonder de auteurshandtekening ongeldig te maken, omdat geen van beide velden in het ondertekende preimage zat. V2 dekt beide, dus elke wijziging van een van beide velden zet verificatie op "mislukt". Omdat verifiers nu alleen V2 accepteren, is deze verwisseling voor elke handtekening gesloten: een handtekening die geen van beide velden bindt (d.w.z. alleen tegen V1 verifieert) wordt regelrecht geweigerd in plaats van dat er naar wordt teruggevallen. - De
author_pubkeybinnen de envelope blijft het echte identiteitsanker — lezers MOETEN de weergegeven identiteit afleiden vanauthor_pubkey(via de §9.5-attestatielaag) in plaats van te vertrouwen opsender_label.
Implementaties die sender_label of reply_to aan eindgebruikers tonen MOETEN de geauthenticeerde identiteit (pubkey-vingerafdruk, attestatie) als het primaire identiteitssignaal naar voren brengen, niet het label.
9.4 Verificatieprocedure
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 (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
only accepted form — the legacy V1 fallback is retired (§9.3), so a
signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
Handtekeningverificatie is de duurste bewerking (vooral ML-DSA-65). Hij ZOU MOETEN worden uitgevoerd nadat alle goedkopere checks (hash, qub_id, unlock_at) zijn geslaagd.
9.5 Identiteitsattestaties
Identiteitsattestaties — de afbeelding van author_pubkey op door mensen herkenbare identiteitsclaims zoals een qub-handle, e-mailadres, socialmedia-handle of passkey-credential — zijn een progressieve verbetering aan de kant van de lezer en zijn niet vereist voor handtekeningverificatie. Lezers die attestaties oplossen naar een weergegeven identiteit MOETEN de volgende voorrang toepassen:
handle > email > social > fingerprint
De vingerafdruk-fallback is de hex in kleine letters van SHA3-256(author_pubkey); deze is altijd beschikbaar voor elke ondertekende qub. Lezers MOGEN deze afkorten voor weergave — de referentielezer rendert qub: gevolgd door de eerste en laatste vier bytes (qub:<8 hex>…<8 hex>).
Een conforme verifier kan elke controle in §9.4 voltooien zonder contact op te nemen met de qub-API, zonder enig netwerk buiten de permanente opslag en drand, en zonder enige serverside-lookup. Attestatieoplossing is een aparte best-effort-stap die alleen wordt uitgevoerd nadat handtekeningverificatie is geslaagd.
9.6 Grootte-impact
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Handtekening | 64 bytes | 3.309 bytes |
| Publieke sleutel | 32 bytes | 1.952 bytes |
| Totaal per qub | 96 bytes | 5.261 bytes |
| Opslagkostenverschil (bij ~$5/MB) | ~$0,0005 | ~$0,026 |
Voor een tekst-qub van 500–2.000 bytes verdrievoudigt ML-DSA-65 ruwweg de opgeslagen grootte. De absolute kosten zijn verwaarloosbaar.
9.7 Medeondertekenaarsverificatie (pact-bilaterale overeenkomsten)
Voor bilaterale overeenkomsten (content_type = 0x03) bewijst een tweede handtekeninglaag dat beide partijen hebben ingestemd met dezelfde voorwaarden.
Envelope-velden:
cosigner_pubkey: ML-DSA-65-publieke sleutel van de medeondertekenaar (Partij B).cosigner_signature: Handtekening over dezelfdesig_inputals die van de auteur (§9.3).
Beide velden MOETEN samen aanwezig zijn of beide afwezig. Als er precies één aanwezig is, MOETEN lezers een integriteitsfout melden.
Verificatieprocedure:
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 (V2 only — the
legacy V1 fallback is retired; all pact clients produce V2 signatures).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."
Eigenschappen:
- De medeondertekenaar ondertekent dezelfde
sig_inputals de auteur — beide partijen leggen zich vast op dezelfdequb_id,body_hashenunlock_at(en, onder V2, dezelfdesender_label_hashenreply_to_or_zero). - Om de medeondertekenaar het V2-preimage te laten reconstrueren zonder toegang tot de ruwe envelope-bytes, dwingt de staging-service op stage-tijd af dat de
sender_labelvan een pact-envelope gelijk is aanpact_terms.party_a.labelen datreply_toafwezig is. Beide gelden voor elk referentieclient-pact; envelopes die dit schenden worden bij het stagen geweigerd. - De
qub_id-afleiding (§4.1) bevat GEEN medeondertekenaarsvelden. Een medeondertekenaar toevoegen aan een bestaande envelope verandert dequb_idniet. - Een pact kan alleen door de auteur ondertekend zijn (eenzijdige toezegging), alleen door de medeondertekenaar (ongebruikelijk), of door beiden (volledige bilaterale bewijs).
E-mailbindingspoort (operationeel). Wanneer een stage-pact een e-mailcontact van Partij B bevat (§6.1), MOET de qub-uploadservice het medeondertekeningsverzoek weigeren tenzij er een kortlevende e-mailverificatiemarker bestaat die overeenkomt met zowel het staging-id als de hash van het genormaliseerde e-mailadres van dat contact. De marker wordt geschreven door /api/v1/auth/verify wanneer het magische-link-token een staging_id draagt en het geverifieerde adres overeenkomt met SHA-256(normalise_email(party_b.contact)) — waarbij normalise_email(addr) de hoofdletter-gevoeligheid van het lokale deel behoudt en alleen het domeingedeelte naar kleine letters omzet (volgens RFC 5321 §2.3.11), en SHA-256 hier de NIST FIPS 180-4-hash is (onderscheiden van de SHA3-256 die wordt gebruikt in de §4-afleidingen) — en verloopt 900 seconden (15 minuten) na uitgifte. Dit is een operationele anti-impersonatiepoort, GEEN onderdeel van het on-chain qub-bewijs — een derde-partij-verifier die §11 herhaalt heeft alleen de permanente opslag en drand nodig, zonder enige serverside-lookup. De marker bestaat alleen aan de server-zijde en is nooit onderdeel van de ondertekende body.
Grootte-impact (ML-DSA-65 auteur + medeondertekenaar):
| Component | Grootte |
|---|---|
| Auteurshandtekening | 3.309 bytes |
| Publieke sleutel auteur | 1.952 bytes |
| Medeondertekenaarshandtekening | 3.309 bytes |
| Publieke sleutel medeondertekenaar | 1.952 bytes |
| Totale crypto-overhead | 10.522 bytes |
| Opslagkostenverschil | ~$0,05 |
10. Markdown-rendering en -sanering
Deze sectie is beveiligingskritiek. De lezer rendert tekst-qubs (content_type = 0x01) met een beperkte Markdown-subset.
10.1 Toegestane elementen
- Koppen:
#tot en met####(geen#####of######) - Nadruk: vet (
**), cursief (*), doorhalen (~~) - Lijsten: geordend (
1.) en ongeordend (-,*) - Blokcitaten (
>) - Code: inline-spans (```) en omsloten blokken (`````)
- Horizontale lijnen (
---) - Regeleinden (twee afsluitende spaties of een lege regel)
- Alinea's
10.2 Verboden elementen
| Element | Behandeling |
|---|---|
Ruwe HTML (<div>, <script>, enz.) |
Volledig verwijderd. Geen HTML komt erdoor. |
Afbeeldingen () |
Verwijderd. Afbeeldingssyntaxis wordt uit de uitvoer verwijderd. |
Links ([text](url)) |
URL wordt gerenderd als zichtbare platte tekst. Niet automatisch gelinkt. Niet aanklikbaar zonder expliciete gebruikersactie. |
| Gevaarlijke URL-schema's | javascript:, data:, vbscript:, file: — verwijderd. |
| Iframes, embeds, objects | Verwijderd. |
| HTML-entiteiten | Gedecodeerd naar weergavetekens alleen als veilig. |
10.3 Implementatie
Implementaties MOETEN een strikte allowlist-parser gebruiken, geen blocklist. De aanbevolen aanpak:
- Parse Markdown met
pulldown-cmark(of equivalent). - Loop door de AST en verwijder elke node die niet in de allowlist staat (§10.1).
- Voor link-nodes: zend de URL uit als zichtbare tekst, niet als een aanklikbaar
<a>-element. - Converteer de gefilterde AST naar een getypeerde tussenrepresentatie (bijv. een
MarkdownNode-enum met alleen veilige varianten). Ruwe HTML is structureel niet representeerbaar in deze IR. - Render vanuit de getypeerde IR naar de doel-viewlaag (bijv. reactieve viewcomponenten, DOM-nodes). Geen HTML-string-concatenatie of
innerHTMLop enig moment.
Blocklist-aanpakken zijn broos omdat nieuwe Markdown-extensies of parserkwaliteiten ongefilterde elementen kunnen introduceren. De getypeerde-AST-aanpak maakt XSS structureel onmogelijk — er is geen variant die willekeurige HTML kan dragen.
10.4 Grootte- en structuurlimieten
- Maximale gerenderde kopdiepte:
####(H4).#####en dieper worden gerenderd als vetgedrukte tekst. - Geen limiet op het aantal alinea's (body-groottelimieten in §6 zijn de beperking).
- Omsloten codeblokken: geen syntaxismarkering in MVP. Gerenderd als monospaced preformatted tekst.
11. Verificatie door derden
Elke derde kan een openbare qub verifiëren zonder medewerking van qub. De verificatieprocedure:
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.
Wat verificatie bewijst:
| Bewijs | Wat het vaststelt |
|---|---|
| Toezegging | De ciphertext bestond op het opslagbloktijdstempel. |
| Integriteit | De platte-tekst-body komt overeen met de vastgelegde hash en is niet gewijzigd. |
| Timing | De inhoud was onleesbaar tot de drand-ronde, die overeenkomt met de gekozen ontgrendelingstijd (onder voorbehoud van de veiligheidsaannames van tlock en drand). |
Wat verificatie NIET bewijst:
| Niet-bewijs | Waarom |
|---|---|
| Auteurschap | Het sender_label is decoratief. Zonder sig_alg ≥ 0x01 had iedereen deze inhoud kunnen verzegelen. |
| Intentie | De qub bewijst inhoud en timing, niet wat de maker subjectief bedoelde. |
| Pre-event timing | Opslagblokopname kan een paar minuten achterlopen op de daadwerkelijke upload. Het toezeggingstijdstempel is de bloktijd, niet het moment waarop de gebruiker op "verzegelen" drukte. |
12. Versionering
12.1 Protocolversie
Het veld version (u8) in zowel SealedQub als QubEnvelope identificeert de hoofdprotocolversie.
- Lezers MOETEN onbekende hoofdversies weigeren met een duidelijke fout.
- Binnen een bekende hoofdversie MOETEN decoders onbekende mapsleutels weigeren (§3.1) — schema-evolutie gebeurt door een nieuwe
versionte introduceren, niet door sleutels toe te voegen die bestaande decoders zouden overslaan. (Eerdere revisies van deze specificatie stonden het tolereren van onbekende optionele velden toe; die bepaling is ingetrokken — ze maakteencode(decode(x))niet-injectief en opende een vector voor verborgen ondertekende inhoud in pact-payloads.) - Inhoudstypen (
content_type) en handtekeningschema's (sig_alg) zijn versiegebonden: nieuwe waarden mogen alleen worden geïntroduceerd samen met een nieuwe protocolversie of expliciete registerupdate.
12.2 Versiegeschiedenis
| Versie | Waarde | Beschrijving |
|---|---|---|
| v1 | 0x01 |
Openbare tekst-qubs (content_type 0x01), pact-bilaterale overeenkomsten (0x03, schema structured/v1, ML-DSA-65 auteur + medeondertekenaar), tlock, SHA3-256 |
12.3 Voorwaartse compatibiliteit
Een v1-lezer die een QubEnvelope tegenkomt met onbekende CBOR-mapsleutels (sleutels die niet in de canonieke volgorde van §3.2 staan) MOET deze weigeren met een decodeerfout (§3.1). Voorwaartse compatibiliteit steunt op het veld version, niet op sleuteltolerantie: toekomstige toevoegingen — zelfs kleine metadata — worden uitgeleverd onder een nieuwe version-waarde, die een v1-lezer weigert met een duidelijke "nieuwer protocol"-fout in plaats van stilzwijgend inhoud te laten vallen waaraan de handtekeningen zich vastleggen.
Een v1-lezer die sig_alg = 0x01 (ML-DSA-65) tegenkomt maar geen ML-DSA-65-verificatieondersteuning heeft, ZOU de qub-inhoud MOETEN weergeven met de mededeling "handtekening aanwezig maar niet verifieerbaar", niet de qub volledig weigeren. De huidige referentie-implementatie weigert elke sig_alg-waarde anders dan 0x00 en 0x01 omdat het v1-register geen ander geldig algoritme bevat — strikte weigering en zacht-falen zijn observationeel identiek totdat een derde algoritme wordt geregistreerd. Het hierboven beschreven zacht-falen-gedrag wordt belangrijk zodra §9.2 een nieuwe vermelding toelaat, en de referentie-lezer zal op dat moment worden bijgewerkt om zacht te falen.
12.4 Versie van de buitenste wrapper
De OuterWrapper beschreven in §13 draagt een eigen version-byte, onafhankelijk van SealedQub.version en QubEnvelope.version. De twee versieruimtes evolueren afzonderlijk: een toekomstige post-quantum-veilige symmetrische vervanging verhoogt de wrapper-byte zonder de inner protocolversie aan te raken, en een toekomstige toevoeging op de protocollaag (bijv. een nieuw envelope-veld) verhoogt de inner versie zonder de wrapper-byte aan te raken.
OUTER_WRAPPER_VERSION_* |
Waarde | Algoritme | Status |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM met 12-byte nonce, 16-byte authenticatietag, AAD gebonden aan qub_id |
v1-standaard |
| — | 0x02–0xFF |
Gereserveerd | Toekomst |
Lezers MOETEN onbekende wrapper-versies weigeren met een duidelijke fout. Het protocol houdt de wrapper-versieruimte opzettelijk smal totdat een concrete migratie-aanleiding verschijnt (bijv. NIST-richtlijnen die de voorkeur geven aan een andere AEAD); een 0x02-slot wordt toegewezen in dezelfde revisie die het algoritme introduceert.
13. Buitenste versleutelingswrapper
13.1 Rationale
De protocollagen (QubEnvelope → tlock → SealedQub) maken een verzegelde qub tijdvergrendeld: de body is onleesbaar totdat unlock_at en de drand-rondehandtekening zijn gepubliceerd. Na ontgrendeling is de rondehandtekening echter openbaar en is de canonieke CBOR-vorm van SealedQub herkenbaar, zodat een verzamelaar die permanente-opslag-transacties heeft geïndexeerd het hele qub-corpus in bulk zou kunnen ontsleutelen.
De buitenste versleutelingswrapper sluit dat kanaal door een aanvullende symmetrische AEAD-laag tussen te plaatsen tussen de canonieke SealedQubCbor en de bytes die naar de permanente opslag worden geüpload. De 256-bit-sleutel K leeft alleen in het URL-fragment van de bezorglink en op gebruikersapparaten; browsers verzenden URL-fragmenten niet naar servers, dus qub.social, elke opslaggateway en elke CDN ervoor zijn observationeel blind voor K. Elke qub in de permanente opslag is daarom een ondoorzichtige ciphertext waarvan de platte tekst niet kan worden hersteld zonder de URL die de maker ervoor heeft gekozen te delen.
Netto effect:
- Standaard immuniteit voor enumeratie. Ingepakte bytes in de permanente opslag zijn byte-onderscheidbaar van willekeurige ciphertext. Een verzamelaarsstrategie van "GraphQL-query voor qub-vormige uploads, bulk-ontsleutel met openbare drand-handtekeningen" eindigt niet met platte tekst.
- Crypto-shredding-privacypositie. qub.social kan zijn eigen corpus letterlijk niet ontsleutelen. Dagvaardingen bereiken ciphertext, geen platte tekst.
- Twee-traps-vertrouwelijkheidsladder. Standaard = link-gecontroleerde toegang (deze sectie). Ontvanger-versleutelde privé-qubs (een gereserveerde fase 2-functie, nog niet gespecificeerd) bouwen daar bovenop als de tweede trap.
13.2 Lagen
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)
Verzegelen en ontgrendelen op de protocollaag (§7, §8) zijn onveranderd onder de wrappergrens; de wrapper wordt aangebracht op de aanroepplaats van seal() en losgemaakt op de aanroepplaats van unlock().
13.3 OuterWrapper-datastructuur
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
}
Veldinvarianten.
versionMOET gelijk zijn aan0x01voor v1.0-wrapper-bytes.qub_idMOET gelijk zijn aan hetqub_id-veld van de SealedQub die wordt hersteld na uitpakken. De uitpakstap dwingt dit niet rechtstreeks af (de AEAD-AAD-binding maakt byte-niveau-manipulatie onmogelijk), maar de ontgrendelingslaag controleert de relatie transitief: als een maker eenSealedQubCborinpakt waarvan de innerqub_idniet overeenkomt met de wrapper-qub_id, faalt §8 stap 11.nonceMOET 96 bits (12 bytes) zijn, vers gegenereerd door een CSPRNG voor elke wrap-bewerking. Het hergebruiken van een nonce onder dezelfde sleutel maakt AEAD-nonce-hergebruik-aanvallen mogelijk die de platte tekst herstellen; producenten MOETEN (key,nonce)-paren behandelen als eenmalig.ciphertextis de AES-256-GCM-uitvoer: ciphertext-bytes samengevoegd met de 16-byte-authenticatietag.ciphertext.len() == SealedQubCbor.len() + 16exact.
CBOR-codering. Canonieke CBOR volgens §3, met dezelfde sleutelvolgorderegel (gesorteerd op gecodeerde bytelengte oplopend, daarna lexicografisch). De vier sleutels zijn:
| Sleutel | Gecodeerde bytes | Volgorde |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
De eerste byte van de OuterWrapper-CBOR is daarom de map-header met bepaalde lengte voor een map met 4 elementen (0xA4).
13.4 AAD-binding aan qub_id
De wrapper bindt qub_id als AEAD-aanvullende geauthenticeerde data. Dit is de dragende structurele verdediging tegen drie klassen aanvallen:
| Aanval | Verdediging |
|---|---|
Ciphertext verplaatsen onder een ander qub_id-veld in de wrapper |
AAD-mismatch → AEAD-authenticatie faalt |
| Het URL-fragment van qub A mengen met de permanente-opslag-bytes van qub B | AAD-mismatch → AEAD-authenticatie faalt |
Knoeien met het qub_id-veld van de wrapper na upload |
AAD-mismatch → AEAD-authenticatie faalt |
Het meedragen van qub_id in de platte tekst van de wrapper verzwakt de enumeratie-immuniteit niet substantieel — qub_id is zelf een SHA3-256-hash van het §4.1-preimage zonder herstelbare preimage uit de digest, en een enumerator die de wrapper-bytes al heeft verzameld leert niets uit de zichtbare qub_id dat hij niet kon afleiden uit het bestaan van de upload zelf.
13.5 Wrap- en unwrap-algoritmen
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
Inklappen van faalmodi. Foute K, foute nonce, AAD-mismatch en gemanipuleerde ciphertext produceren allemaal dezelfde DECRYPT_FAILED-fout. Dit is een opzettelijke AEAD-eigenschap: het onderscheiden van de faalmodus zou een zijkanaal creëren dat een externe aanvaller kan onderzoeken door misvormde wrappers te sturen en de respons te timen. Referentie-implementaties MOETEN alle AEAD-fouten samenvouwen tot één enkele foutvorm.
13.6 Sleutelmateriaal en distributie
De wrapping-sleutel K is een 256-bit-uniforme willekeurige waarde die per qub wordt gegenereerd door een CSPRNG. De referentie-implementaties betrekken deze uit:
- WASM-maker:
getrandom(WebCrypto onder dewasm_js-backend). - Aanroeper van de serverside-verzegelings-API: diens lokale CSPRNG; de aanroeper levert en behoudt
Kalswrapper_key_b64url. De Worker gebruiktKin het geheugen voor de wrapper, maar MAG deze NIET aanhouden. Hierdoor kan een idempotente herhaalpoging een geredigeerd antwoord herstellen met behulp van de behouden capability van de aanroeper, in plaats van afhankelijk te zijn van een eenmalig servergegenereerd geheim.
Distributie: K MOET worden gecodeerd als URL-veilige base64 (RFC 4648 §5, geen padding) en aan de bezorglink worden toegevoegd als het fragmentcomponent:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
Het fragment wordt nooit verzonden naar enige server door een conforme browser. Herstelkanalen (serverside-geschiedenisindex, opt-in e-mail-automatisch-verzenden) die de volledige bezorglink — inclusief het fragment — bewaren buiten het apparaat van de gebruiker, vormen een expliciete afweging tegen de standaard crypto-shredding-positie en MOETEN worden afhankelijk gemaakt van expliciete toestemming van de gebruiker.
Fragmentverlies. Als een gebruiker het URL-fragment verliest en geen herstelkanaal heeft, is de qub onleesbaar. Dit is de dragende afweging van het ontwerp en MOET worden bekendgemaakt aan de gebruiker op het moment van verzegelen. De MVP versterkt de mededeling op verzegelingstijd met expliciete tekst "bewaar deze URL" en een geverifieerd-e-mail-herstelkanaal voor gebruikers die zich aanmelden.
13.7 Buiten de reikwijdte van deze sectie
- Auteurschapsondertekening (§9) is onveranderd: handtekeningen worden berekend binnen de inner
QubEnvelopeen worden hersteld na unwrap → tlock-ontsleuteling → CBOR-parse. - Ontvanger-versleutelde privé-qubs (een gereserveerde fase 2-functie, nog niet gespecificeerd) componeren bovenop deze wrapper als een tweede vertrouwelijkheidstrap; beide trappen kunnen tegelijkertijd actief zijn.
- Pacts (§6, content_type
0x03) worden exact zoals tekst-qubs ingepakt; de wrapper is byte-blind voor het inner inhoudstype.
13.8 Openbare qubs (weglaten van de wrapper)
De buitenste wrapper is optioneel op de bezorglaag. Een maker mag een qub als openbaar verzegelen, in welk geval de canonieke SealedQubCbor rechtstreeks naar de permanente opslag wordt geschreven, zonder OuterWrapper-laag en zonder sleutel K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
Een openbare qub is tijdvergrendeld maar niet link-gecontroleerd: hij blijft onleesbaar totdat zijn drand-ronde wordt gepubliceerd (de tlock-laag is onveranderd), maar na ontgrendeling kan iedereen die de arweave_tx_id heeft hem ontsleutelen — er is geen URL-fragment vereist, omdat er geen K is. Dit is de doelbewuste afweging voor oppervlakken die de server moet aandrijven: onthullingsmeldingsmails, embeds van derden en rijkere SEO na onthulling hebben allemaal een link nodig die werkt zonder een geheim dat de server nooit bezit (§13.6).
Gevolgen waar een producent rekening mee MOET houden:
- Geen immuniteit voor enumeratie. Openbare qubs zien per ontwerp af van de §13.1-eigenschap immuniteit voor enumeratie. De referentie-uploaddienst stempelt er een
Visibility: public-permanente-opslag-tag op (en alleen daarop) zodat ze opzettelijk vindbaar zijn; privé-qubs dragen zo'n tag niet en behouden hun byte-ononderscheidbaarheid. - Platte-tekst-titel zichtbaar op verzegelingstijd. Het §3.2-veld
titleis platte tekst binnenSealedQubCbor. Onder de wrapper is het verborgen totdat een lezerKaanlevert; zonder de wrapper is het voor iedereen leesbaar in de permanente opslag vanaf het moment van uploaden, vóór ontgrendeling. Conforme maker-apps MOETEN dit bekendmaken op verzegelingstijd. - Detectie is structureel. Een conforme lezer/embed onderscheidt de twee vormen door te parsen: bytes die parsen als
OuterWrappervolgen het uitpakpad-met-K; bytes die parsen als een kaleSealedQubCborworden rechtstreeks geaccepteerd. Er is geen wire-vlag vereist, enqub_idbindt zichtbaarheid niet — dezelfde inhoud is byte-identiek op deSealedQub-laag, of hij nu openbaar of privé is verzegeld.
Privé (ingepakt) blijft de standaard; openbaar is een expliciete keuze van de maker per qub.
14. Testvectoren
14.1 qub_id-afleiding
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
Implementaties MOETEN identieke body_hash- en qub_id-waarden produceren voor deze invoer. Deze testvector ZOU de eerste unit test MOETEN zijn die wordt geschreven. De canonieke waarden hierboven werden berekend door de referentie-implementatie en MOETEN bit-voor-bit overeenkomen. Historische preimage-indelingen (vóór de lancering — geen live qubs hingen hiervan af): de 92-byte-V1.0-qub_id was 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; de 100-byte-V1.1-qub_id (na het invouwen van outcome_at_or_zero) was b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 vouwt drand_round erin en verhoogt de domeinseparator naar QUB_ID_V2.
14.2 Unlock-ronde-mapping
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 Canonieke CBOR-rondreis
Implementaties MOETEN verifiëren dat serialize(parse(serialize(qub))) == serialize(qub) voor alle geldige invoeren. Dit is een eigenschapstest, geen enkele vector.
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 canonieke CBOR-bytes en SHA3-256 body_hash worden berekend door de referentie-implementatie. Implementaties MOETEN byte-identieke CBOR produceren voor deze invoer.
Implementaties MOETEN ook verifiëren dat serialize(parse(serialize(pact))) == serialize(pact) voor alle geldige PactTerms-invoeren (eigenschapstest).
14.5 Cross-language-vectoren voor de buitenste wrapper
De buitenste wrapper (§13) heeft een aparte canonieke fixture op crates/qub-core/tests/vectors/wrapper_v1.json. Elk geval legt een (key, nonce, qub_id, sealed_cbor)-tupel vast als ondoorzichtige hex-invoer en stelt een specifieke expected_wrapper_hex-uitvoer vast. Beide referentie-implementaties consumeren hetzelfde JSON-bestand:
- 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).
De fixture pint momenteel drie gevallen vast:
| Geval | Dekking |
|---|---|
basic-text-public |
Kleinste realistische SealedQub-vorm; geen optionele velden. Stelt de canonieke wrapper-vorm vast voor een v1.0-typische qub. |
with-recipient-pubkey |
SealedQub met recipient_pubkey ingesteld (fase 2-pad). Andere inner CBOR-sleutelset, andere qub_id. |
longer-body |
~4 KiB body — oefent multi-byte-CBOR-lengteprefixen uit binnen zowel de inner envelope als de buitenste ciphertext. |
Implementaties MOETEN byte-identieke expected_wrapper_hex produceren voor de vastgelegde invoeren. Het regenereren van de fixture vereist QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors en is voorbehouden aan opzettelijke formaatwijzigingen.
15. Governance van het cryptografieprofiel (toekomst)
Deze sectie is informatief voor v1 en wordt normatief zodra voor het eerst een tweede algoritme in een van de cryptografische primitieven van qub wordt opgenomen.
15.1 Huidige houding
Protocol v1 bindt precies één algoritme per primitief:
- Handtekening: ML-DSA-65 (
sig_alg = 0x01; publieke sleutel van 1.952 bytes, handtekening van 3.309 bytes) en niet ondertekend (sig_alg = 0x00). Het §9.2-register definieert geen andere waarden; een v1-verifier MOET elkesig_algbuiten{0x00, 0x01}weigeren. Een toekomstige Ed25519-vermelding wordt verwacht (§15.3) maar is niet toegewezen in v1. - Timelock: alleen drand quicknet — het chain-hash, de publieke sleutel, de genesis-tijd en de periode zijn vaste netwerkparameters die worden gedragen door de referentie-
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) enconfig/drand-endpoints.json. - Buitenste wrapper: alleen AES-256-GCM v1 (§13).
Verifiers coderen momenteel sleutel- en handtekeninglengtes per primitief hard. Het wire-formaat legt geen wendbaarheidsoppervlak bloot.
15.2 Beoogde vorm
Wanneer een tweede algoritme in het protocol wordt opgenomen, wordt de verifier geconfigureerd voor een benoemd CryptoProfile (bijv. ExqubV1) dat de exacte set van toegestane waarden per primitief opsomt — sig_algs, drand-chains, wrapper-versies, inhoudstypen. Het profiel wordt vastgesteld op het verificatiemoment, nooit in-band onderhandeld. Elke waarde buiten het actieve profiel wordt geweigerd.
Dit garandeert dat het toevoegen van ML-DSA-87 of het activeren van Ed25519 bestaande verifier-configuraties niet met terugwerkende kracht kan verzwakken: een v1-verifier blijft een v1-verifier, zelfs nadat een v2-profiel is gepubliceerd.
15.3 Triggervoorwaarden
Promoveer §15 naar normatieve status zodra een van de volgende zaken wordt voorgesteld:
- Een tweede
sig_alg-byte (Ed25519-activering, ML-DSA-87, of een nieuwe vermelding in het §9-register). - Een tweede drand-chain in productiegebruik.
- Een tweede versie van de buitenste wrapper.
Tot dan is §15 een placeholder die de migratievorm vastlegt zodat toekomstige PR's tegen een bekend doel landen in plaats van het onderhandelingsoppervlak vanaf nul opnieuw te bediscussiëren.
16. Transparantielogboek en duurzaamheidsniveaus (Ontwerp — beoordeling afgerond)
Status. Deze sectie is een ontwerpspecificatie. De wire-formaten, hashing en het vertrouwensmodel hieronder zijn normatief voor de implementatie, maar er is nog geen transparantielogboekcode uitgebracht. De W5-externe beoordeling is afgerond: §16.15 legt de opgeloste beslissingen vast en de bindende lanceringsbeperkingen die eruit voortkwamen. De implementatie mag onder die beperkingen doorgaan. §16 blijft toekomstgericht in dezelfde zin als §15 — het legt het doel vast zodat de implementatie aankomt tegen een vastgesteld ontwerp in plaats van het vertrouwensmodel opnieuw af te leiden tijdens de codebeoordeling. Het is strikt additief — elke bestaande qub behoudt zijn individuele Arweave-transactie en er is geen wijziging aan het
SealedQub/QubEnvelope-wire-formaat.
16.1 Rationale en duurzaamheidsniveaus
Vandaag rusten zowel de duurzaamheid van een qub als zijn temporele toezegging op één enkele Arweave-transactie per qub (§11). Dat koppelt de verzegelingslatentie aan de Arweave-finaliteit, maakt de upload per qub tot een productkostenplafond (ARWEAVE_DAILY_CEILING), en geeft geen manipulatie-evidente ordening over qubs heen. Het transparantielogboek voegt twee lagen onder en rond dat enkele niveau toe:
| Niveau | Naam | Garantie | Wanneer |
|---|---|---|---|
| T1 | R2-eerst synchrone bevestiging | Duurzaamheidsondergrens — verzegelde bytes worden naar duurzame opslag geschreven voordat de verzegeling terugkeert (< 300 ms p95). |
Elke qub, synchroon (§16.10). |
| T2 | Gebatchte transparantielogboek-inclusie | Universele alleen-toevoegen, manipulatie-evidente toezegging + totale ordening, verankerd aan Arweave. | Elke qub, uitgesteld + gebatcht (§16.5–16.7). |
| T3 | Arweave-permanentie per qub | Een individuele Arweave-transactie voor de qub. | Betaalde upsell, en de Arweave-onbeschikbaarheidsterugval (§16.8). |
T2 maakt Arweave per qub een keuze (T3) in plaats van het enige duurzaamheidspad. ARWEAVE_DAILY_CEILING wordt afgeschaft als productplafond en gedegradeerd tot een circuit breaker op alleen de speciale anker-wallet (§16.7); verzegelingen van gebruikers worden nooit afgewezen omdat ze die overschrijden.
Duurzaamheidseerlijkheid (opgelost — §16.15 Q6). Duurzaamheid gaat niet achteruit: de T1-R2-schrijfactie is synchroon en eenmalig-schrijven, dus een gratis-niveau-qub die geen T3 kocht, is volledig duurzaam op het moment dat de verzegeling terugkeert. Wat grover wordt, is de bewijsbare bovengrens van de toezeggingstijd: voor een gratis qub wordt dit de ankerbloktijd in plaats van een bloktijd van een transactie per qub. Bij laag volume — de realistische vroege-lancering- en daltoestand — is de volledige dagelijkse cadans de typische ondergrens, niet een zeldzaam randgeval. De productframing is daarom een bovengrens met geen toegezegde numerieke latentie — "nu verzegeld en duurzaam; een onafhankelijke openbare tijdstempel wordt toegevoegd bij het volgende logboekanker (doorgaans dagelijks)" — en exact-uur-toezeggingsbewijs is een betaalde T3-eigenschap, bekendgemaakt op het niveau-vergelijkingsoppervlak en in de voorwaarden (§16.11, §16.15 Q6). Elke tijdgrens is alleen een interne SLO, nooit een gemarkete SLA.
16.2 LogLeaf-structuur (twee vastgelegde vormen)
Een logboekvermelding is een LogLeaf, gecodeerd als handgeschreven canoniek CBOR onder het §3.1-profiel (bepaalde lengte, geen tags, geen floats, kortste-vorm-integers, NFC-tekst, optionele velden weggelaten indien afwezig, sleutels geordend op gecodeerde-bytelengte oplopend, daarna bytewise). De §3.1-canonieke parse → her-coderen → vergelijken-bescherming wordt toegepast op het codeerpad vóór het hashen (niet alleen bij het decoderen), zodat twee implementaties het niet oneens kunnen zijn over de blad-bytes door een verschil in integer-breedte of sleutelvolgorde. Alle integers zijn u8 / u64 / i64; alle digests zijn 32-byte byte-strings (bstr[32]). Een opgeslagen Arweave-transactie-id is een ruwe 32-byte SHA-256-digest die als bstr[32] wordt gedragen, nooit een base64url-tekststring (komt overeen met §3.3).
Het blad heeft twee vormen geselecteerd door een kind-byte, omdat de Worker op het standaard uploadpad byte-blind is: POST /api/v1/upload ontvangt alleen qub_id en unlock_at als niet-vertrouwde client-beweringen — body_hash, drand_round, created_at en drand_chain_version zijn allemaal verzegeld binnen de §13-buitenste wrapper, waarvan de Worker de sleutel nooit bezit. Alleen het serverside-verzegelingspad (POST /api/v1/seal) leidt body_hash / drand_round af uit platte tekst. Eén enkele bladvorm die body_hash + drand_round draagt, zou daarom waarden vastleggen die de operator nooit verifieerde voor de meerderheid van echte qubs. De splitsing houdt elke vastgelegde waarde eerlijk:
| Sleutel | Gec. len | Type | Aanwezigheid | Betekenis |
|---|---|---|---|---|
seq |
4 | u64 |
vereist | Globale 0-gebaseerde bladindex; de positie waaraan het inclusiebewijs zich vastlegt. |
kind |
5 | u8 |
vereist | 0x01 geattesteerd (serverside-verzegeling) of 0x02 beweerd (client-verzegeling / byte-blinde upload). |
ref |
4 | bstr[32] |
vereist | Bladreferentie-id. Geattesteerd → ruwe qub_id. Beweerd → de geblindeerde id SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
6 | bstr[32] |
vereist | Inhoudsadres SHA3-256(stored_bytes) — de ene inhoudsbinding die de Worker altijd eerlijk kan berekenen, op beide paden. |
unlock_at |
10 | i64 |
vereist | Gekopieerd (geattesteerd) of beweerd (beweerd); gevalideerd > 0 voordat het in het blad terechtkomt. |
received_at |
12 | i64 |
vereist | Worker-wandklok bij R2-bevestiging. Niet-evidentieel (operator-beweerd; §16.6). Aanwezig voor zelfbeschrijving, nooit een bewijs. Gevalideerd > 0. |
body_hash |
10 | bstr[32] |
alleen kind=0x01 |
Weggelaten bij 0x02 — de Worker mist het onder §13. |
drand_round |
12 | u64 |
alleen kind=0x01 |
Weggelaten bij 0x02. |
Een kind=0x02-blad legt opzettelijk noch body_hash noch drand_round vast: het attesteert de toezegging en ordening van een ondoorzichtige ciphertext op inhoudsadres chash, met de claim qub_id en unlock_at — niet de platte tekst of de ronde ervan. De platte-tekst-/ronde-poten voor een beweerde qub komen uit de bestaande §11-.qub-bundelverificatie, niet uit het logboek (§16.11). drand_chain_version zit niet in het blad (het zit binnen de wrapper op het standaardpad); de chain-granulariteit leeft op het anker (§16.7). Codeerdiscipline: weiger een geheel-nul ref of chash, en weiger een niet-positieve unlock_at / received_at, in lijn met de outcome_at > 0-sentinelbescherming in cbor.rs.
16.2.1 Blindering van privé-qubs
Het logboek mag niet de enumeratieorakel worden die de §13-buitenste wrapper bestaat te voorkomen (§13.1). Voor een privé- (ingepakte) qub legt het asserted-blad de geblindeerde identifier SHA3-256(qub_id ‖ log_blind_secret) vast, waarbij log_blind_secret een serverside-geheim is, en laat het body_hash weg. Een derde kan zo'n blad niet binden aan een specifieke qub_id; de houder van de qub, die de bezorglink heeft en daarom qub_id, kan de blindering herberekenen om zijn eigen inclusie te bevestigen. Een openbare qub (al opsombaar, die al de Visibility: public-Arweave-tag draagt per §13.8) legt de ruwe qub_id vast. Dit is de ene plek waar standalone-verifieerbaarheid opzettelijk wijkt voor een dragende privacy-invariant; de standalone-binding voor privé-qubs is chash (§16.9).
Bewaring van log_blind_secret (opgelost — §16.15 Q4). De blindering beschermt bladonkoppelbaarheid, niet de vertrouwelijkheid van de platte tekst (de §13-wrapper houdt dat onafhankelijk). Bij een compromittering van log_blind_secret herberekent de tegenstander voor elke qub_id die hij al bezit of kan reconstrueren (elke qub waarvan hij de bundel/URL heeft, plus elke laag-entropische of openbare qub_id) de blad-ref in één hash en koppelt deze — dit is directe koppeling van een bekende populatie, geen brute-force over een onbekende ruimte. Classificeer log_blind_secret als een correlatie-/Sybil-graad-geheim in dezelfde bewaringscategorie als andere serverside-geheimen, en roteer alleen voorwaarts (een rotatie blindeert toekomstige bladeren opnieuw; het kan al-verankerde bladeren niet met terugwerkende kracht ontkoppelen).
16.3 Blad- en knooppunt-hashing
RFC 6962 §2.1 domeingescheiden hashing met SHA-256 vervangen door SHA3-256:
leaf_hash = SHA3-256(0x00 || canonical_cbor(LogLeaf))
node_hash(l,r) = SHA3-256(0x01 || l || r)
empty tree = SHA3-256("") // defined but never anchored
De domeinprefix-bytes 0x02 (vermeldingsketen, §16.4) en 0x03 (STH-hash, §16.6) zijn gereserveerd en disjunct van deze. Het zijn enkele bytes en kunnen daarom niet botsen met de bestaande 10-byte ASCII-domeinscheiders (QUB_ID_V2, enz.). De boom is de RFC 6962 links-volle ongebalanceerde boom (elke interne splitsing bij de grootste macht van twee strikt kleiner dan het aantal subboom-bladeren), wat inclusie- en consistentiebewijzen één audit-padalgoritme laat delen. De referentiespecificatie draagt expliciete links/rechts-afleidingspseudocode en pint een niet-macht-van-twee (5-blad) testvector vast zodat het rechterrand-promotiegeval — dat een 4-blad-vector verbergt — wordt uitgeoefend.
16.4 Hash-ketening (intern)
De LogDO onderhoudt een interne vermeldingsketen alleen voor crash-consistentie. Deze wordt nooit gepubliceerd en is nooit verifier-gericht:
entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1] = SHA3-256("QUB_TLOG_GENESIS_V1")
De gepubliceerde alleen-toevoegen-autoriteit is de cumulatieve Merkle-wortel + zijn anker (§16.5–16.6), nooit de ruwe volgorde waarin de operator toevallig bladeren serveert: de keten herberekent voor elke geserveerde volgorde, dus alleen de verankerde wortel pint de canonieke positie vast.
16.5 Cumulatieve Merkle-boom en batchen
Er is één steeds groeiende RFC 6962-boom over alle bladeren in seq-volgorde — niet geïsoleerde bomen per batch. (Een carry-blad-geketende constructie per batch werd afgewezen: het is geen echte prefixrelatie, dus de "consistentiebewijzen" ervan zijn onjuist.) De cumulatieve boom geeft echte RFC 9162-consistentiebewijzen en laat één enkel recent anker inclusie bewijzen voor elke oudere qub.
De LogDO-Durable Object is de enige schrijver (blockConcurrencyWhile, in lijn met QuotaDO / EntitlementDO) — toevoegen aan een gedeeld logboek is read-modify-write op gedeelde toestand en MOET daarom via een DO gaan, nooit via KV. Het cachet de rechterrand-frontier van de boom (O(log n) hashes) zodat het sluiten van een batch O(batch) is. Een batch is de set bladeren die samen verankerd worden; de triggers ervan zijn configureerbaar, niet protocol-bevroren: een tree_size-vooruitgang van ten minste LOG_BATCH_MAX_LEAVES (standaard 4096), of leeftijd die de ankercadans bereikt, of een geforceerde flush wanneer een betaalde T3-verzegeling binnenkomt. root_i is de cumulatieve Merkle Tree Hash over bladeren 0 .. tree_size_i.
16.6 Signed Tree Head via Arweave-anker
De Arweave-ankertransactie is de Signed Tree Head en vervangt een operatorhandtekening voor de boomhoofd zelf: het dagelijkse anker heeft geen qub-sleutel nodig omdat de Arweave-tx-owner de handtekening is. De moat-these houdt stand — het onveranderlijke substraat, niet een qub-gehouden geheim, is dragend voor de verankerde wortel.
Er is precies één warme qub-ondertekeningssleutel in het ontwerp, en die is gepind: de ontvangstsleutel per verzegeling (§16.10). De publieke sleutel ervan wordt vastgelegd in LogProfile (gedistribueerd met de verifier) en kruisondertekend door anchor_owner, zodat een verifier een ontvangst valideert tegen dezelfde gepinde wortel als het anker. Dit is de oplossing van §16.15 Q2 — een ongepinde, operator-roteerbare ontvangstsleutel zou verwerpbaar zijn (de operator zou kunnen ontkennen dat de sleutel van hem was), wat de verantwoordingswaarde van de ontvangst zou tenietdoen tegen de tegenstander op operatorniveau die de ontvangst bestaat af te schrikken. Dus: qub houdt geen ongepinde logboek-ondertekeningssleutel; de ontvangstsleutel is gepind en anchor_owner-kruisondertekend.
De SignedTreeHead is canoniek CBOR (sleutels op gecodeerde lengte): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (voorgaande sth_hash; genesis = 32 nul-bytes), log_id:bstr[32], first_seq:u64, anchored_at:i64. De hash ervan is sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).
Gepinde vertrouwenswortel. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Een conforme verifier MOET anchor_tx.owner == LogProfile.anchor_owner vereisen, waarbij anchor_owner (en de publieke sleutel van de ontvangstsleutel) in qub_core is ingebakken als het LogProfile — naast de quicknet-constanten die al in DrandTimelockProvider::quicknet() zitten — en gedistribueerd met het verifier-binary. De verifier MOET ook de Arweave-tx data → tx_id-binding lokaal verifiëren in plaats van een gateway-/raw/-respons te vertrouwen. Dit sluit het schurken-wallet-equivocatiegat: "verankerd op Arweave" is betekenisloos totdat de verifier welke wallet vastpint.
Rotatie is een §15-governance-uitbreiding, geen hergebruik (opgelost — §16.15 Q3). Het profieloppervlak van §15.2 somt momenteel alleen sig_algs / drand-chains / wrapper-versies / inhoudstypen op, en de triggers van §15.3 noemen geen van deze — LogProfile / anchor_owner zit nog niet in het oppervlak van §15. Rotatiegovernance moet daarom worden gebouwd: §15.3 wordt uitgebreid (hieronder) om de LogProfile-trigger toe te voegen, en een rotatie is een ondertekende LogProfile-bump verzonden in een verifier-update. Een geplande rotatie draagt een uitgaande → inkomende kruishandtekening; een compromittering-gedreven rotatie kan dat niet (de uitgaande sleutel is dan precies niet-vertrouwd/onbeschikbaar) en valt terug op de §15-bestuurde bump, waarbij de prev-anker-fork-controle (hieronder) de schade in de tussentijd begrenst.
Equivocatievenster (eersteklas vertrouwensparameter). Een blad is pas equivocatie-bestendig zodra zijn dekkende anker Arweave-bevestigd is. Het venster is received_at → ankerbevestiging (≤ cadans + Arweave-finaliteit). Daarbinnen zijn de enige garanties de gepinde verzegelingsontvangst (§16.10) en de operationele integriteit van qub. Drie verantwoordingsartefacten maken dit eerlijk in plaats van weggewuifd (het getuigenmodel is de oplossing van §16.15 Q2):
- Gepinde ondertekende verzegelingsontvangst — de SCT-analoog die in de uploadrespons wordt teruggegeven (§16.10), ondertekend door de gepinde,
anchor_owner-kruisondertekende ontvangstsleutel. Een blad dat vóór zijn anker wordt verwijderd, laat het slachtoffer een niet-verwerpbare ontvangst achter om te publiceren, wat het stille-weglatingsgat sluit. - Gepubliceerde monitormethodologie + prev-ketenwandeling — de anker-
prev-keten wordt hoofd→genesis afgelopen; een fork (twee ankers bij éénsizemet verschillenderoot, of een gebrokenprev) is publiceerbaar bewijs van wangedrag. Equivocatiedetectie is een verklaarde operationele toezegging, geen stille aanname. - Dubbele zelf-gepubliceerde hoofden — elk nieuw hoofd
{sth_hash, tree_size}wordt geplaatst op een speciale qub-eigen openbare, alleen-toevoegen GitHub-repository (de dragende manipulatie-evidente zelfpublicatiepoot), met een sociale post als alleen best-effort-bevestiging. Een mislukte plaatsing MOET pagen (niet stil falen).
Eerlijkheidsgrens (bindende beperking). Omdat qub beide plaatsingsoppervlakken beheert, is dit zelf-gepubliceerd, niet onafhankelijk bevestigd. Geen enkel product-, marketing- of juridisch oppervlak mag claimen dat het logboek "onafhankelijk bevestigd" is; de toegestane claim is dat equivocatie detecteerbaar is en een niet-verwerpbare ontvangst achterlaat. Een echte onafhankelijke getuige van een derde wordt uitgesteld tot een toekomstige §15-governance-bump.
received_at is operator-beweerd en geen enkele claim mag erop leunen — het wordt nooit als bewijs of als geschilbevestiging op enig product-/juridisch/API-/bewijsweergave-oppervlak getoond. De Arweave-anker-bloktijd T is de enige vertrouwensloze tijdstempel (een bovengrens op "gelogd door"). Elke monitor-sanity-controle op received_at MOET vergelijken met T, niet met het operator-beheerde anchored_at-STH-veld; zo'n controle is alleen een bescherming tegen een klokfout van een eerlijke operator, niet een verantwoordingscontrole tegen een kwaadwillende operator (§16.15 Q5).
16.7 Ankertransactieformaat en -cadans
De AnchorBundle is het canonieke-CBOR-Arweave-transactielichaam, geschreven via de §16.8-bundler: ver:u8, sth:bstr (canonieke SignedTreeHead-bytes), prev_anchor:bstr (ruwe bytes van het voorgaande anker-tx-id; weggelaten bij genesis), chain_hash:tstr (de geldende drand-chain — quicknet), en de blad-CBOR-stroom van de batch in seq-volgorde zodat het anker zelfvoorzienend is: een monitor leidt root opnieuw af uit het lichaam met nul qub-afhankelijkheid. (Als de bladstroom groot wordt bij hoog volume, mag een toekomstige revisie alleen een bladbereik per referentie vastleggen; genoteerd, niet aangenomen in v1.)
Arweave-tags zijn opzettelijk opsombaar — het logboek is bedoeld om gevonden te worden, anders dan privé-qubs: App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. Tags zijn niet-vertrouwde hints; het CBOR-lichaam is de enige autoriteit.
Cadans: standaard dagelijks, herzien met volume (de grootte-trigger verkort automatisch de effectieve cadans onder belasting); een betaalde T3-verzegeling forceert een anker zodat betalende klanten nooit een dag wachten. De anker-wallet is speciaal en laag-snelheid, gescheiden van de upload-wallet — het MOET zijn eigen JWK zijn (een aparte sleutel, geen logische rol op de upload-wallet) zodat een compromittering van de upload-wallet geen ankers kan vervalsen — met een hard maximaal aantal anker-transacties per dag (de gedegradeerde ARWEAVE_DAILY_CEILING). De bewaringshouding wordt onverbloemd verklaard: een smal-bereikte hot key met een strakke circuit breaker en lage balans, niet "koud" — een wallet die dagelijks automatisch ondertekent, kan niet koud zijn, en de specificatie doet niet alsof dat wel zo is.
16.8 ANS-104-bundler
Een eigen ANS-104-DataItem-encoder en deep-hash-ondertekenaar, ongeveer 300 regels, alleen Web Crypto, nul npm-afhankelijkheden (beide Turbo-SDK's falen de npm ci --ignore-scripts-toeleveringsketenpoort). DataItem-byte-indeling:
signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data
Ondertekening is Arweave deepHash — een recursieve SHA-384-digest (Arweaves wire-vereiste, crypto.subtle.digest("SHA-384")) over ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — daarna RSA-PSS over de deep hash met de wallet-JWK via crypto.subtle; id = base64url(SHA-256(signature)). De SHA-384 hier wordt gequarantaineerd als een alleen-Arweave-wire-primitief, nooit een qub-vertrouwensprimitief (§15 legt de afscheiding vast; qub-vertrouwenshashing is overal SHA3-256).
Eén codepad bedient drie consumenten: betaalde T3-permanentie per qub, de Arweave-onbeschikbaarheidsterugval (zet de DataItem in de wachtrij, geef hoe dan ook de R2-eerst-bevestiging terug — dit sluit de huidige ARWEAVE_UNAVAILABLE-503-doodlopende weg), en het schrijven van de AnchorBundle. Handtekeningschema (opgelost — §16.15 Q8): v1 ondertekent met RSA-PSS (handtekeningtype 1) door het bestaande Arweave-wallet-JWK-mechanisme te hergebruiken (nul nieuwe langlevende sleutelbewaring, in dienst van de "één geheim minder"-these); Ed25519 wordt uitgesteld tot het §15-PQ-migratiepad.
De handgerolde deep hash is de code met het hoogste risico en de laagste natuurlijke dekking in W5, dus de afscherming ervan is niet-onderhandelbaar (§16.15 Q8):
- De cross-language-fixture
tlog_v1.json(Rust + TS, het §14.5-wrapper_v1.json-patroon) dekt deep-hash, DataItem-bytes + id, blad-hashes, een 5-blad-wortel + audit-pad, een STH-hash, een inclusiebewijs en een consistentiebewijs — in zowel de onderteken- als de verifieerrichting (de verifieerrichting is van belang omdat de lokale tx → tx_id-controle van §16.6 de deep hash in elke standalone-verifier trekt, niet alleen in de schrijver). - Een eenmalige interop-rondreis door een referentie-ANS-104-bundler, geconsumeerd als alleen statische testdata — nooit een npm-runtime-afhankelijkheid (de alleen-Web-Crypto-/geen-install-scripts-houding blijft staan).
- Het deep-hash- + RSA-PSS-pad moet rondreizen door dezelfde
crypto.subtle-primitieven die de productie gebruikt, zodat de eigen encoder byte-compatibel is. - Een doorlopende post-bundle-acceptatiemonitor bevestigt dat elke anker-/terugval-DataItem daadwerkelijk Arweave-acceptatie bereikt, met een alarm + circuit breaker — omdat de deep hash ook de Arweave-onbeschikbaarheidsterugvalwachtrij bedient, dus een stille regressie zou die wachtrij vullen met netwerk-afgewezen items tijdens precies de storing die het bestaat te dekken.
16.9 Inclusie- en consistentiebewijzen
Beide zijn RFC 9162, SHA3-256, geserveerd als canoniek CBOR.
InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (het exacte blad-CBOR — de verifier herberekent leaf_hash zelf en vertrouwt nooit een aangeleverde hash), index:u64, size:u64, audit:[bstr[32]], root:bstr[32], anchor:{ txid:bstr[32], batch:u64, sth:bstr[32], log_id:bstr[32], block_height?:u64, anchored_at?:i64 }.
ConsistencyProof — GET /api/v1/log/consistency?first=<size_a>&second=<size_b>: ver:u8, first_size:u64, second_size:u64, first_root:bstr[32], second_root:bstr[32], nodes:[bstr[32]], first_anchor, second_anchor. Eén ondubbelzinnige sleutellijst, gepind door testvector.
Standalone-verificatie (geen qub-server, breidt §11 uit):
1. Parse .qub bundle → SealedQub; recompute qub_id (§4.1).
2. Read leaf.kind.
3a. kind=0x01 (attested):
assert leaf.ref == qub_id
assert leaf.body_hash == SHA3-256(body)
assert leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserted):
assert leaf.ref == SHA3-256(qub_id || blind) // holder supplies blind
OR treat ref as opaque and bind via leaf.chash == SHA3-256(stored_bytes)
4. Recompute leaf_hash = SHA3-256(0x00 || leaf); fold `audit` per RFC 6962
using index/size; require derived root == proof.root.
5. Fetch anchor.txid from any gateway; verify the tx data → tx_id binding
(do not trust a gateway /raw/ response); REQUIRE anchor_tx.owner ==
LogProfile.anchor_owner.
6. Parse AnchorBundle; require committed root == proof.root and size ==
proof.size; read the Arweave block time T.
7. Emit the claim scoped by leaf.kind (§16.11).
Bewijs-serverende opslag MOET coördinaat-gesleuteld zijn (opgelost — §16.15 Q7, blokkerende voorwaarde). Koud-blad-bewijsgeneratie is correctheid-neutraal alleen als het R2-auditmateriaal een persistente Merkle-knooppuntopslag is, gesleuteld op absolute boomcoördinaat (level, index) — geen knooppuntdelta's per batch. Met een coördinaat-gesleutelde opslag is elk (blad i, size N)-audit-pad een set van O(log N) directe R2-GETs met geen herberekening over batchgrenzen heen; met een batch-gesleutelde opslag is dat niet zo, wat het opslag-indelingsgat is dat deze oplossing sluit. De blad-lichamen zijn eveneens inhoudsadresseerbaar op seq. Een W5-testvector MOET een koud blad uit het genesis-tijdperk tegen een veel-latere wortel bewijzen met alleen R2 + Arweave terwijl de LogDO-opslag is gewist, zodat de reclamatie-veiligheidsclaim in §16.13 wordt ondersteund in plaats van beweerd. De O(log N) sequentiële R2-GETs horen alleen op het asynchrone bewijs-eindpunt — nooit op het verzegelings-hete-pad (§16.10) of een cron per tick.
16.10 R2-eerst-bevestigingsordening
De POST /api/v1/upload-sequentie wordt:
- Voorste-helft-poorten (auth, validatie, idempotentie-shard-sleutel) — onveranderd.
- Synchroon
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— de duurzaamheidsondergrens; sluit ook W1's precache-race (voorheen eenctx.waitUntilna de Arweave-indiening). - Synchroon
await LogDO.append(leaf)— één in-colo DO-RPC; de enige schrijver wijstseqtoe, breidt de vermeldingsketen uit en werkt de frontier bij. (RMW op gedeelde toestand → DO, nooit KV.) Deappend-RPC doet alleen dat — hetO(batch)-Merkle-batch-sluitwerk draait buiten deze RPC op het LogDO-alarm, anders piekt de append-p95 bij elkeLOG_BATCH_MAX_LEAVES-ste verzegeling. - Geef de bevestiging nu terug — met de verzegelingsontvangst (ondertekend door de gepinde ontvangstsleutel, §16.6) en
{ tx_id, log_seq, anchor_status: "pending" }. De multi-seconden-Arweave-fan-out is uit het kritieke pad verwijderd. - Eén
ctx.waitUntilzet het uitgestelde werk in de wachtrij: de Arweave-indiening per qub (nu best-effort / betaald; bij falen routeert het naar de bundler-terugvalwachtrij in plaats van de gebruiker een 503 te geven) plus de bestaande voorlopige-meta-schrijfacties. Batch-sluiting en verankering draaien onafhankelijk vanuit het LogDO-alarm en de dagelijkse anker-cron. Geenctx.waitUntilbinnen een lus; de bestaande idempotentie-shard-sleutel blijft behouden.
Latentiebudget (opgelost — §16.15 Q7). Het < 300 ms p95-doel is een gemeten lanceringspoort, geen aanname. Het eerlijke kritieke pad is de voorste-helft-KV-leesacties + één R2-PUT + twee geserialiseerde Durable Objects — de bestaande QuotaDO-verzegelingsquota-debet en de nieuwe LogDO-append — dus het budget moet rekening houden met twee in-colo-DO-rondreizen, niet één. Lever een LogDO-latentiealarm dat dat van QuotaDO spiegelt, en behandel een p95-regressie als een releaseblokker.
16.11 Vertrouwensmodel — de precieze claim, gescoped op bladtype
Voor kind=0x01 (geattesteerd): "Deze inhoud — body die overeenkomt met body_hash, geïdentificeerd door qub_id — werd toegezegd aan qubs alleen-toevoegen-logboek op positie seq en bestond niet later dan Arweave-bloktijd T; deze was cryptografisch onleesbaar tot drand-ronde R = unlock_round(unlock_at)." Dit is het volledige {tlock-rondebinding + Merkle-inclusie + verankerde wortel}-drietal.
Voor kind=0x02 (beweerd, de standaard): "Een ondoorzichtige ciphertext met inhoudsadres chash, met de claim qub_id en unlock_at, werd toegezegd aan het alleen-toevoegen-logboek op positie seq en bestond niet later dan Arweave-bloktijd T." De ronde- en body-poten worden geleverd door de bestaande §11-.qub-bundelverificatie (qub_core::unlock), niet door het logboek; wat het logboek toevoegt boven een kale transactie per qub, is manipulatie-evidente ordening, een vertrouwensloze bovengrens-toezeggingstijd en equivocatiebestendigheid.
Beide claims sluiten uit, per §11: auteurschap zonder sig_alg ≥ 0x01, intentie, en timing onder anker-granulariteit. Geen van beide laat enige claim leunen op received_at.
Claimplafond (bindende lanceringsbeperking — opgelost §16.15 Q1). Voor gratis / standaard (kind=0x02)-qubs is de gescopede kind=0x02-claim hierboven het plafond op wat enig product-, marketing-, voorwaarden- of bewijsweergave-oppervlak mag beweren. Geen enkel oppervlak mag stellen of impliceren dat het logboek de inhoud of de ontgrendelingsronde van een standaard-qub bewijst — het logboek bewijst ordening + een vertrouwensloze bovengrens-toezeggingstijd van een ondoorzichtige ciphertext. Inhoud- en rondebewijs komen uitsluitend uit de bestaande §11-.qub-bundelverificatie, die logboekonafhankelijk is. Dit is een harde lanceringsblokker op de copy, geen stilistische voorkeur; het is de oplossing die het byte-blinde standaardpad eerlijk houdt.
16.12 Versionering en W3-coördinatie
Er is geen SealedQub-wire-bump en daarom geen protocolversie-bump (§12.1): het logboek is een sidecar die zich vastlegt aan bestaande velden en bytes, dus het komt niet in de §12.2-protocolversiegeschiedenis. W3's optionele drand_chain_version blijft onaangeroerd en blijft het enige optionele SealedQub-veld. Het logboek introduceert in plaats daarvan zijn eigen onafhankelijke versieruimtes — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — in lijn met de §12.4-wrapper-versieonafhankelijkheid (de wrapper draagt een versie-byte onafhankelijk van de protocolversie, en de logboekversies volgen dezelfde scheiding).
Bewijslevering wordt standaard opgehaald, met een optionele meelift. Een bewijs kan op verzegelingstijd niet bestaan (het anker is nog niet geschreven), dus de .qub-bundel op verzegelingstijd blijft bewijsvrij. W7's verifier haalt GET …/proof één keer op, of reconstrueert in volledig-offline-modus het bewijs uit de openbare AnchorBundle via een Arweave-query op Log-Id. De .qub-bundel (W7) reserveert een optioneel inclusion_proof-lid — afwezig bij verzegeling, gevuld door een post-anker-herexport voor koude archivering — volgens hetzelfde "optioneel, standaard weggelaten, additief"-patroon als W3's drand_chain_version.
16.13 Bewaring
Bewaartermijnen voor de LogDO-open-staart, het R2-bewijs-serverende substraat, de anker-circuit-breaker-tellers en de bundler-terugvalwachtrij worden gespecificeerd in docs/DATA-RETENTION.md. Principe: de hete opslag per vermelding van het logboek (LogDO) is na verankering reclaimeerbaar; het auditmateriaal ervan — de coördinaat-gesleutelde (level, index)-Merkle-knooppuntopslag + de seq-geadresseerde blad-lichamen (§16.9) + de Arweave-ankers — is permanent. Het reclaimeren van een koud blad uit de DO maakt nooit een uitgegeven bewijs ongeldig, omdat een bewijs zich oplost tegen die permanente R2-knooppuntopslag en het Arweave-anker, niet de DO (en de §16.9-gewiste-DO-testvector bewijst het).
16.14 Testvectoren
W5 levert de cross-language-fixture tlog_v1.json (§16.8) plus uitgewerkte vectoren: een kind=0x01- en een kind=0x02-blad → leaf_hash; de 5-blad-cumulatieve wortel; één inclusiebewijs; één consistentiebewijs; één AnchorBundle; en één DataItem-id. Deze leven naast de §14.5-buitenste-wrapper-vectoren en worden uitgeoefend door zowel de Rust- (qub-core) als de TypeScript- (Worker) implementatie.
16.15 Beoordelingsbeslissingen (W5 — opgelost)
De W5-externe beoordeling (een adversariële ontwerppas + eigenaarsgoedkeuring) is afgerond. Elke beslissing hieronder is vastgesteld en weerspiegeld in de §16-tekst hierboven; de bindende lanceringsbeperkingen worden aan het eind herhaald. De implementatie mag eronder doorgaan.
- Standaardpad-(
kind=0x02)-bladeerlijkheid — OPGELOST. Lever de twee-bladtype-splitsing zoals gespecificeerd:kind=0x02legt nochbody_hashnochdrand_roundvast. Geen*_body_hash-veld op het byte-blinde pad (het zou het meest leesbare valse "geverifieerd"-signaal voor integrators zijn en is een gemak dat §11 al uit de bundel levert). Vereis niet serverside-verzegeling voor logboek-geattesteerde qubs (dat zou platte tekst door de Worker dwingen en de crypto-shredding-moat vernietigen). Elke zelfbeschrijvende kortsluiting hoort in de.qub-bundel / bewijs-envelope als een verifier-herberekend veld, nooit een bladveld. Eigenaar-bevestigd claimplafond: §16.11. - Equivocatie-/weglatingsverantwoording — OPGELOST. De verzegelingsontvangstsleutel is gepind in
LogProfile+anchor_owner-kruisondertekend (sluit de eerdere "geen ondertekeningssleutel"-tegenstrijdigheid; §16.6). Lanceringsgetuigenmodel: gepinde ontvangst + monitormethodologie + prev-ketenwandeling + dubbele zelf-gepubliceerde hoofden (qub-eigen openbare GitHub-repo, sociaal best-effort), gemarket als detecteerbaar + ontvangst-gestaafd, nooit onafhankelijk bevestigd. Een echte getuige van een derde wordt uitgesteld tot een §15-governance-bump. - Gepinde anker-owner-vertrouwenswortel + rotatie — OPGELOST. Neem de
LogProfile-pin aan (§16.6); de verifier controleertanchor_tx.owner == anchor_owneren verifieert de tx data → tx_id-binding lokaal. Rotatiegovernance is een §15-uitbreiding om te bouwen (§15.3-trigger toegevoegd), geen hergebruik; geplande rotaties kruisondertekenen, compromittering-gedreven rotaties vallen terug op de §15-bump waarbij de fork-controle de schade begrenst. - Privé-qub-bladblindering — OPGELOST. Behoud blindering voor privé-qubs (
ref = SHA3-256(qub_id ‖ log_blind_secret)), ruwequb_idvoor openbare qubs (al §16.2.1),chashals de standalone-binding.log_blind_secretis een correlatie-/Sybil-graad-geheim, alleen-voorwaarts-roteren (§16.2.1). received_at— OPGELOST. Behoud het in het blad, vastgelegd maar expliciet niet-evidentieel; nooit getoond als bewijs of geschilbevestiging op enig oppervlak. Elke monitor-sanity-controle vergelijkt met de Arweave-bloktijdT, niet met het operator-beheerdeanchored_at(§16.6).- Gratis-niveau-bewijsbare-timing — OPGELOST (eigenaarsgoedkeuring). Duurzaamheid gaat niet achteruit; alleen de bewijsbare bovengrens-toezeggingstijd wordt grover tot de ankerbloktijd. Gratis-niveau-copy gebruikt geen numerieke SLA ("…toegevoegd bij het volgende logboekanker, doorgaans dagelijks"); exact-uur-bewijs is een betaalde T3-eigenschap, bekendgemaakt op het niveau-vergelijkingsoppervlak + in de voorwaarden (§16.1).
- Cumulatieve boom op Workers — OPGELOST. Eén enkele cumulatieve RFC 9162-boom + frontier-gecachete enkele-schrijver-LogDO (comfortabele speling t.o.v. het ~1k-schrijfacties/sec-DO-plafond; stel Merkle-of-shard-roots-sharding uit tot we er dichtbij komen). Blokkerende voorwaarde: coördinaat-gesleutelde
(level, index)-R2-knooppuntopslag + de gewiste-DO-koud-blad-testvector (§16.9);< 300 msis een gemeten lanceringspoort over twee geserialiseerde DO's (§16.10). - ANS-104-handtekeningschema + deep-hash — OPGELOST. RSA-PSS (sig-type 1, hergebruik van de speciale anker-wallet-JWK); Ed25519 uitgesteld tot het §15-PQ-pad. De handgerolde SHA-384-deep-hash is afgeschermd door de beide-richtingen-cross-impl-fixture, een alleen-statische referentie-bundler-interop-controle, de gedeelde-
crypto.subtle-rondreis en de post-bundle-Arweave-acceptatiemonitor (§16.8).
Bindende lanceringsbeperkingen (mee te nemen in implementatie + product-/juridische beoordeling):
- Claimplafond (Q1/Q6). Geen enkel oppervlak mag zeggen dat het logboek de inhoud of ontgrendelingsronde van een standaard-qub bewijst; de toegestane claim is geordend, manipulatie-evident, met een vertrouwensloze bovengrens-toezeggingstijd. Gratis-niveau-tijdstempelcopy draagt geen numerieke latentie; exact-uur-bewijs is alleen betaalde T3.
- Getuigeneerlijkheid (Q2). Market equivocatie als detecteerbaar + ontvangst-gestaafd, nooit onafhankelijk bevestigd.
- Ontvangst- + anker-sleutels (Q2/Q8). De ontvangstsleutel is gepind + kruisondertekend; de anker-wallet is zijn eigen JWK, onderscheiden van de upload-wallet.
- Deep-hash-poort (Q8). Er wordt geen anker of T3-tx geleverd totdat de beide-richtingen-fixture + interop-controle slagen; de acceptatiemonitor paget bij falen.
- Opslagvoorwaarde (Q7). Coördinaat-gesleutelde knooppuntopslag + gewiste-DO-koud-blad-vector zijn voorwaarden voor de "reclamatie maakt nooit een bewijs ongeldig"-garantie.