Specifica del protocollo qub

qub è un protocollo per impegni temporali crittografici: un sistema per sigillare le parole a una data futura e dimostrare, quando tale data arriva, esattamente cosa è stato detto e quando.

Tre primitive lo rendono possibile. drand è un beacon di casualità decentralizzato — la data di rivelazione è applicabile dalla fisica, non dalla buona volontà di alcuna parte. L'archivio pubblico permanente è un registro pubblico a prova di manomissione — nessuna parte può modificare o cancellare un qub una volta che è stato sigillato. ML-DSA-65 è una firma digitale post-quantistica — ogni qub è legato a una coppia di chiavi il cui segreto non lascia mai il dispositivo dell'autore.

Insieme queste primitive producono una dichiarazione che è bloccata nel tempo, a prova di manomissione e attribuibile — una ricevuta il cui valore cresce man mano che migliora la capacità del mondo di fabbricare il passato.

Il resto di questo documento è la specifica normativa richiesta per implementazioni interoperabili.


Specifica del protocollo qub

Campo Valore
Versione 1.0 (versione del protocollo 0x01, versione del wrapper esterno 0x01)
Data 2026-05-01
Stato Bozza
Revisionato fino a 2026-05-01

Questo documento è la specifica normativa del protocollo per il sistema di impegni temporizzati qub. Definisce le strutture dati, le regole di serializzazione, le formule di derivazione e le procedure di verifica necessarie per implementazioni interoperabili.

Ambito: il livello del protocollo è intenzionalmente neutrale rispetto alla lingua — il corpo del qub è plaintext / markdown / byte di patto opachi, e il rendering localizzato è responsabilità del visualizzatore (app web qub.social, iframe <qub-embed>, client MCP, ecc.).


1. Notazione e convenzioni

