Spesifikasi Protokol qub

qub adalah protokol untuk komitmen temporal kriptografis: sebuah sistem untuk menyegel kata-kata ke tanggal masa depan dan membuktikan, ketika tanggal tersebut tiba, secara persis apa yang dikatakan dan kapan.

Tiga primitif membuatnya bekerja. drand adalah beacon keacakan terdesentralisasi — tanggal pengungkapan dapat ditegakkan oleh fisika, bukan oleh niat baik pihak mana pun. Penyimpanan publik permanen adalah penyimpanan publik yang tahan-rusak — tidak ada pihak yang dapat menyunting atau menghapus qub setelah disegel. ML-DSA-65 adalah tanda tangan digital pasca-kuantum — setiap qub terikat pada pasangan kunci yang rahasianya tidak pernah meninggalkan perangkat penulis.

Bersama-sama primitif ini menghasilkan pernyataan yang terkunci waktu, terbukti anti-rusak, dan dapat diatribusikan — sebuah resi yang nilainya tumbuh seiring meningkatnya kemampuan dunia untuk memalsukan masa lalu.

Sisa dokumen ini adalah spesifikasi normatif yang diperlukan untuk implementasi yang dapat saling beroperasi.


Spesifikasi Protokol qub

Field Nilai
Versi 1.0 (versi protokol 0x01, versi pembungkus luar 0x01)
Tanggal 2026-05-01
Status Draf
Ditinjau hingga 2026-05-01

Dokumen ini adalah spesifikasi protokol normatif untuk sistem komitmen berwaktu qub. Dokumen ini mendefinisikan struktur data, aturan serialisasi, rumus derivasi, dan prosedur verifikasi yang diperlukan untuk implementasi yang dapat saling beroperasi.

Cakupan: lapisan protokol secara sengaja bersifat netral-bahasa — body qub adalah plaintext / markdown / byte pakta yang opak, dan rendering yang mengenali lokal adalah tanggung jawab pembaca (aplikasi web qub.social, iframe <qub-embed>, klien MCP, dll.).


1. Notasi dan Konvensi

