qub-Protokollspezifikation

qub ist ein Protokoll für kryptografische temporale Verpflichtungen: ein System, das Worte auf ein zukünftiges Datum versiegelt und beweist, sobald dieses Datum eintritt, exakt was gesagt wurde und wann.

Drei Primitive ermöglichen dies. drand ist ein dezentrales Zufallsbeacon — der Enthüllungszeitpunkt wird durch Physik erzwungen, nicht durch das Wohlwollen einer Partei. Dauerhafter öffentlicher Speicher ist ein manipulationssicherer öffentlicher Speicher — keine Partei kann einen qub bearbeiten oder löschen, sobald er versiegelt wurde. ML-DSA-65 ist eine post-quantensichere digitale Signatur — jeder qub ist an ein Schlüsselpaar gebunden, dessen Geheimnis das Gerät des Autors nie verlässt.

Zusammen erzeugen diese Primitive eine Aussage, die zeitversiegelt, manipulationssicher und zurechenbar ist — eine Quittung, deren Wert mit der Fähigkeit der Welt wächst, die Vergangenheit zu fälschen.

Der Rest dieses Dokuments ist die normative Spezifikation, die für interoperable Implementierungen erforderlich ist.


qub-Protokollspezifikation

Feld Wert
Version 1.0 (Protokollversion 0x01, äußere Wrapper-Version 0x01)
Datum 2026-05-01
Status Entwurf
Geprüft bis 2026-05-01

Dieses Dokument ist die normative Protokollspezifikation für das qub-System für zeitlich versiegelte Verpflichtungen. Es definiert Datenstrukturen, Serialisierungsregeln, Ableitungsformeln und Verifizierungsverfahren, die für interoperable Implementierungen erforderlich sind.

Geltungsbereich: Die Protokollebene ist absichtlich sprachneutral — der qub-Body ist opaker Klartext / Markdown / Pakt-Bytes, und das gebietsschemaabhängige Rendering liegt in der Verantwortung des Empfängers (qub.social-Webanwendung, <qub-embed>-Iframe, MCP-Clients usw.).


1. Notation und Konventionen