Notazione Significato
u8, u64, i64 Interi senza segno/con segno della larghezza in bit specificata
[u8; N] Array di byte a lunghezza fissa di N byte
Vec<u8> Array di byte a lunghezza variabile
Option<T> Valore di tipo T, o assente
String Stringa di testo UTF-8, normalizzata NFC
`
SHA3-256(x) Hash NIST SHA3-256 della stringa di byte x (FIPS 202)
ceil(x) Funzione ceiling: il più piccolo intero ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Byte più significativo per primo

Tutti gli interi nelle costruzioni di preimage sono codificati come array di byte a larghezza fissa big-endian (i64 → 8 byte, u8 → 1 byte) salvo diversa indicazione.

Tutti i timestamp sono secondi Unix in UTC.


2. Strutture dati

2.1 ComposeQub (stato in memoria del creatore)

Non serializzato in CBOR. Non scritto nell'archivio permanente. Locale all'app del creatore.

ComposeQub {
    draft_id:       [u8; 16],        // Casuale, generato localmente
    created_at:     i64,             // Secondi Unix UTC
    unlock_at:      Option<i64>,     // Secondi Unix UTC; None durante la composizione
    visibility:     u8,              // 0x01 = pubblico (unico valore in MVP)
    content_type:   u8,              // 0x01 = testo (unico valore in MVP)
    plaintext:      Vec<u8>,         // Corpo del qub UTF-8
    sender_label:   Option<String>,  // Nome visualizzato decorativo; non autenticato
    status:         DraftStatus,     // Composing | Sealed | Uploaded | Failed
}

2.2 QubEnvelope (payload decifrato)

Serializzato utilizzando CBOR canonico (§3). Cifrato all'interno del SealedQub. Questa è la struttura che dimostra l'integrità del contenuto dopo la decifratura.

QubEnvelope {
    version:             u8,              // Versione major del protocollo (0x01 per v1)
    qub_id:              [u8; 32],        // Derivato (vedi §4.1)
    content_type:        u8,              // Registro dei tipi di contenuto (vedi §6)
    created_at:          i64,             // Secondi Unix UTC
    unlock_at:           i64,             // Secondi Unix UTC
    outcome_at:          Option<i64>,     // V1.1 — quando la realtà emette il verdetto (verdict-uplift-plan §3.1)
    sender_label:        Option<String>,  // Decorativo; non autenticato in MVP
    reply_to:            Option<[u8; 32]>,// qub_id padre per catene di risposte; non nel preimage di qub_id; non firmato (vedi §9.3)
    body:                Vec<u8>,         // Payload del contenuto (UTF-8 per testo, CBOR per patto)
    body_hash:           [u8; 32],        // SHA3-256(body) (vedi §4.2)
    sig_alg:             u8,              // Algoritmo di firma (vedi §9.2)
    author_signature:    Option<Vec<u8>>, // Impostato quando sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Impostato quando sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Impostato per accordi bilaterali di patto cofirmati
    cosigner_signature:  Option<Vec<u8>>, // Impostato per accordi bilaterali di patto cofirmati
}

Baseline (qub testuale non firmato): version = 0x01, content_type = 0x01, sig_alg = 0x00, tutti i campi Option assenti.

Altre configurazioni v1: content_type = 0x03 (corpo di patto, vedi §6.1); sig_alg = 0x01 (ML-DSA-65) con author_signature e author_pubkey presenti (vedi §9.3); cosigner_pubkey e cosigner_signature presenti insieme per patti cofirmati (vedi §9.7); reply_to impostato sul qub_id del qub padre per qub di catena di risposte (vedi §9.3 per le implicazioni sull'ambito della firma).

2.3 SealedQub (formato wire canonico)

Serializzato utilizzando CBOR canonico (§3). Scritto nell'archivio permanente. Questo è l'artefatto on-chain.

SealedQub {
    version:           u8,              // Versione major del protocollo (0x01 per v1)
    qub_id:            [u8; 32],        // Uguale a QubEnvelope.qub_id
    visibility:        u8,              // 0x01 = pubblico; i visualizzatori v1 rifiutano altri valori
    unlock_at:         i64,             // Secondi Unix UTC
    outcome_at:        Option<i64>,     // V1.1 — esposto sulla CTA di verdict-watch
                                        //   prima della rivelazione; rispecchia QubEnvelope.outcome_at;
                                        //   legato a qub_id tramite il preimage §4.1.
    drand_chain_id:    String,          // hash della catena drand (stringa esadecimale)
    drand_round:       u64,             // Numero del drand round target
    tlock_ciphertext:  Vec<u8>,         // Byte CBOR del QubEnvelope cifrati tlock
    recipient_pubkey:  Option<[u8; 32]>,// Campo riservato; accettato dal CBOR canonico
                                        //   ma non interpretato dal visualizzatore di riferimento v1
    title:             Option<String>,  // Titolo in chiaro mostrato sul conto alla rovescia
                                        //   del visualizzatore prima della rivelazione. Legato a qub_id
                                        //   tramite title_hash (§4.1). 1..=100 code point
                                        //   NFC, nessun carattere di controllo.
}

2.4 RevealedQub (stato dell'applicazione visualizzatore)

Non serializzato in CBOR. Locale all'app del visualizzatore. Costruito dopo decifratura e verifica riuscite.

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 — trasportato da QubEnvelope.outcome_at / SealedQub.outcome_at; alimenta il blocco di attesa del verdetto nella pagina di rivelazione (verdict-uplift-plan §5.1)
    drand_chain_id:      String,
    drand_round:         u64,
    sender_label:        Option<String>,
    title:               Option<String>,    // Trasportato da 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. Profilo CBOR canonico

Tutta la serializzazione di SealedQub e QubEnvelope DEVE essere conforme a questo profilo. Due implementazioni dotate della stessa struttura logica DEVONO produrre byte identici.

3.1 Regole di codifica

Regola Specifica
Standard RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Ordinamento delle chiavi di mappa Ordinato prima per lunghezza in byte codificati (più corti prima dei più lunghi), poi lessicograficamente (byte per byte per codifiche della stessa lunghezza)
Codifica degli interi Forma più breve: 0–23 nel byte iniziale; 24–255 in 2 byte; 256–65535 in 3 byte; ecc.
Codifica della lunghezza Solo lunghezze definite. Nessun array, mappa, byte string o stringa di testo a lunghezza indefinita (additional info = 31 è vietato).
Tag Nessun tag CBOR (major type 6 è vietato).
Virgola mobile Nessun float (i valori 0xF9–0xFB del major type 7 sono vietati).
Stringhe di testo Codificate UTF-8, normalizzate NFC (Unicode Normalization Form C).
Byte string Byte grezzi. Nessuna codifica base64 a livello CBOR.
Chiavi duplicate Rifiuto con errore. I parser NON DEVONO accettare silenziosamente chiavi di mappa duplicate.
Valori semplici Solo true (0xF5), false (0xF4) e null (0xF6) sono ammessi.
Campi opzionali I campi opzionali assenti sono omessi completamente dalla mappa CBOR (non codificati come null). I campi opzionali presenti sono inclusi nell'ordine di chiavi ordinato.

3.2 Ordini di chiavi canonici verificati

Questi ordini di chiavi sono normativi. Le implementazioni DEVONO emettere le chiavi esattamente in quest'ordine. Le asserzioni di debug DOVREBBERO verificare l'ordinamento nelle build non-release.

QubEnvelope (version 0x01, non firmato, tutti i campi opzionali assenti):

"body"                (5 byte codificati)
"qub_id"              (7 byte codificati)
"sig_alg"             (8 byte codificati)
"version"             (8 byte codificati)
"reply_to"            (9 byte codificati)   ← solo se presente (catene di risposta)
"body_hash"           (10 byte codificati)
"unlock_at"           (10 byte codificati)
"created_at"          (11 byte codificati)
"outcome_at"          (11 byte codificati)  ← solo se presente (meccanica del verdetto V1.1)
"content_type"        (13 byte codificati)
"sender_label"        (13 byte codificati)  ← solo se presente
"author_pubkey"       (14 byte codificati)  ← solo se presente
"cosigner_pubkey"     (16 byte codificati)  ← solo se presente (cofirma di patto)
"author_signature"    (17 byte codificati)  ← solo se presente
"cosigner_signature"  (19 byte codificati)  ← solo se presente (cofirma di patto)

Derivazione dell'ordine delle chiavi di QubEnvelope: ogni chiave è una stringa di testo CBOR. Lunghezza codificata = 1 byte di header + lunghezza della stringa (per stringhe sotto i 24 byte). Ordinare prima per lunghezza codificata totale, poi lessicograficamente per chiavi della stessa lunghezza.

SealedQub (version 0x01, pubblico, nessun destinatario):

"title"             (6 byte codificati)   ← solo se presente
"qub_id"            (7 byte codificati)
"version"           (8 byte codificati)
"unlock_at"         (10 byte codificati)
"outcome_at"        (11 byte codificati)  ← solo se presente (meccanica del verdetto V1.1)
"visibility"        (11 byte codificati)
"drand_round"       (12 byte codificati)
"drand_chain_id"    (15 byte codificati)
"recipient_pubkey"  (17 byte codificati)  ← solo se presente
"tlock_ciphertext"  (17 byte codificati)

PactTerms (corpo del patto, content_type 0x03):

"notes"         (6 byte codificati)  ← solo se presente
"terms"         (6 byte codificati)
"title"         (6 byte codificati)
"party_a"       (8 byte codificati)
"party_b"       (8 byte codificati)
"pact_version"  (13 byte codificati)

PactTerm (riga dell'array terms):

"key"    (4 byte codificati)
"value"  (6 byte codificati)

PartyIdentifier (mappa party_a / party_b):

"label"    (6 byte codificati)
"contact"  (8 byte codificati)  ← solo se presente

3.3 Riferimento di codifica dei byte

Tipo Codifica CBOR Esempio
Hash SHA3-256 (32 byte) 0x58 0x20 + 32 byte body_hash, qub_id
Timestamp (i64) Major type 0 (positivo) o 1 (negativo), codifica più breve Secondi Unix
Versione (u8, valore 1) 0x01 (singolo byte)
Tipo di contenuto (u8, valore 1) 0x01 (singolo byte)
sig_alg (u8, valore 0) 0x00 (singolo byte)
Firma ML-DSA-65 (3.309 byte) 0x59 0x0C 0xED + 3.309 byte author_signature, cosigner_signature
Chiave pubblica ML-DSA-65 (1.952 byte) 0x59 0x07 0xA0 + 1.952 byte author_pubkey, cosigner_pubkey

4. Derivazioni normative

4.1 qub_id

Il qub_id identifica in modo univoco un qub e lega il QubEnvelope al SealedQub. È derivato deterministicamente dal contenuto dell'envelope.

qub_id = SHA3-256(
    "QUB_ID_V2"    ||    // separatore di dominio: byte ASCII [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 byte) + padding 0x00 (1 byte) = 10 byte
    version        ||    // u8 (1 byte)
    content_type   ||    // u8 (1 byte)
    created_at     ||    // i64 big-endian (8 byte)
    unlock_at            ||  // i64 big-endian (8 byte)
    outcome_at_or_zero   ||  // i64 big-endian (8 byte; 0 quando outcome_at è assente)
    drand_round          ||  // u64 big-endian (8 byte)
    body_hash            ||  // [u8; 32] (32 byte)
    title_hash               // [u8; 32] (32 byte; sentinella di assenza = [0u8; 32])
)
// Preimage totale: 108 byte → output di 32 byte

Codifica del separatore di dominio: La stringa "QUB_ID_V2" è 9 byte ASCII. Un singolo byte di padding 0x00 viene aggiunto per raggiungere 10 byte di allineamento. Le implementazioni DEVONO utilizzare esattamente questi 10 byte: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Codifica di outcome_at: la versione V1.1 ha esteso il preimage da 92 a 100 byte per integrare il campo opzionale outcome_at nel binding. L'assenza di outcome_at è codificata come 8 byte zero; i validatori del protocollo rifiutano ovunque outcome_at <= 0, quindi questa sentinella non può collidere con un valore legittimo. Vedi §3.2 (formato wire) e il documento in-tree tasks/verdict-uplift-plan.md per la meccanica del verdetto che motiva questo campo.

Codifica di drand_round: la versione V1.2 ha esteso il preimage da 100 a 108 byte per integrare drand_round (il drand round di destinazione, §4.3) nel binding, e ha portato il separatore di dominio a QUB_ID_V2. Questo lega il round del timelock all'identità del qub: un gateway non può rilegare il ciphertext a un round diverso (per esempio già passato) da quello implicato dall'unlock_at mostrato. La procedura di sblocco (§8) verifica inoltre che il round inserito nella stanza del ciphertext tlock corrisponda a unlock_round(unlock_at), così l'unlock time mostrato è in modo dimostrabile il round che governa la decifratura.

Proprietà:

4.2 body_hash

body_hash = SHA3-256(body)

Dove body è il payload del contenuto Vec<u8> grezzo. Per qub testuali, è il corpo del qub codificato in UTF-8.

4.2.1 title_hash

title_hash = SHA3-256(NFC(title).utf8_bytes)   se title è presente
title_hash = [0u8; 32]                         se title è assente

Dove title è il titolo in chiaro opzionale mostrato nel conto alla rovescia del visualizzatore prima della rivelazione (vedi §3.2). La normalizzazione NFC viene eseguita al momento dell'hash in modo che il digest sia stabile tra sequenze di code point visivamente equivalenti. La sentinella di tutti zeri è riservata per il caso di assenza; una stringa vuota viene rifiutata al confine CBOR canonico come codifica non canonica di "assente" (la codifica canonica omette completamente il campo).

4.3 Mapping del round di sblocco

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
Parametro Fonte Esempio
unlock_at Secondi Unix UTC scelti dall'utente 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time info della catena drand (genesis_time) 1595431050
chain_period_seconds info della catena drand (period) 30

L'operazione ceil() seleziona il primo drand round il cui reveal time è ≥ unlock_at. Questo garantisce che il qub non diventi decifrabile prima dell'unlock time scelto.

Caso limite: se (unlock_at - chain_genesis_time) è esattamente divisibile per chain_period_seconds, il risultato è esattamente quel round — il qub si sblocca precisamente al reveal time di quel round.

Validazione: unlock_at DEVE essere nel futuro al momento del sigillo. unlock_at NON DEVE superare i 10 anni da created_at (per limitare il rischio di dipendenza drand a lungo termine; l'UI DOVREBBE avvisare per date di sblocco oltre i 2 anni).


5. Newtype del formato wire

I newtype del formato wire forniscono sicurezza in fase di compilazione contro la confusione tra byte CBOR e JSON, plaintext grezzo o altre codifiche di byte.

Tipo Contiene Prodotto da Consumato da
SealedQubCbor CBOR canonico di SealedQub serialize_sealed_qub() Upload nell'archivio permanente, fetch del visualizzatore
QubEnvelopeCbor CBOR canonico di QubEnvelope serialize_qub_envelope() Input cifratura tlock, output decifratura tlock

5.1 Regole di costruzione

// Codice di produzione — solo tramite serializzatori CBOR:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);

// Non c'è deliberatamente alcuna implementazione From<Vec<u8>>.
// Non si possono accidentalmente avvolgere byte arbitrari in un tipo di formato wire.

// Accesso ai byte grezzi:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();

5.2 Validazione alla costruzione

from_encoded() DOVREBBE validare che l'input inizi con un header valido di mappa CBOR. La validazione strutturale completa avviene al momento del parsing, non al momento della costruzione, per evitare il doppio parsing.


6. Registro dei tipi di contenuto

Valore Tipo Dimensione massima del corpo Note
0x00 Riservato (non valido) NON DEVE essere utilizzato
0x01 Testo semplice (UTF-8, Markdown ristretto) 50 KB a pagamento / 10 KB gratuito Vedi §10 per le regole di rendering. La separazione gratuito / a pagamento è imposta dal servizio di upload; il limite massimo a livello di protocollo è 50 KB.
0x02 Riservato (futuro) Allocato per un tipo di contenuto futuro; non valido in v1. I visualizzatori DEVONO rifiutare secondo la regola sottostante.
0x03 Patto (accordo bilaterale, corpo CBOR) 100 KB Il corpo è un PactTerms CBOR canonico (§6.1). Firma del cofirmatario secondo §9.7.
0x04 Verdetto (autovalutazione del creatore, corpo CBOR) 8 KB Il corpo è un VerdictBody CBOR canonico (§6.2). Emesso solo dall'intento verdict lato sistema. Il riferimento al qub genitore è sul tag Arweave Parent-Tx-Id, non nel corpo. Vedi verdict-uplift-plan §3.4.

I visualizzatori DEVONO rifiutare i tipi di contenuto sconosciuti con un errore chiaramente visibile all'utente. I visualizzatori NON DEVONO tentare di renderizzare tipi sconosciuti come testo.

6.1 Corpo del patto (content_type = 0x03)

Un corpo di patto è la codifica CBOR canonica di un valore PactTerms:

PactTerms {
    pact_version:  u8,                    // 0x01 per structured/v1
    title:         String,                // ≤ 200 byte, NFC
    terms:         Vec<PactTerm>,         // ≤ 20 righe
    party_a:       PartyIdentifier,       // iniziatore
    party_b:       PartyIdentifier,       // cofirmatario
    notes:         Option<String>,        // ≤ 5.000 byte, NFC; chiave assente se nessuna
}

PactTerm       { key: String (≤ 100), value: String (≤ 2.000) }   // NFC su entrambi i lati
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }

Gli ordini di chiavi CBOR canonici per tutte e tre le mappe sono indicati in §3.2. Il CBOR del patto serializzato totale NON DEVE superare i 100 KB (coincide con §6).

Discriminatore di schema. La prima riga in terms per un patto structured/v1 DEVE essere { key: "pact_schema", value: "structured/v1" }. Le righe senza questo marcatore sono patti "personalizzati" e non ricevono validazione strutturata o rendering consapevole dello schema.

Slot di riconoscimento congelati. I patti structured/v1 portano esattamente quattro righe di riconoscimento sotto queste chiavi:

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

Il value di ciascuna è una di otto stringhe inglesi congelate scelte dalla coppia (role, kind), dove role ∈ { seller, buyer, provider, client } e kind ∈ { standard, capacity }. Le stringhe stesse sono dati normativi del protocollo — le firme ML-DSA-65 di entrambe le parti si impegnano sui byte esatti tramite body_hash. NON sono localizzate; il corpo firmato è neutrale rispetto alla lingua. Qualsiasi modifica di formulazione richiede una nuova versione dello schema (structured/v2).

Le otto stringhe, la loro lookup (acknowledgement_for(role, kind)) e il razionale di ciascuna sono fissati dall'implementazione di riferimento. Le implementazioni conformi DEVONO emettere valori di riconoscimento byte-identici; i test golden-fixture sul body-hash SHA3-256 che coprono tutte e quattro le combinazioni di ruolo catturano qualsiasi deriva.

Ordine di visualizzazione del visualizzatore. Le stringhe di riconoscimento contengono frasi come "descritto sopra", che presuppongono che le righe di descrizione / ambito siano renderizzate prima dei riconoscimenti. I visualizzatori DEVONO renderizzare l'array terms nell'ordine CBOR; il riordino spezza la semantica del testo.

Contatto della controparte. Quando il contact della Party B è un indirizzo email valido, il servizio di upload del qub invia automaticamente un'email di invito alla revisione / cofirma al momento dello staging e lega l'eventuale cofirma alla verifica dello stesso indirizzo (§9.7). I patti il cui contatto della Party B è assente possono comunque essere cofirmati, ma solo tramite un canale fuori banda — il servizio rifiuta le richieste di cofirma che non possono produrre un marker di verifica email corrispondente di 15 minuti.

6.2 Corpo del verdetto (content_type = 0x04)

Un corpo di verdetto è la codifica CBOR canonica di un valore VerdictBody:

VerdictBody {
    verdict_version: u8,                  // 0x01 per structured/v1
    outcome:         u8,                  // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
    reflection:      Option<String>,      // ≤ 2.000 byte NFC; "cosa è cambiato, cosa hai imparato"
    evidence_url:    Option<String>,      // ≤ 2.048 byte; solo HTTPS; chiave assente quando omesso
}

Ordine canonico delle chiavi CBOR:

"outcome"          (8 byte codificati)
"reflection"       (11 byte codificati)  ← solo se presente
"evidence_url"     (13 byte codificati)  ← solo se presente
"verdict_version"  (16 byte codificati)

Il CBOR del verdetto serializzato totale NON DEVE superare gli 8 KB (coincide con la riga del registro qui sopra).

Enum di esito. Il byte sul wire è neutrale rispetto all'intento; le quattro categorie Right / Partial / Wrong / Unfalsifiable coprono lo spazio degli esiti di ogni intento che porta un verdetto. Le etichette specifiche per intento ("L'ho indovinata" / "L'ho mantenuto" / "Consegnato" / "Confermata" per Right, ecc.) sono una preoccupazione di rendering lato visualizzatore risolta rispetto all'intento del qub genitore — il wire resta neutrale rispetto alla lingua e all'intento. I valori al di fuori di 1..=4 DEVONO essere rifiutati in fase di decodifica.

Collegamento al qub genitore. Un qub di verdetto NON porta il riferimento al qub genitore nel proprio corpo. L'identificatore di transazione Arweave del qub genitore è emesso come tag di archiviazione Parent-Tx-Id al momento dell'upload (§7, livello dei tag di archiviazione). Questo mantiene il corpo una dichiarazione di autovalutazione firmata e autocontenuta; la catena di audit ("avere ragione su cosa?") è stabilita tramite la lookup sul tag Arweave.

Sicurezza dell'URL delle prove (normativa). Quando evidence_url è presente, i validatori (lato compose, lato wire, edge del Worker) DEVONO imporre:

  1. Solo HTTPS. La stringa DEVE iniziare con la sequenza di byte https://. Qualsiasi altro schema — http, ftp, javascript, data, file, ecc. — è rifiutato.
  2. Limite di lunghezza. ≤ 2.048 byte (limite pratico degli URL del browser).
  3. Controllo NFC + codepoint ostili. Stessa regola di title e reflection — i codepoint bidi-override / a larghezza zero / del blocco tag / BOM / C0 / C1 sono rifiutati. La definizione coincide con crate::handle::contains_hostile_text_codepoint in Rust e con workers/api/src/utils/unicode.ts::isHostileCodepoint in TS (da tenere allineati).
  4. Nessuno spazio bianco, nessun carattere di controllo ASCII. Spazi bianchi / DEL / byte sotto 0x20 ovunque nell'URL sono rifiutati — chiude il vettore di iniezione \n/\t che la regola bidi non copre.
  5. Segmento host non vuoto. Tutto ciò che si trova tra https:// e il primo /, ? o # DEVE essere non vuoto.

Nessun fetch lato server. Il Worker NON DEVE fare da proxy, fetch o anteprima dell'URL. Il protocollo memorizza una stringa; il rendering avviene lato visualizzatore con rel="nofollow noopener noreferrer" target="_blank" e un host visibile mostrato accanto al testo del link.

Riflessione. Testo di riflessione facoltativo scritto dal creatore ("cosa è cambiato, cosa hai imparato"). Stessa validazione NFC + codepoint ostili di title. Input vuoto / fatto di soli spazi bianchi si comprime ad assente al momento della costruzione.

Versione di schema. v1 supporta solo verdict_version = 0x01. Le revisioni future di schema incrementano questo byte e arrivano insieme a una nuova versione del protocollo per §12.


7. Protocollo di sigillo

La sequenza completa di sigillo. Ogni passo è normativo.

 1. L'utente compone plaintext e metadati in ComposeQub.
 2. Validare:
    a. body è non vuoto.
    b. body size ≤ max per content_type e tier utente (vedi §6).
    c. unlock_at è nel futuro.
    d. unlock_at ≤ created_at + 10 anni.
    e. content_type è un valore noto e supportato.
 3. Calcolare body_hash = SHA3-256(body).
 4. Impostare created_at = secondi Unix UTC correnti.
 5. Selezionare la catena drand. Caricare chain_genesis_time e chain_period_seconds, e
    calcolare drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
    (Calcolato qui, prima di qub_id, perché drand_round è legato nel preimage
    di qub_id — §4.1, V1.2.)
 6. Calcolare qub_id (vedi §4.1), integrando drand_round dal passo 5.
 7. Costruire QubEnvelope con tutti i campi.
 8. Serializzare QubEnvelope utilizzando CBOR canonico → byte B.
    Asserire: l'output serializzato corrisponde al profilo canonico (§3).
 9. Calcolare C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Costruire SealedQub con tlock_ciphertext = C, e corrispondenti qub_id, version,
    unlock_at, drand_chain_id, drand_round.
12. Serializzare SealedQub utilizzando CBOR canonico → SealedQubCbor.
12a. Generare K = 32 byte casuali (CSPRNG) e N = 12 byte casuali (CSPRNG).
     Calcolare W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
     secondo §13. I byte caricati su archivio permanente sono il CBOR OuterWrapper W,
     mai il SealedQubCbor nudo. K lascia il dispositivo solo come frammento URL
     nel passo 16.
13. Mostrare la disclosure al momento del sigillo. L'utente conferma.
14. Validare l'idoneità per il caricamento tramite il servizio di upload del qub (bot-detection, entitlement, rate limit).
15. Inviare W (i byte OuterWrapper) al servizio di upload del qub; il servizio
    firma e carica su archivio permanente. Il servizio è byte-blind rispetto al SealedQubCbor
    interno e non riceve mai K.
16. Ricevere arweave_tx_id dal servizio. Costruire il link di consegna come
    `<origin>/c/<arweave_tx_id>#<base64url(K)>` (oppure `<origin>/s/<short_code>#<base64url(K)>`
    quando viene assegnato un codice corto). I browser non trasmettono i frammenti URL
    ai server, quindi K non viene mai osservato da qub.social o da qualsiasi
    gateway di archiviazione.