Notasi Arti
u8, u64, i64 Bilangan bulat tanpa tanda/bertanda dengan lebar bit tertentu
[u8; N] Larik byte dengan panjang tetap sebesar N byte
Vec<u8> Larik byte dengan panjang variabel
Option<T> Nilai bertipe T, atau tidak ada
String String teks UTF-8, dinormalkan NFC
`
SHA3-256(x) Hash NIST SHA3-256 dari string byte x (FIPS 202)
ceil(x) Fungsi pembulatan ke atas: bilangan bulat terkecil ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Byte paling signifikan didahulukan

Semua bilangan bulat dalam konstruksi preimage dikodekan sebagai larik byte berlebar tetap big-endian (i64 → 8 byte, u8 → 1 byte) kecuali ditentukan lain.

Semua stempel waktu adalah detik Unix dalam UTC.


2. Struktur Data

2.1 ComposeQub (Status In-Memory Pembuat)

Tidak diserialisasikan ke CBOR. Tidak ditulis ke penyimpanan permanen. Lokal pada aplikasi pembuat.

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 (Payload Terdekripsi)

Diserialisasikan menggunakan CBOR kanonis (§3). Terenkripsi di dalam SealedQub. Inilah struktur yang membuktikan integritas konten setelah dekripsi.

QubEnvelope {
    version:             u8,              // Protocol major version (0x01 for v1)
    qub_id:              [u8; 32],        // Derived (see §4.1)
    content_type:        u8,              // Content type registry (see §6)
    created_at:          i64,             // Unix seconds UTC
    unlock_at:           i64,             // Unix seconds UTC
    outcome_at:          Option<i64>,     // V1.1 — when reality renders judgment (verdict-uplift-plan §3.1)
    sender_label:        Option<String>,  // Decorative; not authenticated in MVP
    reply_to:            Option<[u8; 32]>,// Parent qub_id for reply chains; not in qub_id preimage; not signed (see §9.3)
    body:                Vec<u8>,         // Content payload (UTF-8 for text, CBOR for pact)
    body_hash:           [u8; 32],        // SHA3-256(body) (see §4.2)
    sig_alg:             u8,              // Signature algorithm (see §9.2)
    author_signature:    Option<Vec<u8>>, // Set when sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Set when sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
    cosigner_signature:  Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}

Baseline (qub teks tak bertanda tangan): version = 0x01, content_type = 0x01, sig_alg = 0x00, semua field Option tidak ada.

Konfigurasi v1 lainnya: content_type = 0x03 (body pakta, lihat §6.1); sig_alg = 0x01 (ML-DSA-65) dengan author_signature dan author_pubkey hadir (lihat §9.3); cosigner_pubkey dan cosigner_signature hadir bersama untuk pakta yang ditandatangani bersama (lihat §9.7); reply_to diatur ke qub_id qub induk untuk qub rantai-balasan (lihat §9.3 untuk implikasi cakupan tanda tangan).

2.3 SealedQub (Format Wire Kanonis)

Diserialisasikan menggunakan CBOR kanonis (§3). Ditulis ke penyimpanan permanen. Inilah artefak on-chain.

SealedQub {
    version:           u8,              // Protocol major version (0x01 for v1)
    qub_id:            [u8; 32],        // Same as QubEnvelope.qub_id
    visibility:        u8,              // 0x01 = public; v1 viewers reject other values
    unlock_at:         i64,             // Unix seconds UTC
    outcome_at:        Option<i64>,     // V1.1 — surfaced on the verdict-watch CTA
                                        //   before reveal; mirrors QubEnvelope.outcome_at;
                                        //   bound to qub_id via the §4.1 preimage.
    drand_chain_id:    String,          // drand chain hash (hex string)
    drand_round:       u64,             // Target drand round number
    tlock_ciphertext:  Vec<u8>,         // tlock-encrypted QubEnvelope CBOR bytes
    recipient_pubkey:  Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
                                        //   but not interpreted by the v1 reference viewer
    title:             Option<String>,  // Plaintext title surfaced on the viewer
                                        //   countdown before reveal. Bound to qub_id
                                        //   via title_hash (§4.1). 1..=100 NFC code
                                        //   points, no control characters.
}

2.4 RevealedQub (Status Aplikasi Pembaca)

Tidak diserialisasikan ke CBOR. Lokal pada aplikasi pembaca. Dibangun setelah dekripsi dan verifikasi berhasil.

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 — diteruskan dari QubEnvelope.outcome_at / SealedQub.outcome_at; menggerakkan blok pengawas-putusan di halaman pengungkapan (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. Profil CBOR Kanonis

Semua serialisasi SealedQub dan QubEnvelope HARUS mematuhi profil ini. Dua implementasi yang diberi struktur logis yang sama HARUS menghasilkan byte yang identik.

3.1 Aturan Pengkodean

Aturan Spesifikasi
Standar RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Urutan key map Diurutkan menurut panjang byte terenkode terlebih dahulu (yang lebih pendek sebelum yang lebih panjang), kemudian secara leksikografis (byte per byte untuk pengkodean berpanjang sama)
Pengkodean integer Bentuk terpendek: 0–23 di byte awal; 24–255 dalam 2 byte; 256–65535 dalam 3 byte; dst.
Pengkodean panjang Hanya panjang pasti. Tidak ada array, map, byte string, atau text string panjang-tak-tentu (additional info = 31 dilarang).
Tag Tidak ada tag CBOR (major type 6 dilarang).
Floating-point Tidak ada float (major type 7 nilai 0xF9–0xFB dilarang).
Text string Dikodekan UTF-8, dinormalkan NFC (Unicode Normalization Form C).
Byte string Byte mentah. Tidak ada pengkodean base64 di lapisan CBOR.
Key duplikat Tolak dengan error. Parser TIDAK BOLEH secara diam-diam menerima key map duplikat.
Nilai simple Hanya true (0xF5), false (0xF4), dan null (0xF6) yang diperbolehkan.
Field opsional Field opsional yang tidak ada dihilangkan dari map CBOR sepenuhnya (tidak dikodekan sebagai null). Field opsional yang hadir disertakan dalam urutan key terurut.

3.2 Urutan Key Kanonis yang Diverifikasi

Urutan key ini bersifat normatif. Implementasi HARUS mengeluarkan key dalam urutan yang sama persis ini. Assertion debug SEBAIKNYA memverifikasi urutan dalam build non-release.

QubEnvelope (version 0x01, tak bertanda tangan, semua field opsional tidak ada):

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

Derivasi urutan key QubEnvelope: setiap key adalah text string CBOR. Panjang terenkode = 1 byte header + panjang string (untuk string di bawah 24 byte). Diurutkan menurut total panjang terenkode terlebih dahulu, kemudian secara leksikografis untuk key berpanjang sama.

SealedQub (version 0x01, publik, tanpa penerima):

"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 (body pakta, 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 (baris dari larik terms):

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

PartyIdentifier (map party_a / party_b):

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

3.3 Referensi Pengkodean Byte

Tipe Pengkodean CBOR Contoh
Hash SHA3-256 (32 byte) 0x58 0x20 + 32 byte body_hash, qub_id
Stempel waktu (i64) Major type 0 (positif) atau 1 (negatif), pengkodean terpendek Detik Unix
Version (u8, nilai 1) 0x01 (byte tunggal)
Content type (u8, nilai 1) 0x01 (byte tunggal)
sig_alg (u8, nilai 0) 0x00 (byte tunggal)
Tanda tangan ML-DSA-65 (3.309 byte) 0x59 0x0C 0xED + 3.309 byte author_signature, cosigner_signature
Kunci publik ML-DSA-65 (1.952 byte) 0x59 0x07 0xA0 + 1.952 byte author_pubkey, cosigner_pubkey

4. Derivasi Normatif

4.1 qub_id

qub_id secara unik mengidentifikasi sebuah qub dan mengikat QubEnvelope ke SealedQub. Ia diturunkan secara deterministik dari konten envelope.

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

Pengkodean pemisah domain: String "QUB_ID_V2" adalah 9 byte ASCII. Sebuah byte padding 0x00 tunggal ditambahkan untuk mencapai 10 byte demi penyelarasan. Implementasi HARUS menggunakan tepat 10 byte ini: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Pengkodean outcome_at: V1.1 memperluas preimage dari 92 menjadi 100 byte untuk melipat field outcome_at opsional ke dalam pengikatan. outcome_at yang tidak hadir dikodekan sebagai 8 byte nol; validator protokol menolak outcome_at <= 0 di mana pun sehingga sentinel ini tidak dapat berbenturan dengan nilai yang sah. Lihat §3.2 (format wire) dan tasks/verdict-uplift-plan.md dalam pohon untuk mekanik verdict yang memotivasi field ini.

Pengkodean drand_round: V1.2 memperluas preimage dari 100 menjadi 108 byte untuk melipat drand_round (putaran drand target, §4.3) ke dalam pengikatan, dan menaikkan pemisah domain menjadi QUB_ID_V2. Ini mengikat putaran timelock ke dalam identitas qub: sebuah gateway tidak dapat mengikat ulang ciphertext ke putaran yang berbeda (mis. yang sudah lewat) dari yang diimplikasikan oleh unlock_at yang ditampilkan. Prosedur unlock (§8) selain itu memverifikasi bahwa putaran yang ditanamkan ke dalam stanza ciphertext tlock cocok dengan unlock_round(unlock_at), sehingga waktu unlock yang ditampilkan terbukti merupakan putaran yang menggerbangi dekripsi.

Properti:

4.2 body_hash

body_hash = SHA3-256(body)

Di mana body adalah payload konten Vec<u8> mentah. Untuk qub teks, ini adalah body qub yang dikodekan UTF-8.

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

Di mana title adalah judul plaintext opsional yang dimunculkan pada hitung mundur pembaca sebelum pengungkapan (lihat §3.2). Normalisasi NFC dijalankan pada saat hash sehingga digest stabil di seluruh urutan code-point yang setara secara visual. Sentinel semua-nol dicadangkan untuk kasus tidak-hadir; string kosong ditolak pada batas CBOR kanonis sebagai pengkodean non-kanonis dari "tidak hadir" (pengkodean kanonis menghilangkan field sepenuhnya).

4.3 Pemetaan Unlock-Round

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
Parameter Sumber Contoh
unlock_at Detik Unix UTC yang dipilih pengguna 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time Info chain drand (genesis_time) 1595431050
chain_period_seconds Info chain drand (period) 30

Operasi ceil() memilih putaran drand pertama yang waktu pengungkapannya ≥ unlock_at. Ini memastikan qub tidak menjadi dapat didekripsi sebelum waktu unlock yang dipilih.

Kasus tepi: jika (unlock_at - chain_genesis_time) habis dibagi tepat oleh chain_period_seconds, hasilnya adalah putaran tepat itu — qub terbuka tepat pada waktu pengungkapan putaran tersebut.

Validasi: unlock_at HARUS berada di masa depan pada saat penyegelan. unlock_at TIDAK BOLEH lebih dari 10 tahun dari created_at (untuk membatasi risiko ketergantungan drand horison-panjang; UI SEBAIKNYA memperingatkan untuk tanggal unlock di luar 2 tahun).


5. Newtypes Format Wire

Newtypes format wire memberikan keamanan saat kompilasi terhadap kebingungan antara byte CBOR dengan JSON, plaintext mentah, atau pengkodean byte lainnya.

Tipe Berisi Diproduksi Oleh Dikonsumsi Oleh
SealedQubCbor CBOR kanonis dari SealedQub serialize_sealed_qub() Unggahan penyimpanan permanen, fetch pembaca
QubEnvelopeCbor CBOR kanonis dari QubEnvelope serialize_qub_envelope() Input enkripsi tlock, output dekripsi tlock

5.1 Aturan Konstruksi

// 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 Validasi pada Konstruksi

from_encoded() SEBAIKNYA memvalidasi bahwa input dimulai dengan header map CBOR yang valid. Validasi struktural penuh terjadi pada waktu parse, bukan waktu konstruksi, untuk menghindari double-parsing.


6. Registri Content Type

Nilai Tipe Ukuran Body Maksimum Catatan
0x00 Dicadangkan (tidak valid) TIDAK BOLEH digunakan
0x01 Teks biasa (UTF-8, Markdown terbatas) 50 KB berbayar / 10 KB gratis Lihat §10 untuk aturan rendering. Pemisahan gratis / berbayar ditegakkan oleh layanan upload; batas keras lapisan protokol adalah 50 KB.
0x02 Dicadangkan (masa depan) Dialokasikan untuk content type masa depan; tidak valid di v1. Pembaca HARUS menolak sesuai aturan di bawah.
0x03 Pakta (kesepakatan bilateral, body CBOR) 100 KB Body adalah PactTerms CBOR kanonis (§6.1). Penandatanganan bersama per §9.7.
0x04 Putusan (penilaian-diri pembuat, body CBOR) 8 KB Body adalah VerdictBody CBOR kanonis (§6.2). Dikeluarkan hanya oleh intent verdict sisi-sistem. Hubungan induk berada pada tag Arweave Parent-Tx-Id, bukan pada body. Lihat verdict-uplift-plan §3.4.

Pembaca HARUS menolak content type yang tidak dikenal dengan pesan error yang jelas terlihat oleh pengguna. Pembaca TIDAK BOLEH mencoba merender tipe tidak dikenal sebagai teks.

6.1 Body Pakta (content_type = 0x03)

Body pakta adalah pengkodean CBOR kanonis dari nilai PactTerms:

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

Urutan key CBOR kanonis untuk ketiga map diberikan di §3.2. Total CBOR pakta terserialisasi TIDAK BOLEH melebihi 100 KB (sesuai §6).

Diskriminator skema. Baris pertama dalam terms untuk pakta structured/v1 HARUS berupa { key: "pact_schema", value: "structured/v1" }. Baris tanpa penanda ini adalah pakta "custom" dan tidak mendapatkan validasi terstruktur atau rendering yang sadar-skema.

Slot pengakuan yang dibekukan. Pakta structured/v1 membawa tepat empat baris pengakuan di bawah key berikut:

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

value untuk masing-masing adalah salah satu dari delapan string bahasa Inggris yang dibekukan yang dipilih oleh pasangan (role, kind), di mana role ∈ { seller, buyer, provider, client } dan kind ∈ { standard, capacity }. String itu sendiri adalah data protokol normatif — tanda tangan ML-DSA-65 kedua pihak melakukan commit ke byte yang tepat melalui body_hash. Mereka TIDAK dilokalisasi; body yang ditandatangani bersifat netral-bahasa. Setiap perubahan kata mengharuskan versi skema baru (structured/v2).

Delapan string, pencariannya (acknowledgement_for(role, kind)), dan dasar pemikiran untuk masing-masing dipatok oleh implementasi referensi. Implementasi yang mengikuti standar HARUS mengeluarkan nilai pengakuan yang identik byte demi byte; tes body-hash SHA3-256 golden-fixture yang mencakup keempat kombinasi peran menangkap setiap pergeseran.

Urutan tampilan pembaca. String pengakuan berisi frasa seperti "described above", yang mengandaikan baris deskripsi / cakupan dirender sebelum pengakuan. Pembaca HARUS merender array terms dalam urutan CBOR; menyusun ulang merusak semantik prosa.

Kontak rekan-pihak. Ketika contact Pihak B adalah alamat email yang valid, layanan upload qub secara otomatis mengirimkan email undangan tinjau / tanda-tangan-bersama pada saat staging dan mengikat tanda-tangan-bersama yang akhirnya terjadi pada verifikasi alamat yang sama (§9.7). Pakta yang kontak Pihak B-nya tidak ada masih dapat ditandatangani bersama, tetapi hanya melalui saluran di luar jalur — layanan menolak permintaan tanda-tangan-bersama yang tidak dapat menghasilkan penanda verifikasi-email 15-menit yang cocok.

6.2 Body Putusan (content_type = 0x04)

Body putusan adalah pengkodean CBOR kanonis dari nilai VerdictBody:

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
}

Urutan key CBOR kanonis:

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

Total CBOR putusan terserialisasi TIDAK BOLEH melebihi 8 KB (sesuai baris registri di atas).

Enum outcome. Byte wire bersifat netral-intent; empat kategori Right / Partial / Wrong / Unfalsifiable mencakup seluruh ruang hasil dari setiap intent yang membawa putusan. Label per-intent ("Tepat sasaran" / "Saya menepatinya" / "Dirilis sesuai rencana" / "Dikonfirmasi oleh peristiwa" untuk Right, dsb.) adalah urusan rendering sisi-pembaca yang diselesaikan terhadap intent qub induk — wire tetap netral-bahasa dan netral-intent. Nilai di luar 1..=4 HARUS ditolak pada decode.

Tautan induk. qub putusan TIDAK membawa referensi induk di body-nya. ID transaksi Arweave qub induk dikeluarkan sebagai tag penyimpanan Parent-Tx-Id pada waktu upload (lapisan tag penyimpanan §7). Ini menjaga body sebagai pernyataan penilaian-diri yang terikat tanda tangan dan mandiri; rantai audit ("benar tentang apa?") ditetapkan melalui pencarian tag Arweave.

Keamanan evidence URL (normatif). Ketika evidence_url ada, validator (sisi-compose, sisi-wire, edge Worker) HARUS menegakkan:

  1. HTTPS saja. String HARUS dimulai dengan urutan byte https://. Skema lain — http, ftp, javascript, data, file, dsb. — ditolak.
  2. Batas panjang. ≤ 2,048 byte (batas praktis URL peramban).
  3. Pemeriksaan NFC + codepoint-bermusuhan. Aturan yang sama dengan title dan reflection — codepoint bidi-override / zero-width / tag-block / BOM / C0 / C1 ditolak. Definisi sesuai dengan Rust crate::handle::contains_hostile_text_codepoint dan TS workers/api/src/utils/unicode.ts::isHostileCodepoint (jaga agar selaras).
  4. Tidak ada whitespace, tidak ada kontrol ASCII. Whitespace / DEL / byte sub-0x20 di mana pun dalam URL ditolak — menutup vektor injeksi \n/\t yang tidak tercakup aturan bidi.
  5. Segmen host non-kosong. Semua antara https:// dan /, ?, atau # pertama HARUS non-kosong.

Tidak ada fetch sisi-server. Worker TIDAK BOLEH memproksi, mengambil, atau melakukan preview URL. Protokol menyimpan sebuah string; rendering terjadi sisi-pembaca dengan rel="nofollow noopener noreferrer" target="_blank" dan host yang terlihat ditampilkan di samping teks tautan.

Refleksi. Teks refleksi opsional yang ditulis pembuat ("apa yang berubah, apa yang Anda pelajari"). Validasi NFC + codepoint-bermusuhan yang sama dengan title. Input kosong / hanya-whitespace menyusut menjadi tidak hadir pada saat konstruksi.

Versi skema. v1 hanya mendukung verdict_version = 0x01. Revisi skema masa depan menaikkan byte ini dan dirilis bersamaan dengan versi protokol baru per §12.


7. Protokol Penyegelan

Urutan penyegelan lengkap. Setiap langkah bersifat normatif.

 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.

Lapisan tag penyimpanan (di luar jalur). Layanan upload qub melampirkan sehimpunan kecil tag transaksi penyimpanan secara sengaja di samping payload yang dibungkus. Content-Type=application/octet-stream diwajibkan secara normatif. Layanan referensi juga melampirkan tiga tag opsional ketika pembuat memilih untuk memunculkannya: Intent (intent komposisi yang divalidasi-allowlist — mis., quote, reply, commitment), Author (sidik jari pubkey pembuat §9.3 sebagai hex huruf kecil 64 karakter), dan Parent-Tx-Id (ID transaksi penyimpanan qub induk untuk rantai balasan, base64url 43 karakter).

Tag Author bersifat opt-in per qub: aplikasi pembuat referensi melampirkannya hanya ketika pengguna secara eksplisit mengaktifkan atribusi publik pada waktu penyegelan. Ketika toggle dimatikan — default-nya — tidak ada tag Author yang ditulis dan qub tidak dikaitkan di chain: tidak ada apa pun di penyimpanan permanen yang menghubungkan upload tersebut dengan handle pembuat, email, atau qub lainnya. Ketika toggle dinyalakan, sidik jari Author diresolusi ke @handle pilihan pembuat melalui rantai atestasi §9.5. Hubungan rantai-balasan dan Intent tidak mengidentifikasi. Pembungkus luar (§13) melindungi body dalam dari korelasi ciphertext — mencegah pemanen mengenali dan mendekripsi massal upload berbentuk qub setelah putaran drand mereka diterbitkan.

Layanan referensi secara sengaja TIDAK melampirkan tag App-Name, App-Version, atau Type: filter nilai-tunggal seperti itu akan mengembalikan seluruh korpus qub ke kueri GraphQL, yang tidak konsisten dengan cakupan kerahasiaan body-saja dari pembungkus.

Verifier yang mengikuti standar TIDAK BOLEH bergantung pada tag penyimpanan apa pun untuk verifikasi pihak ketiga §11; body hash / qub_id / tanda tangan hanya melakukan commit ke CBOR dalam, tidak pernah ke set tag.


8. Protokol Unlock

Urutan unlock lengkap. Setiap langkah bersifat normatif.

 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. Penandatanganan Kepenulisan

9.1 Dasar Pemikiran

qub disimpan di penyimpanan permanen. Tanda tangan kepenulisan harus tetap tidak dapat dipalsukan tanpa batas waktu, itulah sebabnya v1.0 menggunakan skema pasca-kuantum ML-DSA-65 (FIPS 204) alih-alih skema klasik yang keamanannya dapat menurun selama masa hidup permanen qub.

9.2 Registri Algoritma

sig_alg Skema Ukuran Kunci Ukuran Tanda Tangan
0x00 Tanpa tanda tangan (tidak ditandatangani)
0x01 ML-DSA-65 (FIPS 204) 1.952 byte 3.309 byte

Pembaca HARUS menolak nilai sig_alg yang tidak dikenal.

9.3 Konstruksi Preimage yang Ditandatangani

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)

Pemisah domain: "QUB_AUTHOR_SIG_V1" adalah 17 byte ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Tanpa padding.

Byte penutup: byte preimage ke-91 HARUS bernilai 0x00. Implementasi referensi memunculkan ini sebagai konstanta ORG_ID_PRESENT_INDIVIDUAL = 0x00 di crates/qub-core/src/signing.rs; pembaca yang merekonstruksi sig_input untuk verifikasi HARUS mengeluarkan byte yang sama.

Cakupan tanda tangan — apa yang dicakup dan tidak dicakup. sig_input melakukan commit pada empat field envelope: version, qub_id, body_hash, unlock_at (ditambah pemisah domain tetap dan byte org_id_present). Tiga dari empat itu adalah invarian struktural: qub_id itu sendiri diturunkan dari version, content_type, created_at, unlock_at, outcome_at, drand_round, dan body_hash melalui preimage §4.1, sehingga setiap perubahan pada field tersebut menghasilkan qub_id yang berbeda dan membatalkan tanda tangan secara transitif. Permukaan yang diautentikasi secara langsung karenanya adalah:

Field Diautentikasi oleh tanda tangan Cara
version Input langsung ke sig_input
qub_id Input langsung
body_hash Input langsung
unlock_at Input langsung
content_type Secara transitif, lewat preimage qub_id
created_at Secara transitif, lewat preimage qub_id
outcome_at Secara transitif, lewat preimage qub_id
drand_round Secara transitif, lewat preimage qub_id (V1.2)
body Secara transitif, lewat body_hash = SHA3-256(body)
author_pubkey — (implisit) Kunci yang memverifikasi tanda tangan adalah penulis, secara definisi
sender_label Teks tampilan saja; dapat berubah tanpa merusak tanda tangan
reply_to Pointer threading; dapat berubah tanpa merusak tanda tangan
cosigner_pubkey / cosigner_signature Ditandatangani secara independen atas sig_input yang sama (lihat §9.7)
drand_chain_id, tlock_ciphertext, visibility Field SealedQub luar, tidak di dalam envelope — dicakup oleh invarian struktural mereka sendiri (konsistensi putaran / chain) tetapi tidak oleh tanda tangan penulis. (drand_round kini terikat secara transitif lewat preimage qub_id — lihat di atas.)

Implikasi keamanan dari field yang tidak diautentikasi.

Implementasi yang menampilkan sender_label atau reply_to kepada pengguna akhir HARUS memunculkan identitas yang diautentikasi (sidik jari pubkey, atestasi) sebagai sinyal identitas utama, bukan label.

9.4 Prosedur Verifikasi

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

Verifikasi tanda tangan adalah operasi yang paling mahal (terutama ML-DSA-65). Sebaiknya dilakukan setelah semua pemeriksaan yang lebih murah (hash, qub_id, unlock_at) berhasil.

9.5 Atestasi Identitas

Atestasi identitas — pemetaan author_pubkey ke klaim identitas yang dapat dikenali manusia seperti handle qub, alamat email, handle sosial, atau kredensial passkey — adalah peningkatan progresif sisi-pembaca dan tidak diperlukan untuk verifikasi tanda tangan. Pembaca yang menyelesaikan atestasi ke identitas tampilan HARUS menerapkan prioritas:

handle > email > social > fingerprint

Fallback sidik jari adalah hex huruf kecil dari SHA3-256(author_pubkey); ini selalu tersedia untuk qub bertanda tangan apa pun. Pembaca BOLEH menyingkatnya untuk tampilan — pembaca referensi merender qub: diikuti oleh empat byte pertama dan terakhir (qub:<8 hex>…<8 hex>).

Verifier yang mengikuti standar dapat menyelesaikan setiap pemeriksaan di §9.4 tanpa menghubungi API qub, tanpa jaringan apa pun di luar penyimpanan permanen dan drand, dan tanpa pencarian sisi-server apa pun. Resolusi atestasi adalah langkah best-effort terpisah yang dilakukan hanya setelah verifikasi tanda tangan berhasil.

9.6 Dampak Ukuran

Ed25519 ML-DSA-65
Tanda tangan 64 byte 3.309 byte
Kunci publik 32 byte 1.952 byte
Total per qub 96 byte 5.261 byte
Delta biaya penyimpanan (sekitar $5/MB) ~$0,0005 ~$0,026

Untuk qub teks 500–2.000 byte, ML-DSA-65 kira-kira melipatgandakan tiga kali lipat ukuran yang disimpan. Biaya absolutnya dapat diabaikan.

9.7 Verifikasi Cosigner (Kesepakatan Bilateral Pakta)

Untuk kesepakatan bilateral (content_type = 0x03), lapisan tanda tangan kedua membuktikan bahwa kedua pihak menyetujui ketentuan yang sama.

Field envelope:

Kedua field HARUS hadir bersama atau keduanya tidak ada. Jika tepat satu yang hadir, pembaca HARUS melaporkan error integritas.

Prosedur verifikasi:

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

Properti:

Gerbang pengikatan email (operasional). Ketika pakta yang di-staging membawa kontak email Pihak B (§6.1), layanan upload qub HARUS menolak permintaan tanda-tangan-bersama kecuali ada penanda verifikasi-email berusia pendek yang cocok dengan id staging dan hash email yang dinormalkan dari kontak tersebut. Penanda ditulis oleh /api/v1/auth/verify ketika token magic-link membawa staging_id dan alamat yang diverifikasi cocok dengan SHA-256(normalise_email(party_b.contact)) — di mana normalise_email(addr) mempertahankan kapitalisasi bagian-lokal dan hanya mengubah bagian domain menjadi huruf kecil (sesuai RFC 5321 §2.3.11), dan SHA-256 di sini adalah hash NIST FIPS 180-4 (berbeda dari SHA3-256 yang digunakan dalam derivasi §4) — dan kedaluwarsa 900 detik (15 menit) setelah diterbitkan. Ini adalah gerbang anti-peniruan operasional, BUKAN bagian dari bukti qub on-chain — verifier pihak ketiga yang memutar ulang §11 hanya memerlukan penyimpanan permanen dan drand, tanpa pencarian sisi-server apa pun. Penanda hanya ada di sisi server dan tidak pernah menjadi bagian dari body yang ditandatangani.

Dampak ukuran (penulis ML-DSA-65 + cosigner):

Komponen Ukuran
Tanda tangan penulis 3.309 byte
Kunci publik penulis 1.952 byte
Tanda tangan cosigner 3.309 byte
Kunci publik cosigner 1.952 byte
Total overhead kripto 10.522 byte
Delta biaya penyimpanan ~$0,05

10. Rendering Markdown dan Sanitasi

Bagian ini sangat kritis untuk keamanan. Pembaca merender qub teks (content_type = 0x01) menggunakan subset Markdown yang dibatasi.

10.1 Elemen yang Diizinkan

10.2 Elemen yang Dilarang

Elemen Penanganan
HTML mentah (<div>, <script>, dll.) Dihapus sepenuhnya. Tidak ada HTML yang lolos.
Gambar (![alt](url)) Dihapus. Sintaks gambar dihapus dari output.
Tautan ([text](url)) URL dirender sebagai teks biasa yang terlihat. Tidak otomatis ditautkan. Tidak dapat diklik tanpa tindakan pengguna eksplisit.
Skema URL berbahaya javascript:, data:, vbscript:, file: — dihapus.
Iframe, embed, object Dihapus.
Entitas HTML Didekode ke karakter tampilan hanya jika aman.

10.3 Implementasi

Implementasi HARUS menggunakan parser allowlist ketat, bukan blocklist. Pendekatan yang direkomendasikan:

  1. Parsing Markdown menggunakan pulldown-cmark (atau setara).
  2. Telusuri AST dan buang setiap node yang tidak ada dalam allowlist (§10.1).
  3. Untuk node tautan: keluarkan URL sebagai teks yang terlihat, bukan sebagai elemen <a> yang dapat diklik.
  4. Konversikan AST yang difilter ke representasi perantara bertipe (mis., enum MarkdownNode dengan hanya varian yang aman). HTML mentah secara struktural tidak dapat direpresentasikan dalam IR ini.
  5. Render dari IR bertipe ke lapisan tampilan target (mis., komponen tampilan reaktif, node DOM). Tanpa konkatenasi string HTML atau innerHTML di titik mana pun.

Pendekatan blocklist rapuh karena ekstensi Markdown baru atau keanehan parser dapat memperkenalkan elemen yang tidak terfilter. Pendekatan AST-bertipe membuat XSS secara struktural tidak mungkin — tidak ada varian yang dapat membawa HTML sembarangan.

10.4 Batas Ukuran dan Struktur


11. Verifikasi Pihak Ketiga

Pihak ketiga mana pun dapat memverifikasi qub publik tanpa kerja sama qub. Prosedur verifikasi:

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.

Apa yang dibuktikan oleh verifikasi:

Bukti Apa yang ditegakkannya
Komitmen Ciphertext sudah ada pada stempel waktu blok penyimpanan.
Integritas Body plaintext cocok dengan hash yang di-commit dan belum diubah.
Waktu Konten tidak dapat dibaca hingga putaran drand, yang sesuai dengan waktu unlock yang dipilih (tunduk pada asumsi keamanan tlock dan drand).

Apa yang TIDAK dibuktikan oleh verifikasi:

Bukan-bukti Mengapa
Kepenulisan sender_label bersifat dekoratif. Tanpa sig_alg0x01, siapa pun bisa saja telah menyegel konten ini.
Maksud qub membuktikan konten dan waktu, bukan apa yang dimaksudkan pembuat secara subjektif.
Waktu pra-peristiwa Inklusi blok penyimpanan dapat tertinggal beberapa menit dari upload sebenarnya. Stempel waktu komitmen adalah waktu blok, bukan saat pengguna menekan "seal".

12. Versi

12.1 Versi Protokol

Field version (u8) di kedua SealedQub dan QubEnvelope mengidentifikasi versi major protokol.

12.2 Riwayat Versi

Versi Nilai Deskripsi
v1 0x01 qub teks publik (content_type 0x01), kesepakatan bilateral pakta (0x03, skema structured/v1, penulis ML-DSA-65 + cosigner), tlock, SHA3-256

12.3 Kompatibilitas Maju

Pembaca v1 yang menemui QubEnvelope dengan key map CBOR opsional yang tidak dikenal (key yang tidak ada dalam urutan kanonis §3.2) SEBAIKNYA mengabaikan key tersebut dan melanjutkan verifikasi menggunakan field yang dikenal. Ini memungkinkan tambahan minor di masa depan (mis., metadata baru) tanpa memerlukan peningkatan versi major.

Pembaca v1 yang menemui sig_alg = 0x01 (ML-DSA-65) tetapi tidak memiliki dukungan verifikasi ML-DSA-65 SEBAIKNYA menampilkan konten qub dengan pemberitahuan "tanda tangan ada tetapi tidak dapat diverifikasi", bukan menolak qub sepenuhnya. Implementasi referensi saat ini menolak setiap nilai sig_alg selain 0x00 dan 0x01 karena registri v1 tidak berisi algoritma valid lainnya — penolakan ketat dan kegagalan-lunak secara observasional identik hingga algoritma ketiga didaftarkan. Perilaku kegagalan-lunak di atas menjadi load-bearing setelah §9.2 menerima entri baru, dan pembaca referensi akan diperbarui untuk gagal-lunak pada titik itu.

12.4 Versi Pembungkus Luar

OuterWrapper yang dideskripsikan di §13 membawa byte version miliknya sendiri, independen dari SealedQub.version dan QubEnvelope.version. Kedua ruang versi berkembang secara terpisah: pengganti simetris pasca-kuantum yang aman di masa depan menaikkan byte pembungkus tanpa menyentuh versi protokol dalam, dan tambahan lapisan-protokol di masa depan (mis., field envelope baru) menaikkan versi dalam tanpa menyentuh byte pembungkus.

OUTER_WRAPPER_VERSION_* Nilai Algoritma Status
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM dengan nonce 12 byte, tag autentikasi 16 byte, AAD terikat ke qub_id default v1
0x020xFF Dicadangkan Masa depan

Pembaca HARUS menolak versi pembungkus yang tidak dikenal dengan error yang jelas. Protokol secara sengaja menjaga ruang versi pembungkus tetap sempit hingga pendorong migrasi konkret muncul (mis., panduan NIST yang menyukai AEAD berbeda); slot 0x02 akan dialokasikan dalam revisi yang sama yang memperkenalkan algoritma tersebut.


13. Pembungkus Enkripsi Luar

13.1 Dasar Pemikiran

Lapisan protokol (QubEnvelope → tlock → SealedQub) membuat qub tersegel terkunci waktu: body tidak dapat dibaca hingga unlock_at dan tanda tangan putaran drand telah diterbitkan. Namun setelah unlock, tanda tangan putaran bersifat publik dan bentuk CBOR kanonis dari SealedQub dapat dikenali, sehingga pemanen yang mengindeks transaksi penyimpanan permanen dapat mendekripsi massal seluruh korpus qub.

Pembungkus enkripsi luar menutup saluran tersebut dengan menyisipkan lapisan AEAD simetris tambahan antara SealedQubCbor kanonis dan byte yang ditulis ke penyimpanan permanen. Kunci 256-bit K hidup hanya di fragment URL dari tautan pengiriman dan pada perangkat pengguna; browser tidak mentransmisikan fragment URL ke server, sehingga qub.social, setiap gateway penyimpanan, dan setiap CDN di depan keduanya secara observasional buta terhadap K. Setiap qub di penyimpanan permanen karena itu adalah ciphertext opak yang plaintext-nya tidak dapat dipulihkan tanpa URL yang dipilih pembuat untuk dibagikan.

Efek bersih:

13.2 Pelapisan

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)

Penyegelan dan unlock pada lapisan protokol (§7, §8) tidak berubah di bawah batas pembungkus; pembungkus melekat pada situs panggilan seal() dan lepas pada situs panggilan unlock().

13.3 Struktur Data OuterWrapper

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
}

Invarian field.

Pengkodean CBOR. CBOR kanonis per §3, dengan aturan urutan-key yang sama (diurutkan menurut panjang byte terenkode menaik, kemudian secara leksikografis). Empat key tersebut adalah:

Key Byte terenkode Urutan
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

Byte pertama dari OuterWrapper CBOR karenanya adalah header map panjang-pasti untuk map 4-entri (0xA4).

13.4 Pengikatan AAD ke qub_id

Pembungkus mengikat qub_id sebagai additional authenticated data AEAD. Ini adalah pertahanan struktural yang menjadi tumpuan terhadap tiga kelas serangan:

Serangan Pertahanan
Memindahkan ciphertext ke bawah field qub_id yang berbeda di pembungkus AAD tidak cocok → autentikasi AEAD gagal
Mencampur fragment URL dari qub A dengan byte penyimpanan permanen dari qub B AAD tidak cocok → autentikasi AEAD gagal
Memanipulasi field qub_id pembungkus setelah upload AAD tidak cocok → autentikasi AEAD gagal

Membawa qub_id dalam plaintext pembungkus tidak secara berarti melemahkan kekebalan enumerasi — qub_id sendiri adalah hash SHA3-256 dari preimage §4.1 tanpa preimage yang dapat dipulihkan dari digest, dan seorang enumerator yang sudah memanen byte pembungkus tidak belajar apa pun dari qub_id yang terlihat yang tidak bisa mereka simpulkan dari keberadaan upload itu sendiri.

13.5 Algoritma Wrap dan Unwrap

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

Peluruhan mode-kegagalan. K salah, nonce salah, AAD tidak cocok, dan ciphertext yang dimanipulasi semuanya menghasilkan error DECRYPT_FAILED yang sama. Ini adalah properti AEAD yang disengaja: membedakan mode kegagalan akan menciptakan saluran samping yang dapat diuji penyerang jarak jauh dengan mengirim pembungkus yang cacat dan mengukur waktu respons. Implementasi referensi HARUS menyusutkan semua kegagalan AEAD ke satu bentuk error.

13.6 Material Kunci dan Distribusi

Kunci pembungkus K adalah nilai acak seragam 256-bit yang dihasilkan per-qub oleh CSPRNG. Implementasi referensi mengambilnya dari:

Distribusi: K HARUS dikodekan sebagai base64 URL-safe (RFC 4648 §5, tanpa padding) dan ditambahkan ke tautan pengiriman sebagai komponen fragment:

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

Fragment tidak pernah ditransmisikan ke server mana pun oleh browser yang mengikuti standar. Saluran pemulihan (indeks riwayat sisi-server, kirim-otomatis email opt-in) yang menyimpan tautan pengiriman lengkap — termasuk fragment — di luar perangkat pengguna adalah trade-off eksplisit terhadap postur crypto-shredding default dan HARUS digerbangi pada persetujuan pengguna eksplisit.

Kehilangan fragment. Jika pengguna kehilangan fragment URL dan tidak memiliki saluran pemulihan, qub tidak dapat dibaca. Ini adalah trade-off yang menjadi tumpuan desain dan HARUS diungkapkan kepada pengguna pada saat penyegelan. MVP memperkuat pengungkapan saat-penyegelan dengan teks "simpan URL ini" yang eksplisit dan saluran pemulihan email-terverifikasi untuk pengguna yang opt-in.

13.7 Di Luar Cakupan untuk Bagian Ini

13.8 qub publik (penghilangan pembungkus)

Pembungkus luar bersifat opsional pada lapisan pengiriman. Pembuat dapat menyegel qub sebagai publik, dalam hal ini SealedQubCbor kanonis ditulis ke penyimpanan permanen secara langsung, tanpa lapisan OuterWrapper dan tanpa kunci K:

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

qub publik bersifat terkunci-waktu tetapi tidak digerbangi-tautan: ia tetap tidak dapat dibaca hingga putaran drand-nya diterbitkan (lapisan tlock tidak berubah), tetapi setelah unlock siapa pun yang memiliki arweave_tx_id dapat mendekripsinya — tidak diperlukan fragment URL, karena tidak ada K. Ini adalah trade-off yang disengaja untuk permukaan yang harus digerakkan server: email pemberitahuan-pengungkapan, sematan pihak ketiga, dan SEO pasca-pengungkapan yang lebih kaya semuanya memerlukan tautan yang berfungsi tanpa rahasia yang tidak pernah dipegang server (§13.6).

Konsekuensi yang HARUS diperhitungkan produsen:

Privat (terbungkus) tetap menjadi default; publik adalah pilihan pembuat per-qub yang eksplisit.


14. Vektor Uji

14.1 Derivasi qub_id

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

Implementasi HARUS menghasilkan nilai body_hash dan qub_id yang identik untuk input ini. Vektor uji ini SEBAIKNYA menjadi unit test pertama yang ditulis. Nilai kanonis di atas dihitung oleh implementasi referensi dan HARUS cocok bit demi bit. Tata letak preimage historis (pra-peluncuran — tidak ada qub langsung yang bergantung padanya): qub_id V1.0 92-byte adalah 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; qub_id V1.1 100-byte (setelah melipat outcome_at_or_zero) adalah b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 melipat drand_round ke dalamnya dan menaikkan pemisah domain menjadi QUB_ID_V2.

14.2 Pemetaan Unlock-Round

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 Round-Trip CBOR Kanonis

Implementasi HARUS memverifikasi bahwa serialize(parse(serialize(qub))) == serialize(qub) untuk semua input yang valid. Ini adalah property test, bukan vektor tunggal.

14.4 CBOR PactTerms (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)

Byte CBOR kanonis dan body_hash SHA3-256 dihitung oleh implementasi referensi. Implementasi HARUS menghasilkan CBOR yang identik byte demi byte untuk input ini.

Implementasi juga HARUS memverifikasi bahwa serialize(parse(serialize(pact))) == serialize(pact) untuk semua input PactTerms yang valid (property test).

14.5 Vektor Lintas-Bahasa Pembungkus Luar

Pembungkus luar (§13) memiliki fixture kanonis terpisah di crates/qub-core/tests/vectors/wrapper_v1.json. Setiap kasus menetapkan tuple (key, nonce, qub_id, sealed_cbor) sebagai input hex opak dan menegaskan output expected_wrapper_hex tertentu. Kedua implementasi referensi mengonsumsi file JSON yang sama:

Fixture saat ini mematok tiga kasus:

Kasus Cakupan
basic-text-public Bentuk SealedQub realistis terkecil; tanpa field opsional. Menetapkan bentuk pembungkus kanonis untuk qub tipikal v1.0.
with-recipient-pubkey SealedQub dengan recipient_pubkey diatur (jalur Fase 2). Set key CBOR dalam berbeda, qub_id berbeda.
longer-body Body ~4 KiB — melatih prefiks panjang CBOR multi-byte di dalam envelope dalam dan ciphertext luar.

Implementasi HARUS menghasilkan expected_wrapper_hex yang identik byte demi byte untuk input yang direkam. Regenerasi fixture memerlukan QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors dan dicadangkan untuk perubahan format yang disengaja.


15. Tata Kelola Profil Kripto (Masa Depan)

Bagian ini bersifat informatif untuk v1 dan menjadi normatif pertama kali algoritma kedua masuk ke salah satu primitif kriptografis qub.

15.1 Postur Saat Ini

Protokol v1 mengikat tepat satu algoritma per primitif:

Verifier saat ini meng-hardcode panjang kunci dan tanda tangan per primitif. Tidak ada permukaan agility yang diekspos oleh format wire.

15.2 Bentuk yang Diinginkan

Ketika algoritma kedua memasuki protokol, verifier akan dikonfigurasi untuk CryptoProfile bernama (mis., ExqubV1) yang mendaftar set persis nilai yang diizinkan per primitif — sig_algs, chain drand, versi pembungkus, content type. Profil ditetapkan pada waktu verifikasi, tidak pernah dinegosiasikan secara in-band. Setiap nilai di luar profil aktif ditolak.

Ini menjamin bahwa menambahkan ML-DSA-87 atau mengaktifkan Ed25519 tidak dapat secara retroaktif melemahkan konfigurasi verifier yang ada: verifier v1 tetap menjadi verifier v1 bahkan setelah profil v2 diterbitkan.

15.3 Kondisi Pemicu

Tingkatkan §15 ke status normatif ketika salah satu dari berikut diusulkan:

Sampai saat itu §15 adalah placeholder yang menetapkan bentuk migrasi sehingga PR di masa depan mendarat pada target yang diketahui daripada melitigasi ulang permukaan negosiasi dari awal.