Notation Bedeutung
u8, u64, i64 Vorzeichenlose / vorzeichenbehaftete Ganzzahlen der angegebenen Bitbreite
[u8; N] Byte-Array fester Länge von N Bytes
Vec<u8> Byte-Array variabler Länge
Option<T> Wert vom Typ T oder abwesend
String UTF-8-Textzeichenkette, NFC-normalisiert
`
SHA3-256(x) NIST-SHA3-256-Hash der Bytefolge x (FIPS 202)
ceil(x) Aufrundungsfunktion: kleinste ganze Zahl ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Höchstwertiges Byte zuerst

Alle Ganzzahlen in Preimage-Konstruktionen werden als big-endian-Byte-Arrays fester Breite codiert (i64 → 8 Bytes, u8 → 1 Byte), sofern nicht anders angegeben.

Alle Zeitstempel sind Unix-Sekunden in UTC.


2. Datenstrukturen

2.1 ComposeQub (In-Memory-Zustand des Erstellers)

Nicht in CBOR serialisiert. Nicht im dauerhaften Speicher gespeichert. Lokal in der Ersteller-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 (entschlüsselte Nutzlast)

Serialisiert mittels kanonischem CBOR (§3). Verschlüsselt innerhalb des SealedQub. Dies ist die Struktur, die nach der Entschlüsselung die Inhaltsintegrität nachweist.

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
}

Basislinie (unsignierter Text-qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, alle Option-Felder abwesend.

Andere v1-Konfigurationen: content_type = 0x03 (Pakt-Body, siehe §6.1); sig_alg = 0x01 (ML-DSA-65) mit vorhandenen author_signature und author_pubkey (siehe §9.3); cosigner_pubkey und cosigner_signature zusammen vorhanden für mitunterzeichnete Pakte (siehe §9.7); reply_to gesetzt auf den qub_id des übergeordneten qubs für qubs in Antwortketten (siehe §9.3 für die Auswirkungen auf den Signaturumfang).

2.3 SealedQub (kanonisches Wire-Format)

Serialisiert mittels kanonischem CBOR (§3). In den dauerhaften Speicher hochgeladen. Dies ist das On-Chain-Artefakt.

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 (Anwendungszustand des Empfängers)

Nicht in CBOR serialisiert. Lokal im Viewer. Konstruiert nach erfolgreicher Entschlüsselung und Verifizierung.

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 — übernommen aus QubEnvelope.outcome_at / SealedQub.outcome_at; steuert den Verdikt-Beobachtungsblock auf der Enthüllungsseite (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. Kanonisches CBOR-Profil

Jegliche Serialisierung von SealedQub und QubEnvelope MUSS diesem Profil entsprechen. Zwei Implementierungen, denen dieselbe logische Struktur vorliegt, MÜSSEN identische Bytes erzeugen.

3.1 Codierungsregeln

Regel Spezifikation
Standard RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Reihenfolge der Map-Schlüssel Sortiert zuerst nach codierter Bytelänge (kürzer vor länger), anschließend lexikografisch (Byte für Byte bei gleicher Länge)
Ganzzahl-Codierung Kürzeste Form: 0–23 im initialen Byte; 24–255 in 2 Bytes; 256–65535 in 3 Bytes; usw.
Längencodierung Nur definite Längen. Keine Arrays, Maps, Byte-Strings oder Text-Strings unbestimmter Länge (additional info = 31 ist verboten).
Tags Keine CBOR-Tags (Major Type 6 ist verboten).
Gleitkomma Keine Floats (Major Type 7 Werte 0xF9–0xFB sind verboten).
Text-Strings UTF-8-codiert, NFC-normalisiert (Unicode Normalization Form C).
Byte-Strings Rohe Bytes. Keine Base64-Codierung auf der CBOR-Ebene.
Doppelte Schlüssel Mit Fehler ablehnen. Parser DÜRFEN doppelte Map-Schlüssel NICHT stillschweigend akzeptieren.
Einfache Werte Nur true (0xF5), false (0xF4) und null (0xF6) sind zulässig.
Optionale Felder Abwesende optionale Felder werden vollständig aus der CBOR-Map weggelassen (nicht als null codiert). Vorhandene optionale Felder werden in sortierter Schlüsselreihenfolge eingefügt.

3.2 Verifizierte kanonische Schlüsselreihenfolgen

Diese Schlüsselreihenfolgen sind normativ. Implementierungen MÜSSEN die Schlüssel exakt in dieser Reihenfolge ausgeben. Debug-Assertions SOLLTEN die Reihenfolge in Nicht-Release-Builds prüfen.

QubEnvelope (Version 0x01, unsigniert, alle optionalen Felder abwesend):

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

Herleitung der QubEnvelope-Schlüsselreihenfolge: Jeder Schlüssel ist ein CBOR-Text-String. Codierte Länge = 1 Byte Header + Stringlänge (für Strings unter 24 Bytes). Sortieren Sie zuerst nach codierter Gesamtlänge, dann lexikografisch bei gleicher Länge.

SealedQub (Version 0x01, öffentlich, ohne Empfänger):

"title"             (6 encoded bytes)   ← only if present
"qub_id"            (7 encoded bytes)
"version"           (8 encoded bytes)
"unlock_at"         (10 encoded bytes)
"outcome_at"        (11 encoded bytes)  ← only if present (V1.1 verdict mechanic)
"visibility"        (11 encoded bytes)
"drand_round"       (12 encoded bytes)
"drand_chain_id"    (15 encoded bytes)
"recipient_pubkey"  (17 encoded bytes)  ← only if present
"tlock_ciphertext"  (17 encoded bytes)

PactTerms (Pakt-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 (Zeile des terms-Arrays):

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

PartyIdentifier (party_a / party_b-Map):

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

3.3 Byte-Codierungsreferenz

Typ CBOR-Codierung Beispiel
SHA3-256-Hash (32 Bytes) 0x58 0x20 + 32 Bytes body_hash, qub_id
Zeitstempel (i64) Major Type 0 (positiv) oder 1 (negativ), kürzeste Codierung Unix-Sekunden
Version (u8, Wert 1) 0x01 (einzelnes Byte)
Content-Typ (u8, Wert 1) 0x01 (einzelnes Byte)
sig_alg (u8, Wert 0) 0x00 (einzelnes Byte)
ML-DSA-65-Signatur (3.309 Bytes) 0x59 0x0C 0xED + 3.309 Bytes author_signature, cosigner_signature
ML-DSA-65-Public-Key (1.952 Bytes) 0x59 0x07 0xA0 + 1.952 Bytes author_pubkey, cosigner_pubkey

4. Normative Ableitungen

4.1 qub_id

Die qub_id identifiziert einen qub eindeutig und bindet das QubEnvelope an den SealedQub. Sie wird deterministisch aus dem Inhalt des Envelopes abgeleitet.

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

Codierung des Domain-Separators: Die Zeichenkette "QUB_ID_V2" umfasst 9 ASCII-Bytes. Ein einzelnes 0x00-Padding-Byte wird angehängt, um die 10 Bytes für die Ausrichtung zu erreichen. Implementierungen MÜSSEN exakt diese 10 Bytes verwenden: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Codierung von outcome_at: V1.1 hat das Preimage von 92 auf 100 Bytes erweitert, um das optionale outcome_at-Feld in die Bindung einzubeziehen. Ein abwesendes outcome_at wird als 8 Nullbytes codiert; die Protokoll-Validatoren lehnen outcome_at <= 0 überall ab, sodass dieser Sentinel nicht mit einem legitimen Wert kollidieren kann. Siehe §3.2 (Wire-Format) und den im Repository liegenden Plan tasks/verdict-uplift-plan.md für die Verdict-Mechanik, die dieses Feld motiviert.

Codierung von drand_round: V1.2 hat das Preimage von 100 auf 108 Bytes erweitert, um drand_round (die Ziel-drand-Runde, §4.3) in die Bindung einzubeziehen, und den Domain-Separator auf QUB_ID_V2 angehoben. Dies bindet die Timelock-Runde in die qub-Identität ein: Ein Gateway kann den Ciphertext nicht an eine andere (z. B. bereits vergangene) Runde rebinden, als das angezeigte unlock_at impliziert. Das Entsperrverfahren (§8) verifiziert zusätzlich, dass die in der tlock-Ciphertext-Stanza eingebackene Runde mit unlock_round(unlock_at) übereinstimmt, sodass der angezeigte Entsperrzeitpunkt nachweislich die Runde ist, die die Entschlüsselung freigibt.

Eigenschaften:

4.2 body_hash

body_hash = SHA3-256(body)

Wobei body die rohe Vec<u8>-Inhaltsnutzlast ist. Bei Text-qubs ist dies der UTF-8-codierte 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

Wobei title der optionale Klartext-Titel ist, der im Viewer-Countdown vor der Enthüllung angezeigt wird (siehe §3.2). Die NFC-Normalisierung wird zum Hash-Zeitpunkt durchgeführt, sodass der Digest über visuell äquivalente Codepunkt-Sequenzen hinweg stabil ist. Der All-Nullen-Sentinel ist für den Abwesend-Fall reserviert; eine leere Zeichenkette wird an der kanonischen CBOR-Grenze als nicht-kanonische Codierung von „abwesend" abgelehnt (die kanonische Codierung lässt das Feld vollständig weg).

4.3 Zuordnung von Entsperrzeitpunkt zu Runde

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
Parameter Quelle Beispiel
unlock_at Vom Benutzer gewählte Unix-Sekunden 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

Die ceil()-Operation wählt die erste drand-Runde, deren Enthüllungszeitpunkt ≥ unlock_at ist. Dies stellt sicher, dass der qub nicht vor dem gewählten Entsperrzeitpunkt entschlüsselbar wird.

Grenzfall: Wenn (unlock_at - chain_genesis_time) exakt durch chain_period_seconds teilbar ist, ist das Ergebnis genau diese Runde — der qub wird präzise zum Enthüllungszeitpunkt dieser Runde entsperrt.

Validierung: unlock_at MUSS zum Zeitpunkt der Versiegelung in der Zukunft liegen. unlock_at DARF NICHT mehr als 10 Jahre nach created_at liegen (um das Risiko langfristiger drand-Abhängigkeit zu begrenzen; die Benutzeroberfläche SOLLTE bei Entsperrdaten jenseits von 2 Jahren warnen).


5. Newtypes des Wire-Formats

Newtypes des Wire-Formats bieten Compile-Zeit-Sicherheit gegen das Verwechseln von CBOR-Bytes mit JSON, rohem Klartext oder anderen Byte-Codierungen.

Typ Enthält Erzeugt von Konsumiert von
SealedQubCbor Kanonisches CBOR von SealedQub serialize_sealed_qub() Upload in den dauerhaften Speicher, Viewer-Fetch
QubEnvelopeCbor Kanonisches CBOR von QubEnvelope serialize_qub_envelope() tlock-Encrypt-Eingabe, tlock-Decrypt-Ausgabe

5.1 Konstruktionsregeln

// 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 Validierung bei der Konstruktion

from_encoded() SOLLTE prüfen, dass die Eingabe mit einem gültigen CBOR-Map-Header beginnt. Die vollständige strukturelle Validierung erfolgt zum Parse-Zeitpunkt, nicht zum Konstruktionszeitpunkt, um doppeltes Parsen zu vermeiden.


6. Registry der Inhaltstypen

Wert Typ Maximale Body-Größe Anmerkungen
0x00 Reserviert (ungültig) DARF NICHT verwendet werden
0x01 Klartext (UTF-8, eingeschränktes Markdown) 50 KB bezahlt / 10 KB kostenlos Siehe §10 für Rendering-Regeln. Die Aufteilung kostenlos / bezahlt wird vom Upload-Dienst durchgesetzt; die harte Obergrenze auf Protokollebene beträgt 50 KB.
0x02 Reserviert (zukünftig) Für einen zukünftigen Inhaltstyp zugewiesen; nicht gültig in v1. Empfänger MÜSSEN gemäß der nachstehenden Regel ablehnen.
0x03 Pakt (bilaterale Vereinbarung, CBOR-Body) 100 KB Body ist kanonisches CBOR PactTerms (§6.1). Mitunterzeichner-Signierung gemäß §9.7.
0x04 Verdikt (Selbstbenotung der Ersteller:in, CBOR-Body) 8 KB Body ist kanonisches CBOR VerdictBody (§6.2). Wird ausschließlich durch die systemseitige verdict-Intention ausgegeben. Die Eltern-Beziehung steht auf dem Arweave-Tag Parent-Tx-Id, nicht im Body. Siehe verdict-uplift-plan §3.4.

Empfänger MÜSSEN unbekannte Inhaltstypen mit einem klaren, für den Benutzer sichtbaren Fehler ablehnen. Empfänger DÜRFEN unbekannte Typen NICHT als Text zu rendern versuchen.

6.1 Pakt-Body (content_type = 0x03)

Ein Pakt-Body ist die kanonische CBOR-Codierung eines PactTerms-Werts:

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

Die kanonischen CBOR-Schlüsselreihenfolgen für alle drei Maps sind in §3.2 angegeben. Die insgesamt serialisierten Pakt-CBOR-Bytes DÜRFEN 100 KB NICHT überschreiten (entspricht §6).

Schema-Diskriminator. Die erste Zeile in terms für einen structured/v1-Pakt MUSS { key: "pact_schema", value: "structured/v1" } sein. Zeilen ohne diesen Marker sind „custom"-Pakte und erhalten keine strukturierte Validierung oder schemabewusste Darstellung.

Eingefrorene Bestätigungs-Slots. structured/v1-Pakte tragen genau vier Bestätigungszeilen unter diesen Schlüsseln:

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

Der value für jede ist eine von acht eingefrorenen englischen Zeichenketten, ausgewählt durch das Paar (role, kind), wobei role ∈ { seller, buyer, provider, client } und kind ∈ { standard, capacity }. Die Zeichenketten selbst sind normative Protokolldaten — die ML-DSA-65-Signaturen beider Parteien verpflichten sich auf die exakten Bytes über body_hash. Sie werden NICHT lokalisiert; der signierte Body ist sprachneutral. Jede Wortlautänderung erfordert eine neue Schemaversion (structured/v2).

Die acht Zeichenketten, ihre Auflösung (acknowledgement_for(role, kind)) und die Begründung jeder einzelnen sind durch die Referenzimplementierung fixiert. Konforme Implementierungen MÜSSEN bytidentische Bestätigungswerte ausgeben; SHA3-256-Body-Hash-Tests mit Golden-Fixtures, die alle vier Rollenkombinationen abdecken, fangen jegliche Drift ab.

Anzeigereihenfolge im Empfänger. Die Bestätigungszeichenketten enthalten Wendungen wie „described above", die voraussetzen, dass die Beschreibungs- / Umfangszeilen vor den Bestätigungen gerendert werden. Empfänger MÜSSEN das terms-Array in CBOR-Reihenfolge rendern; eine Umordnung bricht die Textsemantik.

Kontakt der Gegenpartei. Wenn der contact von Partei B eine gültige E-Mail-Adresse ist, versendet der qub-Upload-Dienst beim Staging automatisch eine Einladungs-E-Mail zur Prüfung / Mitunterzeichnung und bindet die spätere Mitunterzeichnung an die Verifizierung derselben Adresse (§9.7). Pakte, deren Kontakt der Partei B abwesend ist, können dennoch mitunterzeichnet werden, jedoch nur über einen Out-of-Band-Kanal — der Dienst lehnt Mitunterzeichnungsanfragen ab, die keinen passenden 15-minütigen E-Mail-Verifizierungsmarker erzeugen können.

6.2 Verdikt-Body (content_type = 0x04)

Ein Verdikt-Body ist die kanonische CBOR-Codierung eines VerdictBody-Werts:

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
}

Kanonische CBOR-Schlüsselreihenfolge:

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

Die insgesamt serialisierten Verdikt-CBOR-Bytes DÜRFEN 8 KB NICHT überschreiten (entspricht der obigen Registry-Zeile).

Ausgangs-Enum. Das Wire-Byte ist intentionsneutral; die vier Kategorien Right / Partial / Wrong / Unfalsifiable decken den Ausgangsraum jeder verdiktfähigen Intention ab. Pro-Intention-Bezeichnungen („Richtig getippt" / „Eingehalten" / „Ausgeliefert" / „Bestätigt" für Right usw.) sind eine empfängerseitige Rendering-Angelegenheit, die gegen die Intention des Eltern-qubs aufgelöst wird — die Leitung bleibt sprach- und intentionsneutral. Werte außerhalb von 1..=4 MÜSSEN beim Decodieren abgelehnt werden.

Eltern-Verknüpfung. Ein Verdikt-qub trägt die Eltern-Referenz NICHT in seinem Body. Die Arweave-Transaktions-ID des Eltern-qubs wird beim Upload als Speicher-Tag Parent-Tx-Id ausgegeben (§7 Speicher-Tag-Schicht). Dadurch bleibt der Body eine in sich geschlossene signierte Selbsteinschätzungs-Aussage; die Audit-Kette („wobei recht?") wird über das Arweave-Tag-Lookup hergestellt.

Sicherheit der Beleg-URL (normativ). Wenn evidence_url vorhanden ist, MÜSSEN Validatoren (compose-seitig, wire-seitig, Worker-Edge) Folgendes durchsetzen:

  1. Nur HTTPS. Die Zeichenkette MUSS mit der Byte-Sequenz https:// beginnen. Jedes andere Schema — http, ftp, javascript, data, file usw. — wird abgelehnt.
  2. Längenbegrenzung. ≤ 2.048 Bytes (praktische Browser-URL-Grenze).
  3. NFC + Prüfung auf feindliche Codepunkte. Dieselbe Regel wie für title und reflection — Bidi-Override-, Zero-Width-, Tag-Block-, BOM-, C0- und C1-Codepunkte werden abgelehnt. Die Definition entspricht dem Rust-crate::handle::contains_hostile_text_codepoint und dem TS-workers/api/src/utils/unicode.ts::isHostileCodepoint (im Gleichschritt halten).
  4. Keine Leerzeichen, keine ASCII-Steuerzeichen. Leerzeichen / DEL / Bytes unter 0x20 an beliebiger Stelle in der URL werden abgelehnt — schließt den \n/\t-Injektionsvektor, den die Bidi-Regel nicht abdeckt.
  5. Nicht-leeres Host-Segment. Alles zwischen https:// und dem ersten /, ? oder # MUSS nicht leer sein.

Kein serverseitiges Abrufen. Der Worker DARF die URL NICHT per Proxy abrufen, laden oder voranzeigen. Das Protokoll speichert eine Zeichenkette; das Rendering erfolgt empfängerseitig mit rel="nofollow noopener noreferrer" target="_blank" und einem sichtbaren Host, der neben dem Link-Text angezeigt wird.

Reflexion. Optionaler, von der Ersteller:in verfasster Reflexionstext („was hat sich verändert, was haben Sie gelernt"). Dieselbe NFC- und Prüfung auf feindliche Codepunkte wie bei title. Leere oder nur aus Leerraum bestehende Eingaben werden zur Konstruktionszeit auf abwesend kollabiert.

Schemaversion. v1 unterstützt ausschließlich verdict_version = 0x01. Künftige Schemarevisionen erhöhen dieses Byte und werden zusammen mit einer neuen Protokollversion gemäß §12 eingeführt.


7. Versiegelungsprotokoll

Die vollständige Versiegelungssequenz. Jeder Schritt ist normativ.

 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.

Speicher-Tag-Schicht (out-of-band). Der qub-Upload-Dienst hängt einen bewusst kleinen Satz von Speicher-Transaktions-Tags zusammen mit der gewrappten Nutzlast an. Content-Type=application/octet-stream ist normativ vorgeschrieben. Der Referenzdienst hängt zusätzlich drei optionale Tags an, wenn der Ersteller sie offenlegen möchte: Intent (durch Allowlist validierte Compose-Intention — z. B. quote, reply, commitment), Author (Fingerabdruck des §9.3-Pubkeys des Erstellers als 64-stelliger Kleinbuchstaben-Hex) und Parent-Tx-Id (Speicher-Transaktions-ID des übergeordneten qubs für Antwortketten, 43-stellige base64url).

Der Author-Tag ist opt-in pro qub: Die Referenz-Ersteller-App hängt ihn nur an, wenn der Benutzer die öffentliche Zuschreibung zum Versiegelungszeitpunkt explizit aktiviert. Wenn der Schalter aus ist — der Standard — wird kein Author-Tag geschrieben und der qub bleibt auf der Chain unzugeordnet: nichts im dauerhaften Speicher verknüpft den Upload mit dem Handle, der E-Mail oder anderen qubs eines Erstellers. Wenn der Schalter an ist, löst sich der Author-Fingerabdruck über die §9.5-Attestierungskette zum vom Ersteller gewählten @handle auf. Antwortkettenbeziehungen und Intent sind nicht identifizierend. Der äußere Wrapper (§13) schützt den inneren Body vor Ciphertext-Korrelation — wodurch verhindert wird, dass ein Harvester qub-förmige Uploads erkennt und nach Veröffentlichung ihrer drand-Runde massenhaft entschlüsselt.

Der Referenzdienst hängt absichtlich KEINE Tags App-Name, App-Version oder Type an: Jeder solche Filter mit einzelnem Wert würde den gesamten qub-Korpus an eine GraphQL-Abfrage zurückgeben, was mit dem rein körperbezogenen Vertraulichkeitsumfang des Wrappers unvereinbar ist.

Ein konformer Verifizierer DARF sich für die Drittparteien-Verifizierung gemäß §11 NICHT auf irgendeinen Speicher-Tag verlassen; der Body-Hash / die qub_id / die Signatur verpflichten sich nur auf das innere CBOR, niemals auf den Tag-Satz.


8. Entsperrprotokoll

Die vollständige Entsperrsequenz. Jeder Schritt ist normativ.

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

9.1 Begründung

qubs werden im dauerhaften Speicher gespeichert. Autorensignaturen müssen unbegrenzt unfälschbar bleiben, weshalb v1.0 das post-quantensichere ML-DSA-65-Verfahren (FIPS 204) verwendet anstelle eines klassischen Verfahrens, dessen Sicherheit innerhalb der dauerhaften Lebensdauer des qubs degradieren könnte.

9.2 Algorithmus-Registry

sig_alg Verfahren Schlüsselgröße Signaturgröße
0x00 Keine Signatur (unsigniert)
0x01 ML-DSA-65 (FIPS 204) 1.952 Bytes 3.309 Bytes

Empfänger MÜSSEN unbekannte sig_alg-Werte ablehnen.

9.3 Konstruktion des signierten Preimage

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

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

signature = Sign(author_secret_key, sig_input)

Domain-Separator: "QUB_AUTHOR_SIG_V1" umfasst 17 ASCII-Bytes: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Kein Padding.

Letztes Byte: Das 91. Preimage-Byte MUSS 0x00 sein. Die Referenzimplementierung legt dies als Konstante ORG_ID_PRESENT_INDIVIDUAL = 0x00 in crates/qub-core/src/signing.rs offen; Empfänger, die sig_input zur Verifizierung rekonstruieren, MÜSSEN dasselbe Byte ausgeben.

Signaturumfang — was abgedeckt ist und was nicht. sig_input verpflichtet sich auf vier Envelope-Felder: version, qub_id, body_hash, unlock_at (zuzüglich des festen Domain-Separators und des org_id_present-Bytes). Drei dieser vier sind strukturelle Invarianten: qub_id selbst wird über das §4.1-Preimage aus version, content_type, created_at, unlock_at, outcome_at, drand_round und body_hash abgeleitet, sodass jede Änderung an diesen Feldern eine andere qub_id erzeugt und die Signatur transitiv invalidiert. Die direkt authentifizierte Oberfläche ist somit:

Feld Durch Signatur authentifiziert Wie
version Direkte Eingabe für sig_input
qub_id Direkte Eingabe
body_hash Direkte Eingabe
unlock_at Direkte Eingabe
content_type Transitiv, über das qub_id-Preimage
created_at Transitiv, über das qub_id-Preimage
outcome_at Transitiv, über das qub_id-Preimage
drand_round Transitiv, über das qub_id-Preimage (V1.2)
body Transitiv, über body_hash = SHA3-256(body)
author_pubkey — (implizit) Der Schlüssel, der die Signatur verifiziert hat, ist per Definition der Autor
sender_label Reiner Anzeigetext; veränderbar, ohne die Signatur zu brechen
reply_to Threading-Zeiger; veränderbar, ohne die Signatur zu brechen
cosigner_pubkey / cosigner_signature Unabhängig über dasselbe sig_input signiert (siehe §9.7)
drand_chain_id, tlock_ciphertext, visibility Felder des äußeren SealedQub, außerhalb des Envelopes — abgedeckt durch ihre eigenen strukturellen Invarianten (Konsistenz von Runde / Chain), aber nicht durch die Autorensignatur. (drand_round ist nun transitiv über das qub_id-Preimage gebunden — siehe oben.)

Sicherheitsimplikationen nicht authentifizierter Felder.

Implementierungen, die sender_label oder reply_to Endbenutzern anzeigen, MÜSSEN die authentifizierte Identität (Pubkey-Fingerabdruck, Attestierung) als primäres Identitätssignal hervorheben, nicht das Label.

9.4 Verifizierungsverfahren

1. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (same formula as §9.3).
6. Verify(author_pubkey, sig_input, author_signature).
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."

Die Signaturverifizierung ist die teuerste Operation (insbesondere ML-DSA-65). Sie SOLLTE durchgeführt werden, nachdem alle günstigeren Prüfungen (Hash, qub_id, unlock_at) bestanden wurden.

9.5 Identitäts-Attestierungen

Identitäts-Attestierungen — die Zuordnung von author_pubkey zu menschlich erkennbaren Identitätsansprüchen wie einem qub-Handle, einer E-Mail-Adresse, einem Social-Handle oder einem Passkey-Credential — sind eine Progressive Enhancement auf Empfängerseite und sind für die Signaturverifizierung nicht erforderlich. Empfänger, die Attestierungen zu einer Anzeigeidentität auflösen, MÜSSEN folgende Präzedenz anwenden:

handle > email > social > fingerprint

Das Fingerabdruck-Fallback ist der Kleinbuchstaben-Hex von SHA3-256(author_pubkey); es ist immer für jeden signierten qub verfügbar. Empfänger DÜRFEN es zur Anzeige abkürzen — der Referenz-Empfänger rendert qub: gefolgt von den ersten und letzten vier Bytes (qub:<8 hex>…<8 hex>).

Ein konformer Verifizierer kann jede Prüfung in §9.4 abschließen, ohne die qub-API zu kontaktieren, ohne ein Netzwerk außer dem dauerhaften Speicher und drand und ohne irgendeine serverseitige Suche. Die Attestierungsauflösung ist ein separater Best-Effort-Schritt, der erst nach erfolgreicher Signaturverifizierung durchgeführt wird.

9.6 Größenauswirkung

Ed25519 ML-DSA-65
Signatur 64 Bytes 3.309 Bytes
Public Key 32 Bytes 1.952 Bytes
Insgesamt pro qub 96 Bytes 5.261 Bytes
Speicherkostendelta (bei ~$5/MB) ~$0,0005 ~$0,026

Für einen Text-qub von 500–2.000 Bytes verdreifacht ML-DSA-65 die gespeicherte Größe in etwa. Die absoluten Kosten sind vernachlässigbar.

9.7 Mitunterzeichner-Verifizierung (bilaterale Pakt-Vereinbarungen)

Für bilaterale Vereinbarungen (content_type = 0x03) beweist eine zweite Signaturschicht, dass beide Parteien denselben Bedingungen zugestimmt haben.

Envelope-Felder:

Beide Felder MÜSSEN gemeinsam vorhanden oder gemeinsam abwesend sein. Wenn genau eines vorhanden ist, MÜSSEN Empfänger einen Integritätsfehler melden.

Verifizierungsverfahren:

1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
   Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3.
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."

Eigenschaften:

E-Mail-Bindungs-Gate (operativ). Wenn ein gestagter Pakt einen E-Mail-Kontakt der Partei B trägt (§6.1), MUSS der qub-Upload-Dienst die Mitunterzeichnungsanfrage ablehnen, sofern kein kurzlebiger E-Mail-Verifizierungsmarker existiert, der sowohl die Staging-ID als auch den Hash der normalisierten E-Mail dieses Kontakts erfasst. Der Marker wird von /api/v1/auth/verify geschrieben, wenn das Magic-Link-Token eine staging_id trägt und die verifizierte Adresse mit SHA-256(normalise_email(party_b.contact)) übereinstimmt — wobei normalise_email(addr) die Groß-/Kleinschreibung des Local-Parts beibehält und nur den Domain-Teil in Kleinbuchstaben umwandelt (gemäß RFC 5321 §2.3.11) und SHA-256 hier der NIST-FIPS-180-4-Hash ist (verschieden vom in den §4-Ableitungen verwendeten SHA3-256) — und läuft 900 Sekunden (15 Minuten) nach der Ausstellung ab. Dies ist ein operatives Anti-Imitations-Gate, KEIN Bestandteil des On-Chain-qub-Beweises — ein Drittparteien-Verifizierer, der §11 nachvollzieht, benötigt nur den dauerhaften Speicher und drand, ohne irgendeine serverseitige Suche. Der Marker existiert ausschließlich serverseitig und ist niemals Teil des signierten Bodys.

Größenauswirkung (ML-DSA-65 Autor + Mitunterzeichner):

Komponente Größe
Autorensignatur 3.309 Bytes
Autoren-Public-Key 1.952 Bytes
Mitunterzeichner-Signatur 3.309 Bytes
Mitunterzeichner-Public-Key 1.952 Bytes
Krypto-Gesamtoverhead 10.522 Bytes
Speicherkostendelta ~$0,05

10. Markdown-Rendering und -Sanitisierung

Dieser Abschnitt ist sicherheitskritisch. Der Empfänger rendert Text-qubs (content_type = 0x01) unter Verwendung einer eingeschränkten Markdown-Untermenge.

10.1 Erlaubte Elemente

10.2 Verbotene Elemente

Element Behandlung
Rohes HTML (<div>, <script>, etc.) Vollständig entfernt. Kein HTML wird durchgereicht.
Bilder (![alt](url)) Entfernt. Bildsyntax wird aus der Ausgabe gestrichen.
Links ([text](url)) URL als sichtbarer Klartext gerendert. Nicht automatisch verlinkt. Nicht klickbar ohne explizite Benutzeraktion.
Gefährliche URL-Schemata javascript:, data:, vbscript:, file: — entfernt.
Iframes, Embeds, Objects Entfernt.
HTML-Entities Nur dann zu Anzeigezeichen dekodiert, wenn dies sicher ist.

10.3 Implementierung

Implementierungen MÜSSEN einen strikten Allowlist-Parser verwenden, keine Blocklist. Der empfohlene Ansatz:

  1. Markdown mit pulldown-cmark (oder gleichwertig) parsen.
  2. Den AST durchlaufen und jeden Knoten verwerfen, der nicht in der Allowlist (§10.1) enthalten ist.
  3. Für Link-Knoten: Die URL als sichtbaren Text ausgeben, nicht als anklickbares <a>-Element.
  4. Den gefilterten AST in eine typisierte Zwischendarstellung umwandeln (z. B. ein MarkdownNode-Enum mit ausschließlich sicheren Varianten). Rohes HTML ist in dieser IR strukturell nicht repräsentierbar.
  5. Aus der typisierten IR auf die Zielsicht-Ebene rendern (z. B. reaktive View-Komponenten, DOM-Knoten). Keine HTML-Stringverkettung oder innerHTML an irgendeiner Stelle.

Blocklist-Ansätze sind brüchig, da neue Markdown-Erweiterungen oder Parser-Eigenheiten ungefilterte Elemente einführen können. Der Ansatz mit typisiertem AST macht XSS strukturell unmöglich — es gibt keine Variante, die beliebiges HTML tragen kann.

10.4 Größen- und Strukturgrenzen


11. Drittparteien-Verifizierung

Jede dritte Partei kann einen öffentlichen qub ohne Mitwirkung von qub verifizieren. Das Verifizierungsverfahren:

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.

Was die Verifizierung beweist:

Beweis Was er feststellt
Verpflichtung Der Ciphertext existierte zum Zeitpunkt des Speicher-Blockzeitstempels.
Integrität Der Klartext-Body entspricht dem festgelegten Hash und wurde nicht verändert.
Timing Der Inhalt war unlesbar bis zur drand-Runde, die dem gewählten Entsperrzeitpunkt entspricht (vorbehaltlich der Sicherheitsannahmen von tlock und drand).

Was die Verifizierung NICHT beweist:

Nicht-Beweis Warum
Urheberschaft Das sender_label ist dekorativ. Ohne sig_alg0x01 könnte jeder diesen Inhalt versiegelt haben.
Absicht Der qub beweist Inhalt und Timing, nicht das, was der Ersteller subjektiv meinte.
Vor-Ereignis-Timing Die Speicher-Blockaufnahme kann gegenüber dem tatsächlichen Upload um Minuten verzögert sein. Der Verpflichtungszeitstempel ist die Blockzeit, nicht der Moment, in dem der Benutzer auf „versiegeln" geklickt hat.

12. Versionierung

12.1 Protokollversion

Das Feld version (u8) sowohl in SealedQub als auch in QubEnvelope identifiziert die Hauptversion des Protokolls.

12.2 Versionsverlauf

Version Wert Beschreibung
v1 0x01 Öffentliche Text-qubs (content_type 0x01), bilaterale Pakt-Vereinbarungen (0x03, structured/v1-Schema, ML-DSA-65 Autor + Mitunterzeichner), tlock, SHA3-256

12.3 Vorwärtskompatibilität

Ein v1-Empfänger, der auf ein QubEnvelope mit unbekannten optionalen CBOR-Map-Schlüsseln (Schlüssel, die nicht in der kanonischen Reihenfolge gemäß §3.2 enthalten sind) trifft, SOLLTE diese Schlüssel ignorieren und mit der Verifizierung anhand der bekannten Felder fortfahren. Dies ermöglicht zukünftige kleinere Ergänzungen (z. B. neue Metadaten), ohne ein Hauptversions-Bump zu erfordern.

Ein v1-Empfänger, der auf sig_alg = 0x01 (ML-DSA-65) trifft, jedoch keine ML-DSA-65-Verifizierungsunterstützung besitzt, SOLLTE den qub-Inhalt mit dem Hinweis „Signatur vorhanden, aber nicht verifizierbar" anzeigen, anstatt den qub vollständig abzulehnen. Die Referenzimplementierung lehnt heute jeden sig_alg-Wert außer 0x00 und 0x01 ab, weil die v1-Registry keinen weiteren gültigen Algorithmus enthält — strikte Ablehnung und Soft-Fail sind beobachtbar identisch, bis ein dritter Algorithmus registriert wird. Das oben beschriebene Soft-Fail-Verhalten wird tragend, sobald §9.2 einen neuen Eintrag zulässt, und der Referenz-Empfänger wird zu diesem Zeitpunkt auf Soft-Fail umgestellt.

12.4 Äußere Wrapper-Version

Der in §13 beschriebene OuterWrapper trägt sein eigenes version-Byte, unabhängig von SealedQub.version und QubEnvelope.version. Die beiden Versionsräume entwickeln sich getrennt: Ein zukünftiger post-quantensicherer symmetrischer Ersatz erhöht das Wrapper-Byte, ohne die innere Protokollversion zu berühren, und eine zukünftige Ergänzung auf Protokollebene (z. B. ein neues Envelope-Feld) erhöht die innere Version, ohne das Wrapper-Byte zu berühren.

OUTER_WRAPPER_VERSION_* Wert Algorithmus Status
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM mit 12-Byte-Nonce, 16-Byte-Authentifizierungs-Tag, AAD an qub_id gebunden v1-Standard
0x020xFF Reserviert Zukunft

Empfänger MÜSSEN unbekannte Wrapper-Versionen mit einem klaren Fehler ablehnen. Das Protokoll hält den Wrapper-Versionsraum bewusst eng, bis ein konkreter Migrationstreiber auftritt (z. B. NIST-Empfehlung für ein anderes AEAD); ein 0x02-Slot wird in derselben Revision zugewiesen, die den Algorithmus einführt.


13. Äußerer Verschlüsselungs-Wrapper

13.1 Begründung

Die Protokollschichten (QubEnvelope → tlock → SealedQub) machen einen versiegelten qub zeitversiegelt: Der Body ist bis unlock_at und bis zur Veröffentlichung der drand-Rundensignatur unlesbar. Nach der Entsperrung ist die Rundensignatur jedoch öffentlich, und die kanonische CBOR-Form von SealedQub ist erkennbar, sodass ein Harvester, der Transaktionen im dauerhaften Speicher indexiert hätte, den gesamten qub-Korpus massenhaft entschlüsseln könnte.

Der äußere Verschlüsselungs-Wrapper schließt diesen Kanal, indem er eine zusätzliche symmetrische AEAD-Schicht zwischen das kanonische SealedQubCbor und die in den dauerhaften Speicher hochgeladenen Bytes setzt. Der 256-Bit-Schlüssel K lebt nur im URL-Fragment des Lieferlinks und auf den Geräten der Benutzer; Browser übertragen URL-Fragmente nicht an Server, sodass qub.social, jedes Speicher-Gateway und jedes davorliegende CDN für K beobachtbar blind sind. Jeder qub im dauerhaften Speicher ist daher ein opaker Ciphertext, dessen Klartext ohne die vom Ersteller gewählte Freigabe-URL unwiederherstellbar ist.

Nettoeffekt:

13.2 Schichtung

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)

Versiegelung und Entsperrung auf der Protokollebene (§7, §8) bleiben unterhalb der Wrapper-Grenze unverändert; der Wrapper wird an der Aufrufstelle von seal() angefügt und an der Aufrufstelle von unlock() abgelöst.

13.3 OuterWrapper-Datenstruktur

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
}

Feldinvarianten.

CBOR-Codierung. Kanonisches CBOR gemäß §3, mit derselben Schlüsselsortierungsregel (sortiert nach codierter Bytelänge aufsteigend, dann lexikografisch). Die vier Schlüssel sind:

Schlüssel Codierte Bytes Reihenfolge
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

Das erste Byte des OuterWrapper-CBOR ist daher der Map-Header definiter Länge für eine 4-Eintrags-Map (0xA4).

13.4 AAD-Bindung an qub_id

Der Wrapper bindet qub_id als zusätzliche authentifizierte AEAD-Daten. Dies ist die tragende strukturelle Verteidigung gegen drei Angriffsklassen:

Angriff Verteidigung
Ciphertext unter ein anderes qub_id-Feld im Wrapper verschieben AAD-Mismatch → AEAD-Authentifizierung schlägt fehl
Das URL-Fragment von qub A mit den Bytes von qub B aus dem dauerhaften Speicher mischen AAD-Mismatch → AEAD-Authentifizierung schlägt fehl
Das qub_id-Feld des Wrappers nach dem Upload manipulieren AAD-Mismatch → AEAD-Authentifizierung schlägt fehl

Das Mitführen von qub_id im Wrapper-Klartext schwächt die Aufzählungsimmunität nicht wesentlich — qub_id ist selbst ein SHA3-256-Hash des §4.1-Preimage ohne aus dem Digest wiederherstellbares Preimage, und ein Aufzählender, der die Wrapper-Bytes bereits gesammelt hat, lernt aus der sichtbaren qub_id nichts hinzu, was er nicht bereits aus der Existenz des Uploads selbst ableiten könnte.

13.5 Wrap- und Unwrap-Algorithmen

wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
    require K.len() == 32 and N.len() == 12 and Q.len() == 32
    C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
    // C includes the 16-byte authentication tag at the end
    return canonical_cbor_encode(OuterWrapper{
        version:    0x01,
        qub_id:     Q,
        nonce:      N,
        ciphertext: C,
    })

unwrap_sealed_qub(OuterWrapper bytes W, key K):
    require K.len() == 32
    O := canonical_cbor_decode(W) as OuterWrapper
    require O.version == 0x01           // §12.4
    P := AES_256_GCM_decrypt(
            key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
         )
    // any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
    return P                            // P is the inner SealedQubCbor

Kollaps der Fehlermodi. Falscher K, falsche Nonce, AAD-Mismatch und manipulierter Ciphertext erzeugen alle denselben DECRYPT_FAILED-Fehler. Dies ist eine bewusste AEAD-Eigenschaft: Eine Unterscheidung des Fehlermodus würde einen Seitenkanal schaffen, den ein entfernter Angreifer durch das Senden von missgestalteten Wrappern und das Messen der Antwortzeit sondieren könnte. Referenzimplementierungen MÜSSEN alle AEAD-Fehler zu einer einzigen Fehlerform zusammenfassen.

13.6 Schlüsselmaterial und Verteilung

Der Wrapping-Schlüssel K ist ein 256-Bit-uniformer-Zufallswert, der pro qub von einem CSPRNG generiert wird. Die Referenzimplementierungen beziehen ihn von:

Verteilung: K MUSS als URL-sicheres Base64 (RFC 4648 §5, ohne Padding) codiert und als Fragment-Komponente an die Liefer-URL angehängt werden:

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

Das Fragment wird von einem konformen Browser niemals an irgendeinen Server übertragen. Wiederherstellungskanäle (serverseitiger Verlaufsindex, opt-in E-Mail-Auto-Versand), die den vollständigen Lieferlink — einschließlich des Fragments — über das Gerät des Benutzers hinaus persistieren, sind ein expliziter Trade gegen die standardmäßige Crypto-Shredding-Haltung und MÜSSEN an ausdrückliche Benutzerzustimmung gekoppelt sein.

Verlust des Fragments. Wenn ein Benutzer das URL-Fragment verliert und keinen Wiederherstellungskanal hat, ist der qub unlesbar. Dies ist der tragende Trade-off des Designs und MUSS dem Benutzer zum Versiegelungszeitpunkt offengelegt werden. Das MVP verstärkt die Offenlegung zum Versiegelungszeitpunkt mit explizitem „diese URL speichern"-Text und einem verifizierten E-Mail-Wiederherstellungskanal für Benutzer, die zustimmen.

13.7 Außerhalb des Geltungsbereichs dieses Abschnitts

13.8 Öffentliche qubs (Weglassen des Wrappers)

Der äußere Wrapper ist auf der Lieferebene optional. Eine Ersteller:in kann einen qub als öffentlich versiegeln, in welchem Fall das kanonische SealedQubCbor direkt in den dauerhaften Speicher geschrieben wird, ohne OuterWrapper-Schicht und ohne Schlüssel K:

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

Ein öffentlicher qub ist zeitversiegelt, aber nicht linkgesteuert: Er bleibt unlesbar, bis seine drand-Runde veröffentlicht wird (die tlock-Schicht ist unverändert), aber nach der Entsperrung kann ihn jeder, der die arweave_tx_id hat, entschlüsseln — es ist kein URL-Fragment erforderlich, weil es kein K gibt. Dies ist der bewusste Trade für Oberflächen, die der Server ansteuern muss: Enthüllungs-Benachrichtigungs-E-Mails, Drittanbieter-Embeds und reichhaltigere SEO nach der Enthüllung benötigen allesamt einen Link, der ohne ein Geheimnis funktioniert, das der Server nie hält (§13.6).

Konsequenzen, die ein Erzeuger berücksichtigen MUSS:

Privat (gewrappt) bleibt der Standard; öffentlich ist eine explizite Ersteller-Wahl pro qub.


14. Testvektoren

14.1 qub_id-Ableitung

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

Implementierungen MÜSSEN für diese Eingabe identische body_hash- und qub_id-Werte erzeugen. Dieser Testvektor SOLLTE der erste geschriebene Unit-Test sein. Die oben angegebenen kanonischen Werte wurden von der Referenzimplementierung berechnet und MÜSSEN bitgenau übereinstimmen. Historische Preimage-Layouts (vor dem Launch — kein Live-qub hing von diesen ab): die 92-Byte-V1.0-qub_id lautete 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; die 100-Byte-V1.1-qub_id (nach Einbeziehung von outcome_at_or_zero) lautete b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 bezieht drand_round ein und hebt den Domain-Separator auf QUB_ID_V2 an.

14.2 Zuordnung von Entsperrzeitpunkt zu Runde

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

Calculation:
  (1735689600 - 1595431050) / 30 = 4675285.0
  ceil(4675285.0) = 4675285

drand_round = 4675285

14.3 Kanonisches CBOR-Round-Trip

Implementierungen MÜSSEN überprüfen, dass serialize(parse(serialize(qub))) == serialize(qub) für alle gültigen Eingaben gilt. Dies ist ein Property-Test, kein einzelner Vektor.

14.4 PactTerms-CBOR (content_type 0x03)

Input:
  pact_version = 1
  title        = "Scooter deposit"
  terms        = [
    { key: "Item",    value: "Honda Metropolitan scooter" },
    { key: "Price",   value: "$100" },
    { key: "Deposit", value: "$10" }
  ]
  party_a      = { label: "Alice" }
  party_b      = { label: "Bob", contact: "bob@example.com" }
  notes        = absent

Canonical CBOR key order (PactTerms):
  "notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)

Canonical CBOR key order (PactTerm):
  "key"(4) < "value"(6)

Canonical CBOR key order (PartyIdentifier):
  "label"(6) < "contact"(8)

Die kanonischen CBOR-Bytes und der SHA3-256-body_hash werden von der Referenzimplementierung berechnet. Implementierungen MÜSSEN für diese Eingabe bytidentisches CBOR erzeugen.

Implementierungen MÜSSEN außerdem überprüfen, dass serialize(parse(serialize(pact))) == serialize(pact) für alle gültigen PactTerms-Eingaben gilt (Property-Test).

14.5 Cross-Language-Vektoren des äußeren Wrappers

Der äußere Wrapper (§13) hat eine separate kanonische Fixture unter crates/qub-core/tests/vectors/wrapper_v1.json. Jeder Fall fixiert ein Tupel (key, nonce, qub_id, sealed_cbor) als opake Hex-Eingaben und behauptet eine spezifische expected_wrapper_hex-Ausgabe. Beide Referenzimplementierungen konsumieren dieselbe JSON-Datei:

Die Fixture fixiert derzeit drei Fälle:

Fall Abdeckung
basic-text-public Kleinste realistische SealedQub-Form; keine optionalen Felder. Etabliert die kanonische Wrapper-Form für einen v1.0-typischen qub.
with-recipient-pubkey SealedQub mit gesetztem recipient_pubkey (Pfad Phase 2). Anderer innerer CBOR-Schlüsselsatz, andere qub_id.
longer-body Body mit ~4 KiB — übt Multi-Byte-CBOR-Längenpräfixe sowohl im inneren Envelope als auch im äußeren Ciphertext.

Implementierungen MÜSSEN für die aufgezeichneten Eingaben bytidentisches expected_wrapper_hex erzeugen. Die Neuerzeugung der Fixture erfordert QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors und ist deliberaten Formatänderungen vorbehalten.


15. Governance des Krypto-Profils (Zukünftig)

Dieser Abschnitt ist für v1 informativ und wird normativ, sobald zum ersten Mal ein zweiter Algorithmus in eines der kryptografischen Primitive von qub eintritt.

15.1 Aktuelle Haltung

Protokoll v1 bindet pro Primitiv genau einen Algorithmus:

Verifizierer kodieren Schlüssel- und Signaturlängen derzeit pro Primitiv fest. Das Wire-Format legt keine Agilitätsoberfläche offen.

15.2 Vorgesehene Form

Wenn ein zweiter Algorithmus in das Protokoll eintritt, wird der Verifizierer für ein benanntes CryptoProfile (z. B. ExqubV1) konfiguriert, das den exakten Satz zulässiger Werte pro Primitiv auflistet — sig_algs, drand-Chains, Wrapper-Versionen, Inhaltstypen. Das Profil wird zum Verifizierungszeitpunkt festgelegt, niemals in-band ausgehandelt. Jeder Wert außerhalb des aktiven Profils wird abgelehnt.

Dies garantiert, dass das Hinzufügen von ML-DSA-87 oder die Aktivierung von Ed25519 bestehende Verifizierer-Konfigurationen nicht rückwirkend schwächen kann: Ein v1-Verifizierer bleibt ein v1-Verifizierer, auch nachdem ein v2-Profil veröffentlicht wurde.

15.3 Auslösebedingungen

Stufen Sie §15 in den normativen Status hoch, sobald eines der Folgenden vorgeschlagen wird:

Bis dahin ist §15 ein Platzhalter, der die Migrationsform fixiert, damit zukünftige PRs gegen ein bekanntes Ziel landen, anstatt die Aushandlungsoberfläche von Grund auf neu zu verhandeln.