Livello dei tag dell'archivio (fuori banda). Il servizio di upload del qub allega un insieme deliberatamente piccolo di tag della transazione dell'archivio insieme al payload avvolto. Content-Type=application/octet-stream è normativamente richiesto. Il servizio di riferimento allega inoltre tre tag opzionali quando il creatore sceglie di esporli: Intent (intento di composizione validato da allowlist — ad es. quote, reply, commitment), Author (fingerprint della pubkey §9.3 del creatore come hex minuscolo a 64 caratteri) e Parent-Tx-Id (ID della transazione dell'archivio del qub padre per catene di risposta, base64url a 43 caratteri).

Il tag Author è opt-in per qub: l'app creatore di riferimento lo allega solo quando l'utente abilita esplicitamente l'attribuzione pubblica al momento del sigillo. Quando il toggle è disattivato — il default — non viene scritto alcun tag Author e il qub è non attribuito sulla catena: nulla nell'archivio permanente collega l'upload all'handle del creatore, all'email o ad altri qub. Quando il toggle è attivato, il fingerprint Author risolve all'@handle scelto dal creatore tramite la catena di attestazione §9.5. Le relazioni di catena di risposta e Intent non sono identificative. Il wrapper esterno (§13) protegge il corpo interno dalla correlazione del ciphertext — impedendo a un harvester di riconoscere e decifrare massivamente upload a forma di qub dopo che il loro drand round è pubblicato.

Il servizio di riferimento intenzionalmente NON allega i tag App-Name, App-Version o Type: qualsiasi filtro a valore singolo del genere restituirebbe l'intero corpus di qub a una query GraphQL, il che è incompatibile con l'ambito di riservatezza solo-corpo del wrapper.

Un verificatore conforme NON DEVE dipendere da alcun tag dell'archivio per la verifica di terze parti §11; il body hash / qub_id / firma si impegnano solo sul CBOR interno, mai sull'insieme dei tag.


8. Protocollo di sblocco

La sequenza completa di sblocco. Ogni passo è normativo.

 1. Il visualizzatore apre il link di consegna. Estrarre arweave_tx_id dal path E
    K = base64url_decode(fragment) dal frammento dell'URL. Se il frammento
    è assente o malformato → mostrare "questo URL manca della sua chiave
    di decifratura" e fermarsi; il visualizzatore NON DEVE contattare il gateway
    di archiviazione senza K, poiché recuperare byte avvolti che il visualizzatore non può
    decifrare non serve a nulla e fa solo trapelare il tentativo di accesso.
 2. Controllare la denylist. Se tx_id è in denylist → mostrare il messaggio di blocco. Fermarsi.
 3. Recuperare i byte OuterWrapper da archivio permanente (con fallback multi-gateway).
 3a. Sciogliere il wrapper: parsare i byte come OuterWrapper (§13), verificare che
    il byte `version` del wrapper sia `0x01` e calcolare SealedQubCbor =
    unwrap_sealed_qub(OuterWrapper, key=K). Qualsiasi fallimento di autenticazione
    AEAD (K errata, ciphertext manomesso, qub_id-come-AAD scambiato,
    nonce scambiato) → mostrare "la chiave di decifratura di questo URL non corrisponde
    al qub memorizzato" e fermarsi. I fallimenti di autenticazione sono
    indistinguibili per il visualizzatore secondo §13.5.
 4. Parsare SealedQubCbor → SealedQub.
 5. Validare: SealedQub.version è nota (0x01). Rifiutare versioni sconosciute.
 6. Se tempo corrente < SealedQub.unlock_at → mostrare conto alla rovescia. Fare polling o attendere.
 6a. Controllo del binding del round (V1.2). Ricalcolare expected_round =
    ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
    Rifiutare a meno che SealedQub.drand_round == expected_round E il round inserito
    nella stanza del ciphertext tlock (letto tramite l'header age/tlock, senza firma
    richiesta) == expected_round. Il round della stanza è quello che governa
    effettivamente la decifratura; senza questo controllo un creatore malevolo
    potrebbe legare il ciphertext a un round già passato mostrando un conto alla
    rovescia futuro, così chiunque legga i byte memorizzati potrebbe decifrare prima
    di unlock_at. Le implementazioni senza identità di catena (mock di test) saltano
    questo controllo.
 7. Una volta che tempo corrente ≥ SealedQub.unlock_at:
    a. Recuperare la firma del drand round per SealedQub.drand_round dalla rete drand.
    b. Calcolare B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
 8. Parsare B → QubEnvelope.
 9. Validare che QubEnvelope.version sia nota.
10. Verificare: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
    Fallimento → errore di integrità.
11. Verificare: QubEnvelope.qub_id == SealedQub.qub_id.
    Fallimento → errore di integrità.
12. Verificare: QubEnvelope.unlock_at == SealedQub.unlock_at.
    Fallimento → errore di integrità.
13. Verificare: QubEnvelope.content_type è noto e renderizzabile.
    Valori noti: 0x01 (testo), 0x03 (patto). Sconosciuto → mostrare errore.
14. Se QubEnvelope.sig_alg != 0x00 → verificare la firma dell'autore (vedi §9.4).
15. Se cosigner_pubkey o cosigner_signature presenti → verificare il cofirmatario (vedi §9.7).
16. Renderizzare il contenuto utilizzando il renderer appropriato (vedi §10 per testo, §6 per patto).
17. Costruire RevealedQub per la visualizzazione.

9. Firma di autorialità

9.1 Razionale

I qub sono memorizzati nell'archivio permanente. Le firme di autorialità devono rimanere non falsificabili a tempo indeterminato, motivo per cui v1.0 utilizza lo schema post-quantistico ML-DSA-65 (FIPS 204) anziché uno schema classico la cui sicurezza potrebbe degradarsi entro la vita permanente del qub.

9.2 Registro degli algoritmi

sig_alg Schema Dimensione chiave Dimensione firma
0x00 Nessuna firma (non firmato)
0x01 ML-DSA-65 (FIPS 204) 1.952 byte 3.309 byte

I visualizzatori DEVONO rifiutare valori di sig_alg sconosciuti.

9.3 Costruzione del preimage firmato

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V1"  ||    // separatore di dominio (17 byte)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 byte)
    body_hash            ||    // [u8; 32] (32 byte)
    unlock_at            ||    // i64 big-endian (8 byte)
    0x00                       // u8 (1 byte): DEVE essere 0x00 in v1.0
)

