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:
- Die Änderung eines beliebigen Felds im QubEnvelope (Body, Zeitstempel, Content-Typ, Version) erzeugt eine andere qub_id.
- Die qub_id wird vor der Verschlüsselung berechnet. Sowohl QubEnvelope als auch SealedQub tragen dieselbe qub_id. Der Empfänger prüft nach der Entschlüsselung, dass sie übereinstimmen.
- qub_id hängt nicht von
sender_label,author_signatureoderauthor_pubkeyab. Dies bedeutet, dass derselbe Inhalt, zur selben Zeit versiegelt, unabhängig davon, wer signiert, dieselbe qub_id ergibt. - Eine Änderung des SealedQub-
title(bei sonst identischen Werten) ändert diequb_idübertitle_hash. Ein Gateway kann daher den im Countdown angezeigten Klartext-Titel nicht austauschen, ohne die qub-Identität zu invalidieren. - Eine Änderung des SealedQub-
outcome_at(bei sonst identischen Werten) ändert diequb_idüber das Preimage. Ein Gateway kann das im Countdown vor der Enthüllung angezeigte Verdict-Datum nicht austauschen, ohne die qub-Identität zu invalidieren. - Eine Änderung von
drand_round(bei sonst identischen Werten) ändert diequb_idüber das Preimage. Ein Gateway kann den Timelock-Ciphertext nicht an eine andere Runde rebinden, ohne die qub-Identität zu invalidieren; in Kombination mit der Stanza-Runden-Prüfung beim Entsperren (§8) ist das angezeigteunlock_atdie Runde, die die Entschlüsselung tatsächlich freigibt.
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:
- Nur HTTPS. Die Zeichenkette MUSS mit der Byte-Sequenz
https://beginnen. Jedes andere Schema —http,ftp,javascript,data,fileusw. — wird abgelehnt. - Längenbegrenzung. ≤ 2.048 Bytes (praktische Browser-URL-Grenze).
- NFC + Prüfung auf feindliche Codepunkte. Dieselbe Regel wie für
titleundreflection— Bidi-Override-, Zero-Width-, Tag-Block-, BOM-, C0- und C1-Codepunkte werden abgelehnt. Die Definition entspricht dem Rust-crate::handle::contains_hostile_text_codepointund dem TS-workers/api/src/utils/unicode.ts::isHostileCodepoint(im Gleichschritt halten). - Keine Leerzeichen, keine ASCII-Steuerzeichen. Leerzeichen / DEL / Bytes unter
0x20an beliebiger Stelle in der URL werden abgelehnt — schließt den\n/\t-Injektionsvektor, den die Bidi-Regel nicht abdeckt. - 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.
- Eine Partei mit Schreibzugriff auf die gespeicherten Bytes könnte
sender_label(„Alice" → „Mallory") tauschen, ohne die Autorensignatur zu invalidieren. Derauthor_pubkeyinnerhalb des Envelopes bleibt der wahre Identitätsanker — Empfänger MÜSSEN die Anzeigeidentität ausauthor_pubkey(über die §9.5-Attestierungsschicht) ableiten, anstattsender_labelzu vertrauen. - Ein
reply_to-Feld kann ebenso nach dem Signieren bearbeitet werden. Daqub_idinhaltsadressiert ist, kann ein Angreiferreply_tonicht auf ein nicht-existentes Ziel zeigen lassen, aber er kann eine Antwort stillschweigend an einen anderen existierenden qub umhängen.
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:
cosigner_pubkey: ML-DSA-65-Public-Key des Mitunterzeichners (Partei B).cosigner_signature: Signatur über dasselbesig_inputwie der Autor (§9.3).
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:
- Der Mitunterzeichner signiert dasselbe
sig_inputwie der Autor — beide Parteien verpflichten sich auf dieselbequb_id,body_hashundunlock_at. - Die Ableitung der
qub_id(§4.1) umfasst KEINE Mitunterzeichner-Felder. Das Hinzufügen eines Mitunterzeichners zu einem bestehenden Envelope ändert diequb_idnicht. - Ein Pakt kann nur vom Autor signiert sein (einseitige Verpflichtung), nur vom Mitunterzeichner (ungewöhnlich) oder von beiden (vollständiger bilateraler Beweis).
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
- Überschriften:
#bis####(kein#####oder######) - Hervorhebung: fett (
**), kursiv (*), durchgestrichen (~~) - Listen: geordnet (
1.) und ungeordnet (-,*) - Blockzitate (
>) - Code: Inline-Spans (```) und eingezäunte Blöcke (`````)
- Horizontale Linien (
---) - Zeilenumbrüche (zwei nachfolgende Leerzeichen oder Leerzeile)
- Absätze
10.2 Verbotene Elemente
| Element | Behandlung |
|---|---|
Rohes HTML (<div>, <script>, etc.) |
Vollständig entfernt. Kein HTML wird durchgereicht. |
Bilder () |
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:
- Markdown mit
pulldown-cmark(oder gleichwertig) parsen. - Den AST durchlaufen und jeden Knoten verwerfen, der nicht in der Allowlist (§10.1) enthalten ist.
- Für Link-Knoten: Die URL als sichtbaren Text ausgeben, nicht als anklickbares
<a>-Element. - 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. - Aus der typisierten IR auf die Zielsicht-Ebene rendern (z. B. reaktive View-Komponenten, DOM-Knoten). Keine HTML-Stringverkettung oder
innerHTMLan 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
- Maximale gerenderte Überschriftentiefe:
####(H4).#####und tiefer werden als Fettschrift gerendert. - Keine Begrenzung der Absatzanzahl (die Body-Größenlimits in §6 sind die Beschränkung).
- Eingezäunte Codeblöcke: Keine Syntaxhervorhebung im MVP. Gerendert als monospace-vorformatierter Text.
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_alg ≥ 0x01 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.
- Empfänger MÜSSEN unbekannte Hauptversionen mit einem klaren Fehler ablehnen.
- Bekannte Hauptversionen DÜRFEN unbekannte optionale Felder tolerieren, wenn die Vorwärtskompatibilitätsregeln dies erlauben (optionale Felder, die nicht in der kanonischen Schlüsselreihenfolge enthalten sind, werden ignoriert).
- Inhaltstypen (
content_type) und Signaturverfahren (sig_alg) sind versionsgebunden: Neue Werte dürfen nur zusammen mit einer neuen Protokollversion oder einem expliziten Registry-Update eingeführt werden.
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 |
| — | 0x02–0xFF |
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:
- Aufzählungsimmunität standardmäßig. Gewrappte Bytes im dauerhaften Speicher sind byteweise nicht von beliebigem Ciphertext zu unterscheiden. Eine Harvester-Strategie, „qub-förmige Uploads per GraphQL abzufragen und mit öffentlichen drand-Signaturen massenhaft zu entschlüsseln", endet nicht in Klartext.
- Crypto-Shredding-Datenschutzhaltung. qub.social kann seinen eigenen Korpus buchstäblich nicht entschlüsseln. Vorladungen erreichen Ciphertext, nicht Klartext.
- Zweistufige Vertraulichkeitsleiter. Standard = linkgesteuerter Zugriff (dieser Abschnitt). Empfängerverschlüsselte private qubs (eine reservierte Phase-2-Funktion, noch nicht spezifiziert) bilden als zweite Stufe darauf.
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.
versionMUSS für v1.0-Wrapper-Bytes gleich0x01sein.qub_idMUSS dem Feldqub_iddes nach dem Unwrap wiederhergestellten SealedQubs entsprechen. Der Unwrap-Schritt erzwingt dies nicht direkt (die AEAD-AAD-Bindung macht byte-Level-Manipulation unmöglich), aber die Entsperrebene prüft die Beziehung transitiv: Wenn ein Ersteller einSealedQubCboreinwickelt, dessen internequb_idnicht mit der Wrapper-qub_idübereinstimmt, schlägt §8 Schritt 11 fehl.nonceMUSS 96 Bit (12 Bytes) lang sein, frisch von einem CSPRNG für jeden Wrap-Vorgang generiert. Die Wiederverwendung einer Nonce unter demselben Schlüssel ermöglicht AEAD-Nonce-Reuse-Angriffe, die den Klartext wiederherstellen; Erzeuger MÜSSEN (key,nonce)-Paare als Einmalverwendung behandeln.ciphertextist die AES-256-GCM-Ausgabe: Ciphertext-Bytes konkateniert mit dem 16-Byte-Authentifizierungs-Tag. Genauciphertext.len() == SealedQubCbor.len() + 16.
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:
- WASM-Ersteller:
getrandom(WebCrypto unter demwasm_js-Backend). - Serverseitige Versiegelungsroute des Workers:
crypto.getRandomValues.
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
- Die Autorensignatur (§9) bleibt unverändert: Signaturen werden innerhalb des inneren
QubEnvelopeberechnet und nach Unwrap → tlock-Decrypt → CBOR-Parse wiederhergestellt. - Empfängerverschlüsselte private qubs (eine reservierte Phase-2-Funktion, noch nicht spezifiziert) komponieren als zweite Vertraulichkeitsstufe auf diesem Wrapper; beide Stufen können gleichzeitig aktiv sein.
- Pakte (§6, content_type
0x03) werden exakt wie Text-qubs gewrappt; der Wrapper ist gegenüber dem inneren Inhaltstyp byte-blind.
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:
- Keine Aufzählungsimmunität. Öffentliche qubs verzichten konstruktionsbedingt auf die Aufzählungsimmunitäts-Eigenschaft aus §13.1. Der Referenz-Upload-Dienst prägt ihnen (und nur ihnen) einen
Visibility: public-Tag im dauerhaften Speicher auf, sodass sie absichtlich auffindbar sind; private qubs tragen keinen solchen Tag und behalten ihre Byte-Ununterscheidbarkeit. - Klartext-Titel zum Versiegelungszeitpunkt offengelegt. Das
title-Feld aus §3.2 ist Klartext innerhalb vonSealedQubCbor. Unter dem Wrapper ist es verborgen, bis ein EmpfängerKliefert; ohne den Wrapper ist es vom Moment des Uploads an — vor der Entsperrung — im dauerhaften Speicher für jeden lesbar. Konforme Ersteller-Apps MÜSSEN dies zum Versiegelungszeitpunkt offenlegen. - Die Erkennung ist strukturell. Ein konformer Viewer bzw. Embed unterscheidet die beiden Formen durch Parsen: Bytes, die als
OuterWrapperparsen, nehmen den Unwrap-mit-K-Pfad; Bytes, die als blankesSealedQubCborparsen, werden direkt akzeptiert. Es ist kein Wire-Flag erforderlich, undqub_idbindet die Sichtbarkeit nicht — derselbe Inhalt ist auf derSealedQub-Ebene byte-identisch, ob öffentlich oder privat versiegelt.
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:
- 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).
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:
- Signatur: ML-DSA-65 (
sig_alg = 0x01; öffentlicher Schlüssel mit 1.952 Bytes, Signatur mit 3.309 Bytes) und unsigniert (sig_alg = 0x00). Die Registry aus §9.2 definiert keine weiteren Werte; ein v1-Verifizierer MUSS jedensig_alg-Wert außerhalb von{0x00, 0x01}ablehnen. Ein zukünftiger Ed25519-Eintrag wird antizipiert (§15.3), ist aber in v1 nicht zugewiesen. - Timelock: ausschließlich drand quicknet — der Chain-Hash, der öffentliche Schlüssel, der Genesis-Zeitpunkt und die Periode sind feste Netzwerkparameter, die von der Referenzimplementierung
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) undconfig/drand-endpoints.jsongetragen werden. - Äußerer Wrapper: ausschließlich AES-256-GCM v1 (§13).
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:
- Ein zweites
sig_alg-Byte (Ed25519-Aktivierung, ML-DSA-87 oder ein beliebiger neuer Eintrag in der Registry aus §9). - Eine zweite drand-Chain im Produktiveinsatz.
- Eine zweite äußere Wrapper-Version.
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.