// Preimage totale: 91 byte → hash di 32 byte

signature = Sign(author_secret_key, sig_input)

Separatore di dominio: "QUB_AUTHOR_SIG_V1" è 17 byte ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Nessun padding.

Byte finale: il 91° byte del preimage DEVE essere 0x00. L'implementazione di riferimento espone questo come la costante ORG_ID_PRESENT_INDIVIDUAL = 0x00 in crates/qub-core/src/signing.rs; i visualizzatori che ricostruiscono sig_input per la verifica DEVONO emettere lo stesso byte.

Ambito della firma — cosa è e cosa non è coperto. sig_input si impegna su quattro campi dell'envelope: version, qub_id, body_hash, unlock_at (più il separatore di dominio fisso e il byte org_id_present). Tre di quei quattro sono invarianti strutturali: qub_id è esso stesso derivato da version, content_type, created_at, unlock_at, outcome_at, drand_round e body_hash tramite il preimage §4.1, quindi qualsiasi modifica a tali campi produce un qub_id diverso e invalida la firma transitivamente. La superficie direttamente autenticata è quindi:

Campo Autenticato dalla firma Come
version Input diretto a sig_input
qub_id Input diretto
body_hash Input diretto
unlock_at Input diretto
content_type Transitivamente, tramite il preimage di qub_id
created_at Transitivamente, tramite il preimage di qub_id
outcome_at Transitivamente, tramite il preimage di qub_id
drand_round Transitivamente, tramite il preimage di qub_id (V1.2)
body Transitivamente, tramite body_hash = SHA3-256(body)
author_pubkey — (implicito) La chiave che ha verificato la firma è l'autore, per definizione
sender_label Testo solo per visualizzazione; modificabile senza rompere la firma
reply_to Puntatore di threading; modificabile senza rompere la firma
cosigner_pubkey / cosigner_signature Firmati indipendentemente sullo stesso sig_input (vedi §9.7)
drand_chain_id, tlock_ciphertext, visibility Campi SealedQub esterni, non all'interno dell'envelope — coperti dalle proprie invarianti strutturali (coerenza round / chain) ma non dalla firma dell'autore. (drand_round ora è legato in modo transitivo tramite il preimage di qub_id — vedi sopra.)

Implicazioni di sicurezza dei campi non autenticati.

Le implementazioni che mostrano sender_label o reply_to agli utenti finali DEVONO esporre l'identità autenticata (fingerprint della pubkey, attestazione) come segnale di identità primario, non l'etichetta.

9.4 Procedura di verifica

1. Leggere sig_alg dal QubEnvelope.
2. Se sig_alg == 0x00 → non firmato. Nessuna verifica. Mostrare "qub non firmato."
3. Se sig_alg è sconosciuto → rifiutare. Mostrare "schema di firma non riconosciuto."
4. Estrarre author_signature e author_pubkey. Se uno dei due è assente → errore di integrità.
5. Ricostruire sig_input utilizzando i campi del QubEnvelope (stessa formula di §9.3).
6. Verify(author_pubkey, sig_input, author_signature).
7. Se la verifica ha successo → mostrare "firmato da [fingerprint chiave]."
8. Se la verifica fallisce → mostrare "verifica della firma fallita."

La verifica della firma è l'operazione più costosa (in particolare ML-DSA-65). DOVREBBE essere eseguita dopo che tutti i controlli più economici (hash, qub_id, unlock_at) sono stati superati.

9.5 Attestazioni di identità

Le attestazioni di identità — la mappatura di author_pubkey a rivendicazioni di identità riconoscibili dall'uomo come un handle qub, un indirizzo email, un handle sociale o una credenziale passkey — sono un miglioramento progressivo lato visualizzatore e non sono richieste per la verifica della firma. I visualizzatori che risolvono attestazioni a un'identità di visualizzazione DEVONO applicare la precedenza:

handle > email > social > fingerprint

Il fallback al fingerprint è l'hex minuscolo di SHA3-256(author_pubkey); è sempre disponibile per qualsiasi qub firmato. I visualizzatori POSSONO abbreviarlo per la visualizzazione — il visualizzatore di riferimento rende qub: seguito dai primi e ultimi quattro byte (qub:<8 hex>…<8 hex>).

Un verificatore conforme può completare ogni controllo in §9.4 senza contattare l'API qub, senza alcuna rete oltre l'archivio permanente e drand, e senza alcuna lookup lato server. La risoluzione dell'attestazione è un passaggio separato best-effort eseguito solo dopo che la verifica della firma è riuscita.

9.6 Impatto sulla dimensione

Ed25519 ML-DSA-65
Firma 64 byte 3.309 byte
Chiave pubblica 32 byte 1.952 byte
Totale per qub 96 byte 5.261 byte
Delta costo di archiviazione (a ~$5/MB) ~$0,0005 ~$0,026

Per un qub testuale di 500–2.000 byte, ML-DSA-65 triplica all'incirca la dimensione memorizzata. Il costo assoluto è trascurabile.

9.7 Verifica del cofirmatario (accordi bilaterali di patto)

Per accordi bilaterali (content_type = 0x03), un secondo livello di firma dimostra che entrambe le parti hanno acconsentito agli stessi termini.

Campi dell'envelope:

Entrambi i campi DEVONO essere presenti insieme o entrambi assenti. Se è presente esattamente uno, i visualizzatori DEVONO segnalare un errore di integrità.

Procedura di verifica:

1. Se cosigner_pubkey assente e cosigner_signature assente → nessun cofirmatario. Fatto.
2. Se è presente esattamente uno → errore di integrità.
3. Verificare cosigner_pubkey != author_pubkey (impedire l'auto-cofirma).
   Fallimento → mostrare "la pubkey del cofirmatario deve differire dall'autore."
4. Ricostruire sig_input utilizzando la stessa formula di §9.3.
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Successo → mostrare "cofirmato da [fingerprint cofirmatario]."
7. Fallimento → mostrare "verifica della cofirma fallita."

Proprietà:

Gate di binding email (operativo). Quando un patto in staging porta un contatto email della Party B (§6.1), il servizio di upload del qub DEVE rifiutare la richiesta di cofirma a meno che non esista un marker di verifica email di breve durata corrispondente sia all'id di staging sia all'hash dell'email normalizzata di quel contatto. Il marker viene scritto da /api/v1/auth/verify quando il token magic-link porta uno staging_id e l'indirizzo verificato corrisponde a SHA-256(normalise_email(party_b.contact)) — dove normalise_email(addr) preserva il case della local-part e mette in minuscolo solo la parte di dominio (secondo RFC 5321 §2.3.11), e SHA-256 qui è l'hash NIST FIPS 180-4 (distinto dallo SHA3-256 utilizzato nelle derivazioni §4) — e scade 900 secondi (15 minuti) dopo l'emissione. Questo è un gate operativo anti-impersonificazione, NON parte della prova on-chain del qub — un verificatore di terze parti che replica §11 ha bisogno solo dell'archivio permanente e di drand, senza alcuna lookup lato server. Il marker esiste solo lato server e non fa mai parte del corpo firmato.

Impatto sulla dimensione (autore ML-DSA-65 + cofirmatario):

Componente Dimensione
Firma dell'autore 3.309 byte
Chiave pubblica dell'autore 1.952 byte
Firma del cofirmatario 3.309 byte
Chiave pubblica del cofirmatario 1.952 byte
Overhead crittografico totale 10.522 byte
Delta costo di archiviazione ~$0,05

10. Rendering e sanitizzazione Markdown

Questa sezione è critica per la sicurezza. Il visualizzatore renderizza i qub testuali (content_type = 0x01) utilizzando un sottoinsieme ristretto di Markdown.

10.1 Elementi consentiti

10.2 Elementi vietati

Elemento Gestione
HTML grezzo (<div>, <script>, ecc.) Rimosso completamente. Nessun HTML passa.
Immagini (![alt](url)) Rimosse. La sintassi di immagine è eliminata dall'output.
Link ([text](url)) URL renderizzato come testo semplice visibile. Non auto-linkato. Non cliccabile senza azione esplicita dell'utente.
Schemi URL pericolosi javascript:, data:, vbscript:, file: — rimossi.
Iframe, embed, object Rimossi.
Entità HTML Decodificate in caratteri visualizzati solo se sicuri.

10.3 Implementazione

Le implementazioni DEVONO utilizzare un parser con allowlist rigorosa, non una blocklist. L'approccio raccomandato:

  1. Parsare Markdown utilizzando pulldown-cmark (o equivalente).
  2. Attraversare l'AST e scartare qualsiasi nodo non presente nell'allowlist (§10.1).
  3. Per i nodi link: emettere l'URL come testo visibile, non come elemento <a> cliccabile.
  4. Convertire l'AST filtrato in una rappresentazione intermedia tipizzata (ad es. un enum MarkdownNode con solo varianti sicure). L'HTML grezzo è strutturalmente non rappresentabile in questa IR.
  5. Renderizzare dalla IR tipizzata al livello di view target (ad es. componenti di view reattivi, nodi DOM). Nessuna concatenazione di stringhe HTML o innerHTML in alcun punto.

Gli approcci a blocklist sono fragili perché nuove estensioni Markdown o stranezze del parser possono introdurre elementi non filtrati. L'approccio AST tipizzato rende XSS strutturalmente impossibile — non c'è alcuna variante che possa trasportare HTML arbitrario.

10.4 Limiti di dimensione e struttura


11. Verifica di terze parti

Qualsiasi terza parte può verificare un qub pubblico senza cooperazione di qub. La procedura di verifica:

1. Ottenere arweave_tx_id (dal link di consegna o per conoscenza diretta).
2. Recuperare SealedQubCbor da qualsiasi gateway di archiviazione.
3. Confermare l'inclusione nel blocco di archiviazione (altezza del blocco, timestamp del blocco).
4. Parsare SealedQubCbor → SealedQub.
5. Recuperare la firma del drand round per SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → byte CBOR del QubEnvelope.
7. Parsare → QubEnvelope.
8. Verificare SHA3-256(body) == body_hash.
9. Verificare QubEnvelope.qub_id == SealedQub.qub_id.
10. Verificare QubEnvelope.unlock_at == SealedQub.unlock_at.
11. Se sig_alg != 0x00: verificare author_signature (vedi §9.4).
12. Tutti i controlli passano → qub è verificato.

Cosa dimostra la verifica:

Prova Cosa stabilisce
Impegno Il ciphertext esisteva entro il timestamp del blocco dell'archivio.
Integrità Il corpo in plaintext corrisponde all'hash impegnato e non è stato alterato.
Tempistica Il contenuto era illeggibile fino al drand round, che corrisponde all'unlock time scelto (soggetto alle ipotesi di sicurezza di tlock e drand).

Cosa la verifica NON dimostra:

Non-prova Perché
Autorialità Il sender_label è decorativo. Senza sig_alg0x01, chiunque potrebbe aver sigillato questo contenuto.
Intento Il qub dimostra il contenuto e la tempistica, non ciò che il creatore intendeva soggettivamente.
Tempistica pre-evento L'inclusione nel blocco dell'archivio può ritardare l'upload effettivo di minuti. Il timestamp di impegno è il tempo del blocco, non il momento in cui l'utente ha premuto "sigilla".

12. Versionamento

12.1 Versione del protocollo

Il campo version (u8) sia in SealedQub sia in QubEnvelope identifica la versione major del protocollo.

12.2 Cronologia delle versioni

Versione Valore Descrizione
v1 0x01 qub testuali pubblici (content_type 0x01), accordi bilaterali di patto (0x03, schema structured/v1, autore + cofirmatario ML-DSA-65), tlock, SHA3-256

12.3 Compatibilità in avanti

Un visualizzatore v1 che incontra un QubEnvelope con chiavi di mappa CBOR opzionali sconosciute (chiavi non nell'ordine canonico §3.2) DOVREBBE ignorare quelle chiavi e procedere con la verifica utilizzando i campi noti. Ciò consente future aggiunte minori (ad es. nuovi metadati) senza richiedere un bump della versione major.

Un visualizzatore v1 che incontra sig_alg = 0x01 (ML-DSA-65) ma manca del supporto di verifica ML-DSA-65 DOVREBBE mostrare il contenuto del qub con un avviso "firma presente ma non verificabile", non rifiutare interamente il qub. L'implementazione di riferimento oggi rifiuta ogni valore di sig_alg diverso da 0x00 e 0x01 perché il registro v1 non contiene altro algoritmo valido — il rifiuto rigoroso e il soft-fail sono osservazionalmente identici finché non viene registrato un terzo algoritmo. Il comportamento di soft-fail sopra diventa load-bearing una volta che §9.2 ammette una nuova voce, e il visualizzatore di riferimento sarà aggiornato per fare soft-fail a quel punto.

12.4 Versione del wrapper esterno

L'OuterWrapper descritto in §13 porta il proprio byte version, indipendente da SealedQub.version e QubEnvelope.version. I due spazi di versione evolvono separatamente: una futura sostituzione simmetrica post-quantistica sicura aumenta il byte del wrapper senza toccare la versione del protocollo interno, e una futura aggiunta a livello di protocollo (ad es. un nuovo campo dell'envelope) aumenta la versione interna senza toccare il byte del wrapper.

OUTER_WRAPPER_VERSION_* Valore Algoritmo Stato
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM con nonce di 12 byte, tag di autenticazione di 16 byte, AAD legato a qub_id default v1
0x020xFF Riservato Futuro

I visualizzatori DEVONO rifiutare versioni del wrapper sconosciute con un errore chiaro. Il protocollo mantiene intenzionalmente stretto lo spazio della versione del wrapper finché non appare un driver di migrazione concreto (ad es. linee guida NIST a favore di un AEAD diverso); uno slot 0x02 sarà allocato nella stessa revisione che introduce l'algoritmo.


13. Wrapper di cifratura esterno

13.1 Razionale

I livelli del protocollo (QubEnvelope → tlock → SealedQub) rendono un qub sigillato bloccato nel tempo: il corpo è illeggibile finché unlock_at e la firma del drand round non sono pubblicati. Dopo lo sblocco, tuttavia, la firma del round è pubblica e la forma CBOR canonica di SealedQub è riconoscibile, quindi un harvester che ha indicizzato le transazioni dell'archivio permanente potrebbe decifrare in massa l'intero corpus dei qub.

Il wrapper di cifratura esterno chiude quel canale interponendo un ulteriore livello AEAD simmetrico tra il SealedQubCbor canonico e i byte scritti nell'archivio permanente. La chiave a 256 bit K vive solo nel frammento URL del link di consegna e sui dispositivi degli utenti; i browser non trasmettono i frammenti URL ai server, quindi qub.social, ogni gateway di archiviazione e ogni CDN davanti a uno qualsiasi di essi sono osservazionalmente ciechi a K. Ogni qub nell'archivio permanente è quindi un ciphertext opaco il cui plaintext è irrecuperabile senza l'URL che il creatore ha scelto di condividere.

Effetto netto:

13.2 Stratificazione

corpo plaintext                      ← QubEnvelope.body (§2.2)
  ↓ CBOR canonico (§3)
CBOR dell'envelope
  ↓ tlock encrypt al drand round (§7 passo 10)
tlock_ciphertext (dentro SealedQub) (§2.3)
  ↓ CBOR canonico (§3)
byte SealedQubCbor                   ← artefatto wire interno
  ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 passo 12a, questa sezione)
byte CBOR OuterWrapper               ← caricati su archivio permanente (§7 passo 15)

Il sigillo e lo sblocco a livello di protocollo (§7, §8) sono invariati al di sotto del confine del wrapper; il wrapper si attacca al call site di seal() e si stacca al call site di unlock().

13.3 Struttura dati OuterWrapper

struct OuterWrapper {
    version:    u8,           // 0x01, vedi §12.4
    qub_id:     [u8; 32],     // copiato dal SealedQub interno; AAD AEAD
    nonce:      [u8; 12],     // nonce AEAD a 96 bit
    ciphertext: Vec<u8>,      // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || tag di 16 byte
}

Invarianti dei campi.

Codifica CBOR. CBOR canonico secondo §3, con la stessa regola di ordinamento delle chiavi (ordinato per lunghezza in byte codificati crescente, poi lessicograficamente). Le quattro chiavi sono:

Chiave Byte codificati Ordine
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

Il primo byte del CBOR OuterWrapper è quindi l'header di mappa a lunghezza definita per una mappa di 4 voci (0xA4).

13.4 Binding AAD a qub_id

Il wrapper lega qub_id come additional authenticated data AEAD. Questa è la difesa strutturale load-bearing contro tre classi di attacco:

Attacco Difesa
Spostare il ciphertext sotto un campo qub_id diverso nel wrapper AAD non corrisponde → l'autenticazione AEAD fallisce
Mescolare il frammento URL del qub A con i byte dell'archivio del qub B AAD non corrisponde → l'autenticazione AEAD fallisce
Manomettere il campo qub_id del wrapper dopo l'upload AAD non corrisponde → l'autenticazione AEAD fallisce

Trasportare qub_id nel plaintext del wrapper non indebolisce significativamente l'immunità all'enumerazione — qub_id è esso stesso un hash SHA3-256 del preimage §4.1 senza preimage recuperabile dal digest, e un enumeratore che ha già raccolto i byte del wrapper non apprende nulla dal qub_id visibile che non potrebbe dedurre dall'esistenza stessa dell'upload.

13.5 Algoritmi di wrap e unwrap

wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
    richiedere K.len() == 32 e N.len() == 12 e Q.len() == 32
    C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
    // C include il tag di autenticazione di 16 byte alla fine
    return canonical_cbor_encode(OuterWrapper{
        version:    0x01,
        qub_id:     Q,
        nonce:      N,
        ciphertext: C,
    })

unwrap_sealed_qub(OuterWrapper bytes W, key K):
    richiedere K.len() == 32
    O := canonical_cbor_decode(W) as OuterWrapper
    richiedere O.version == 0x01           // §12.4
    P := AES_256_GCM_decrypt(
            key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
         )
    // qualsiasi fallimento AEAD → DECRYPT_FAILED, indistinguibile per il chiamante
    return P                            // P è il SealedQubCbor interno

Collasso della modalità di fallimento. K errata, nonce errato, mancata corrispondenza AAD e ciphertext manomesso producono tutti lo stesso errore DECRYPT_FAILED. Questa è una proprietà AEAD deliberata: distinguere la modalità di fallimento creerebbe un canale laterale che un attaccante remoto potrebbe sondare inviando wrapper malformati e cronometrando la risposta. Le implementazioni di riferimento DEVONO collassare tutti i fallimenti AEAD in un'unica forma di errore.

13.6 Materiale chiave e distribuzione

La chiave di wrapping K è un valore casuale uniforme di 256 bit generato per-qub da un CSPRNG. Le implementazioni di riferimento la traggono da:

Distribuzione: K DEVE essere codificata come base64 URL-safe (RFC 4648 §5, senza padding) e aggiunta al link di consegna come componente di frammento:

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

Il frammento non viene mai trasmesso ad alcun server da un browser conforme. I canali di recupero (indice di cronologia lato server, auto-invio email opt-in) che persistono il link di consegna completo — incluso il frammento — oltre il dispositivo dell'utente sono un trade-off esplicito contro la postura di crypto-shredding predefinita e DEVONO essere gestiti su consenso esplicito dell'utente.

Perdita del frammento. Se un utente perde il frammento URL e non ha alcun canale di recupero, il qub è illeggibile. Questo è il trade-off load-bearing del design e DEVE essere divulgato all'utente al momento del sigillo. L'MVP rafforza la disclosure al momento del sigillo con copy esplicito "salva questo URL" e un canale di recupero email verificata per gli utenti che si scelgono di aderire.

13.7 Fuori ambito per questa sezione

13.8 qub pubblici (omissione del wrapper)

Il wrapper esterno è opzionale a livello di consegna. Un creatore può sigillare un qub come pubblico, nel qual caso il SealedQubCbor canonico viene scritto direttamente nell'archivio permanente, senza livello OuterWrapper e senza chiave K:

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

Un qub pubblico è bloccato nel tempo ma non protetto dal link: resta illeggibile finché il suo drand round non viene pubblicato (il livello tlock è invariato), ma dopo lo sblocco chiunque possieda l'arweave_tx_id può decifrarlo — non è richiesto alcun frammento URL, perché non esiste alcuna K. Questo è il trade-off deliberato per le superfici che il server deve pilotare: le email di notifica di rivelazione, gli embed di terze parti e una SEO post-rivelazione più ricca hanno tutti bisogno di un link che funzioni senza un segreto che il server non detiene mai (§13.6).

Conseguenze di cui un produttore DEVE tenere conto:

Privato (avvolto) resta il default; pubblico è una scelta esplicita del creatore per singolo qub.


14. Vettori di test

14.1 Derivazione di 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   = assente
  drand_round  = 4695445  (= (1736294400 - 1595431050) / 30, parametri drand mainnet §14.2)
  body         = "Hello, future."  (UTF-8, 14 byte)
  title        = assente

Intermedio:
  body_hash  = SHA3-256("Hello, future.")
             = 76ab8b3f843c6ed4f2d0fd75b9f457b4
               ad49dd4450f9c22723ae430e3af3211d
  title_hash = [0u8; 32]   (title assente — sentinella §4.2.1)

Separatore di dominio (10 byte):
  [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]

Preimage (108 byte — V1.2):
  domain_separator   ||  // 10 byte
  0x01               ||  // version
  0x01               ||  // content_type
  0x0000000067748580 ||  // created_at come i64 big-endian (1735689600)
  0x00000000677DC000 ||  // unlock_at come i64 big-endian (1736294400)
  0x0000000000000000 ||  // outcome_at_or_zero (outcome_at assente)
  0x000000000047A595 ||  // drand_round come u64 big-endian (4695445)
  body_hash          ||  // 32 byte
  title_hash             // 32 byte (sentinella tutta-zeri; title assente)

Output atteso:
  qub_id = SHA3-256(preimage)
         = 3a9fcb31b750d985c262fada6d4f777f
           d6a28be831d941d85c131f5a4bbaf8a4

Le implementazioni DEVONO produrre valori di body_hash e qub_id identici per questo input. Questo vettore di test DOVREBBE essere il primo unit test scritto. I valori canonici sopra sono stati calcolati dall'implementazione di riferimento e DEVONO corrispondere bit per bit. Layout storici del preimage (pre-lancio — nessun qub attivo dipendeva da questi): il qub_id V1.0 da 92 byte era 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; il qub_id V1.1 da 100 byte (dopo l'integrazione di outcome_at_or_zero) era b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 integra drand_round e porta il separatore di dominio a QUB_ID_V2.

14.2 Mapping del round di sblocco

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

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

drand_round = 4675285

14.3 Round-trip CBOR canonico

Le implementazioni DEVONO verificare che serialize(parse(serialize(qub))) == serialize(qub) per tutti gli input validi. Questo è un property test, non un singolo vettore.

14.4 CBOR di 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        = assente

Ordine canonico delle chiavi CBOR (PactTerms):
  "notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)

Ordine canonico delle chiavi CBOR (PactTerm):
  "key"(4) < "value"(6)

Ordine canonico delle chiavi CBOR (PartyIdentifier):
  "label"(6) < "contact"(8)

I byte CBOR canonici e il body_hash SHA3-256 sono calcolati dall'implementazione di riferimento. Le implementazioni DEVONO produrre CBOR byte-identico per questo input.

Le implementazioni DEVONO inoltre verificare che serialize(parse(serialize(pact))) == serialize(pact) per tutti gli input PactTerms validi (property test).

14.5 Vettori cross-linguaggio del wrapper esterno

Il wrapper esterno (§13) ha una fixture canonica separata in crates/qub-core/tests/vectors/wrapper_v1.json. Ogni caso fissa una tupla (key, nonce, qub_id, sealed_cbor) come input hex opachi e asserisce uno specifico output expected_wrapper_hex. Entrambe le implementazioni di riferimento consumano lo stesso file JSON:

La fixture attualmente fissa tre casi:

Caso Copertura
basic-text-public Forma SealedQub realistica più piccola; nessun campo opzionale. Stabilisce la forma canonica del wrapper per un qub tipico di v1.0.
with-recipient-pubkey SealedQub con recipient_pubkey impostato (path di Phase 2). Diverso insieme di chiavi CBOR interno, diverso qub_id.
longer-body Corpo ~4 KiB — esercita prefissi di lunghezza CBOR multibyte sia all'interno dell'envelope interno sia del ciphertext esterno.

Le implementazioni DEVONO produrre expected_wrapper_hex byte-identico per gli input registrati. Rigenerare la fixture richiede QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors ed è riservato a modifiche di formato deliberate.


15. Governance del profilo crittografico (futuro)

Questa sezione è informativa per v1 e diventa normativa la prima volta che un secondo algoritmo entra in una delle primitive crittografiche di qub.

15.1 Postura corrente

Il protocollo v1 lega esattamente un algoritmo per primitiva:

I verificatori attualmente hard-codano lunghezze di chiave e firma per primitiva. Nessuna superficie di agility è esposta dal formato wire.

15.2 Forma prevista

Quando un secondo algoritmo entra nel protocollo, il verificatore sarà configurato per un CryptoProfile nominato (ad es. ExqubV1) che elenca l'insieme esatto di valori permessi per primitiva — sig_alg, catene drand, versioni del wrapper, tipi di contenuto. Il profilo è fissato al momento della verifica, mai negoziato in-band. Qualsiasi valore al di fuori del profilo attivo viene rifiutato.

Ciò garantisce che aggiungere ML-DSA-87 o attivare Ed25519 non possa indebolire retroattivamente le configurazioni esistenti dei verificatori: un verificatore v1 rimane un verificatore v1 anche dopo che un profilo v2 è pubblicato.

15.3 Condizioni di trigger

Promuovere §15 a stato normativo quando è proposto uno dei seguenti:

Fino ad allora §15 è un segnaposto che fissa la forma della migrazione in modo che future PR atterrino contro un target noto piuttosto che ri-litigare la superficie di negoziazione da zero.