Especificação do protocolo qub

qub é um protocolo de compromissos temporais criptográficos: um sistema para selar palavras a uma data futura e provar, quando essa data chegar, exatamente o que foi dito e quando.

Três primitivas o tornam possível. drand é um beacon de aleatoriedade descentralizado — a data de revelação é imposta pela física, não pela boa vontade de qualquer parte. O armazenamento público e permanente é um repositório público à prova de adulteração — nenhuma parte pode editar ou apagar um qub depois de selado. ML-DSA-65 é uma assinatura digital pós-quântica — cada qub é vinculado a um par de chaves cujo segredo nunca deixa o dispositivo do autor.

Juntas, essas primitivas produzem uma declaração com bloqueio temporal, à prova de adulterações e atribuível — um recibo cujo valor cresce à medida que a capacidade do mundo de fabricar o passado melhora.

O restante deste documento é a especificação normativa exigida para implementações interoperáveis.


Especificação do protocolo qub

Campo Valor
Versão 1.0 (versão do protocolo 0x01, versão do wrapper externo 0x01)
Data 2026-05-01
Status Rascunho
Revisado até 2026-05-01

Este documento é a especificação normativa do protocolo para o sistema de compromisso temporal qub. Define as estruturas de dados, as regras de serialização, as fórmulas de derivação e os procedimentos de verificação exigidos para implementações interoperáveis.

Âmbito: a camada do protocolo é intencionalmente neutra em relação ao idioma — o corpo do qub é texto puro / markdown / bytes de pacto opacos, e a renderização localizada é responsabilidade do leitor (aplicação web qub.social, iframe <qub-embed>, clientes MCP, etc.).


1. Notação e convenções

Notação Significado
u8, u64, i64 Inteiros sem sinal / com sinal da largura de bits indicada
[u8; N] Arranjo de bytes de comprimento fixo de N bytes
Vec<u8> Arranjo de bytes de comprimento variável
Option<T> Valor do tipo T, ou ausente
String Cadeia de texto UTF-8, normalizada em NFC
`
SHA3-256(x) Hash NIST SHA3-256 da cadeia de bytes x (FIPS 202)
ceil(x) Função teto: o menor inteiro ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Byte mais significativo primeiro

Todos os inteiros nas construções de pré-imagem são codificados como arranjos de bytes big-endian de largura fixa (i64 → 8 bytes, u8 → 1 byte), salvo indicação em contrário.

Todos os timestamps são segundos Unix em UTC.


2. Estruturas de dados

2.1 ComposeQub (estado em memória do criador)

Não serializado em CBOR. Não gravado no armazenamento permanente. Local à aplicação do criador.

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 (carga útil decifrada)

Serializado usando CBOR canónico (§3). Criptografado dentro do SealedQub. É a estrutura que prova a integridade do conteúdo após a decifragem.

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
}

Linha de base (qub de texto sem assinatura): version = 0x01, content_type = 0x01, sig_alg = 0x00, todos os campos Option ausentes.

Outras configurações v1: content_type = 0x03 (corpo de pacto, ver §6.1); sig_alg = 0x01 (ML-DSA-65) com author_signature e author_pubkey presentes (ver §9.3); cosigner_pubkey e cosigner_signature presentes juntos para pactos co-assinados (ver §9.7); reply_to definido com o qub_id do qub pai para qubs de cadeia de respostas (ver §9.3 para as implicações no âmbito da assinatura).

2.3 SealedQub (formato canónico de wire)

Serializado usando CBOR canónico (§3). Gravado no armazenamento permanente. É o artefato 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 (estado da aplicação do leitor)

Não serializado em CBOR. Local à aplicação do leitor. Construído após decifragem e verificação bem-sucedidas.

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 — propagado de QubEnvelope.outcome_at / SealedQub.outcome_at; alimenta o bloco verdict-watch na página de revelação (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. Perfil canónico de CBOR

Toda serialização de SealedQub e QubEnvelope DEVE estar em conformidade com este perfil. Duas implementações, dada a mesma estrutura lógica, DEVEM produzir bytes idênticos.

3.1 Regras de codificação

Regra Especificação
Padrão RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Ordenação de chaves de mapa Ordenado primeiro por comprimento codificado em bytes (mais curto antes do mais longo), depois lexicograficamente (byte a byte para codificações de mesmo comprimento)
Codificação de inteiros Forma mais curta: 0–23 no byte inicial; 24–255 em 2 bytes; 256–65535 em 3 bytes; etc.
Codificação de comprimento Apenas comprimentos definidos. Nada de arrays, mapas, byte strings ou text strings com comprimento indefinido (additional info = 31 é proibido).
Tags Sem tags CBOR (major type 6 é proibido).
Ponto flutuante Sem floats (major type 7 valores 0xF9–0xFB são proibidos).
Text strings Codificadas em UTF-8, normalizadas em NFC (Unicode Normalization Form C).
Byte strings Bytes brutos. Nenhuma codificação base64 na camada CBOR.
Chaves duplicadas Rejeitar com erro. Os parsers NÃO DEVEM aceitar silenciosamente chaves duplicadas em mapas.
Valores simples Apenas true (0xF5), false (0xF4) e null (0xF6) são permitidos.
Campos opcionais Campos opcionais ausentes são omitidos inteiramente do mapa CBOR (não codificados como null). Os campos opcionais presentes são incluídos na ordem de chave ordenada.

3.2 Ordens canónicas verificadas de chaves

Estas ordens de chaves são normativas. As implementações DEVEM emitir as chaves exatamente nesta ordem. Asserções de depuração DEVERIAM verificar a ordenação em builds não-release.

QubEnvelope (versão 0x01, sem assinatura, todos os campos opcionais ausentes):

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

Derivação da ordem de chaves do QubEnvelope: cada chave é uma text string CBOR. Comprimento codificado = 1 byte de cabeçalho + comprimento da string (para strings com menos de 24 bytes). Ordene primeiro por comprimento codificado total e depois lexicograficamente para chaves de mesmo comprimento.

SealedQub (versão 0x01, público, sem destinatário):

"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 (corpo de pacto, 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 (linha do array terms):

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

PartyIdentifier (mapa party_a / party_b):

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

3.3 Referência de codificação de bytes

Tipo Codificação CBOR Exemplo
Hash SHA3-256 (32 bytes) 0x58 0x20 + 32 bytes body_hash, qub_id
Timestamps (i64) Major type 0 (positivo) ou 1 (negativo), codificação mais curta Segundos Unix
Versão (u8, valor 1) 0x01 (byte único)
Tipo de conteúdo (u8, valor 1) 0x01 (byte único)
sig_alg (u8, valor 0) 0x00 (byte único)
Assinatura ML-DSA-65 (3.309 bytes) 0x59 0x0C 0xED + 3.309 bytes author_signature, cosigner_signature
Chave pública ML-DSA-65 (1.952 bytes) 0x59 0x07 0xA0 + 1.952 bytes author_pubkey, cosigner_pubkey

4. Derivações normativas

4.1 qub_id

O qub_id identifica de forma única um qub e vincula o QubEnvelope ao SealedQub. É derivado deterministicamente do conteúdo do 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

Codificação do separador de domínio: A string "QUB_ID_V2" corresponde a 9 bytes ASCII. Um único byte 0x00 de preenchimento é acrescentado para totalizar 10 bytes para alinhamento. As implementações DEVEM usar exatamente estes 10 bytes: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Codificação de outcome_at: a V1.1 estendeu a pré-imagem de 92 para 100 bytes, de forma a integrar o campo opcional outcome_at na vinculação. A ausência de outcome_at é codificada como 8 bytes a zero; os validadores do protocolo rejeitam em todo o lado outcome_at <= 0, pelo que esta sentinela não pode colidir com um valor legítimo. Ver §3.2 (formato de wire) e o documento no repositório tasks/verdict-uplift-plan.md para o mecanismo de veredito que motiva este campo.

Codificação de drand_round: a V1.2 estendeu a pré-imagem de 100 para 108 bytes, de forma a integrar drand_round (o round drand alvo, §4.3) na vinculação, e elevou o separador de domínio para QUB_ID_V2. Isto vincula o round de bloqueio temporal à identidade do qub: um gateway não pode revincular o ciphertext a um round diferente (por exemplo, já passado) daquele que o unlock_at apresentado implica. O procedimento de desbloqueio (§8) verifica adicionalmente que o round embutido na stanza do ciphertext tlock corresponde a unlock_round(unlock_at), pelo que o instante de desbloqueio apresentado é, de forma comprovável, o round que controla a decifragem.

Propriedades:

4.2 body_hash

body_hash = SHA3-256(body)

Onde body é a carga útil de conteúdo Vec<u8> bruta. Para qubs de texto, é o corpo do qub codificado em 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

Onde title é o título opcional em texto puro exibido na contagem decrescente do leitor antes da revelação (ver §3.2). A normalização NFC é executada no momento do hash para que o digest seja estável entre sequências de code points visualmente equivalentes. O sentinela todo-zeros é reservado para o caso ausente; uma string vazia é rejeitada na fronteira do CBOR canónico como uma codificação não canónica de "ausente" (a codificação canónica omite o campo por completo).

4.3 Mapeamento entre instante de desbloqueio e round

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
Parâmetro Origem Exemplo
unlock_at Segundos Unix UTC escolhidos pelo utilizador 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time drand chain info (genesis_time) 1595431050
chain_period_seconds drand chain info (period) 30

A operação ceil() seleciona o primeiro round drand cujo instante de revelação é ≥ unlock_at. Isso garante que o qub não se torne decifrável antes do instante de desbloqueio escolhido.

Caso limite: se (unlock_at - chain_genesis_time) for exatamente divisível por chain_period_seconds, o resultado é precisamente esse round — o qub é desbloqueado exatamente no instante de revelação desse round.

Validação: unlock_at DEVE estar no futuro no momento do selamento. unlock_at NÃO DEVE estar mais de 10 anos além de created_at (para limitar o risco de dependência drand de longo prazo; a interface DEVERIA avisar para datas de desbloqueio além de 2 anos).


5. Newtypes do formato de wire

Os newtypes do formato de wire fornecem segurança em tempo de compilação contra a confusão de bytes CBOR com JSON, texto puro bruto ou outras codificações de bytes.

Tipo Contém Produzido por Consumido por
SealedQubCbor CBOR canónico do SealedQub serialize_sealed_qub() Upload ao armazenamento permanente, fetch do leitor
QubEnvelopeCbor CBOR canónico do QubEnvelope serialize_qub_envelope() Entrada do tlock encrypt, saída do tlock decrypt

5.1 Regras de construção

// 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 Validação na construção

from_encoded() DEVERIA validar que a entrada começa com um cabeçalho CBOR de mapa válido. A validação estrutural completa ocorre no momento do parse, não no momento da construção, para evitar parse duplicado.


6. Registro de tipos de conteúdo

Valor Tipo Tamanho máximo do corpo Notas
0x00 Reservado (inválido) NÃO DEVE ser usado
0x01 Texto puro (UTF-8, Markdown restrito) 50 KB pago / 10 KB grátis Ver §10 para regras de renderização. A divisão grátis / pago é imposta pelo serviço de upload; o teto rígido na camada do protocolo é 50 KB.
0x02 Reservado (futuro) Alocado para um tipo de conteúdo futuro; inválido na v1. Os leitores DEVEM rejeitar conforme a regra abaixo.
0x03 Pacto (acordo bilateral, corpo CBOR) 100 KB O corpo é PactTerms em CBOR canónico (§6.1). Co-assinatura por §9.7.
0x04 Veredito (autoavaliação do criador, corpo CBOR) 8 KB O corpo é VerdictBody em CBOR canónico (§6.2). Emitido apenas pela intenção do lado do sistema verdict. A relação com o pai está na tag Arweave Parent-Tx-Id, não no corpo. Ver verdict-uplift-plan §3.4.

Os leitores DEVEM rejeitar tipos de conteúdo desconhecidos com um erro claro visível ao utilizador. Os leitores NÃO DEVEM tentar renderizar tipos desconhecidos como texto.

6.1 Corpo de pacto (content_type = 0x03)

Um corpo de pacto é a codificação em CBOR canónico de um valor 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)> }

As ordens canónicas de chaves CBOR para os três mapas estão dadas em §3.2. O CBOR de pacto serializado total NÃO DEVE exceder 100 KB (corresponde a §6).

Discriminador de esquema. A primeira linha em terms para um pacto structured/v1 DEVE ser { key: "pact_schema", value: "structured/v1" }. Linhas sem esse marcador são pactos "custom" e não recebem validação estruturada nem renderização ciente do esquema.

Slots de reconhecimento congelados. Os pactos structured/v1 carregam exatamente quatro linhas de reconhecimento sob estas chaves:

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

O value para cada uma é uma de oito strings em inglês congeladas, escolhidas pelo par (role, kind), onde role ∈ { seller, buyer, provider, client } e kind ∈ { standard, capacity }. As strings em si são dados normativos do protocolo — as assinaturas ML-DSA-65 de ambas as partes se comprometem com os bytes exatos via body_hash. Elas NÃO são localizadas; o corpo assinado é neutro em relação ao idioma. Qualquer alteração de redação requer uma nova versão de esquema (structured/v2).

As oito strings, sua busca (acknowledgement_for(role, kind)) e a justificativa de cada uma são fixadas pela implementação de referência. Implementações conformes DEVEM emitir valores de reconhecimento idênticos byte a byte; testes de body-hash SHA3-256 com fixtures-padrão cobrindo as quatro combinações de papéis detectam qualquer drift.

Ordem de exibição no leitor. As strings de reconhecimento contêm frases como "described above", o que pressupõe que as linhas de descrição / âmbito sejam renderizadas antes dos reconhecimentos. Os leitores DEVEM renderizar o array terms na ordem CBOR; reordenar quebra a semântica do texto.

Contacto da contraparte. Quando o contact da Parte B é um endereço de e-mail válido, o serviço de upload do qub despacha automaticamente um e-mail de convite para revisão / co-assinatura no momento do staging e vincula a co-assinatura subsequente à verificação desse mesmo endereço (§9.7). Pactos cujo contacto da Parte B é ausente ainda podem ser co-assinados, mas apenas por um canal fora de banda — o serviço recusa pedidos de co-assinatura que não consigam produzir um marcador de verificação de e-mail correspondente de 15 minutos.

6.2 Corpo de veredito (content_type = 0x04)

Um corpo de veredito é a codificação em CBOR canónico de um valor 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
}

Ordem canónica das chaves CBOR:

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

O CBOR de veredito serializado total NÃO DEVE exceder 8 KB (corresponde à linha do registo acima).

Enumeração do desfecho. O byte na wire é neutro em relação à intenção; as quatro categorias Right / Partial / Wrong / Unfalsifiable cobrem o espaço de desfecho de cada intenção que comporta veredito. Rótulos por intenção («Acertei» / «Cumpri» / «Lançado» / «Confirmada» para Right, etc.) são uma preocupação de renderização do lado do leitor, resolvida em função da intenção do qub pai — a wire mantém-se neutra em relação ao idioma e à intenção. Valores fora de 1..=4 DEVEM ser rejeitados na descodificação.

Vinculação ao pai. Um qub de veredito NÃO carrega a referência ao pai no seu corpo. O id de transação Arweave do qub pai é emitido como a tag de armazenamento Parent-Tx-Id no momento do upload (camada de tags de armazenamento, §7). Isto mantém o corpo como uma declaração assinada autocontida de autoavaliação; a cadeia de auditoria («certo sobre quê?») estabelece-se via a consulta da tag Arweave.

Segurança do URL de evidência (normativo). Quando evidence_url está presente, os validadores (lado da composição, lado da wire, edge do Worker) DEVEM impor:

  1. Apenas HTTPS. A string DEVE começar pela sequência de bytes https://. Qualquer outro esquema — http, ftp, javascript, data, file, etc. — é rejeitado.
  2. Limite de comprimento. ≤ 2 048 bytes (limite prático de URL nos navegadores).
  3. Verificação de NFC + codepoints hostis. Mesma regra que title e reflection — codepoints de bidi-override / largura zero / tag-block / BOM / C0 / C1 são rejeitados. A definição corresponde à do Rust crate::handle::contains_hostile_text_codepoint e à do TS workers/api/src/utils/unicode.ts::isHostileCodepoint (manter sincronizados).
  4. Sem espaços em branco, sem controlos ASCII. Espaços em branco / DEL / bytes abaixo de 0x20 em qualquer ponto do URL são rejeitados — fecha o vetor de injeção de \n/\t que a regra de bidi não cobre.
  5. Segmento de anfitrião não vazio. Tudo entre https:// e o primeiro /, ? ou # DEVE ser não vazio.

Sem busca do lado do servidor. O Worker NÃO DEVE proxar, buscar nem pré-visualizar o URL. O protocolo armazena uma string; a renderização acontece do lado do leitor com rel="nofollow noopener noreferrer" target="_blank" e um anfitrião visível mostrado ao lado do texto do link.

Reflexão. Texto opcional de reflexão escrito pelo criador («o que mudou, o que aprendeu»). Mesma validação de NFC + codepoints hostis que title. Entrada vazia / só com espaços em branco recolhe-se para ausente no momento da construção.

Versão de esquema. A v1 suporta apenas verdict_version = 0x01. Revisões futuras de esquema incrementam este byte e chegam acompanhadas de uma nova versão de protocolo, conforme §12.


7. Protocolo de selamento

A sequência completa de selamento. Cada etapa é normativa.

 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.

Camada de tags de armazenamento (fora de banda). O serviço de upload do qub anexa um conjunto deliberadamente pequeno de tags de transação de armazenamento junto com o payload empacotado. Content-Type=application/octet-stream é normativamente exigido. O serviço de referência adicionalmente anexa três tags opcionais quando o criador escolhe expô-las: Intent (intenção de composição validada por allowlist — por exemplo, quote, reply, commitment), Author (impressão digital da pubkey §9.3 do criador como hex minúsculo de 64 caracteres) e Parent-Tx-Id (ID da transação de armazenamento do qub pai para cadeias de respostas, base64url de 43 caracteres).

A tag Author é opt-in por qub: a aplicação de referência do criador a anexa somente quando o utilizador habilita explicitamente a atribuição pública no momento do selamento. Quando o toggle está desligado — o padrão — nenhuma tag Author é gravada e o qub é não atribuído na cadeia: nada no armazenamento permanente vincula o upload ao @handle do criador, e-mail ou outros qubs. Quando o toggle está ligado, a impressão digital Author resolve-se ao @handle escolhido pelo criador via a cadeia de atestações §9.5. Os relacionamentos de cadeia de respostas e Intent não são identificadores. O wrapper externo (§13) protege o corpo interno contra correlação de ciphertext — impedindo que um harvester reconheça e decifre em massa uploads em formato de qub depois que seu round drand for publicado.

O serviço de referência intencionalmente NÃO anexa as tags App-Name, App-Version ou Type: qualquer filtro de valor único desse tipo retornaria todo o corpus de qubs a uma consulta GraphQL, o que é inconsistente com o âmbito de confidencialidade somente do corpo do wrapper.

Um verificador conforme NÃO DEVE depender de qualquer tag de armazenamento para a verificação de terceiros do §11; o body hash / qub_id / assinatura comprometem-se apenas com o CBOR interno, nunca com o conjunto de tags.


8. Protocolo de desbloqueio

A sequência completa de desbloqueio. Cada etapa é normativa.

 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. Assinatura de autoria

9.1 Justificativa

Os qubs são armazenados num armazenamento permanente. As assinaturas de autoria devem permanecer infalsificáveis indefinidamente, razão pela qual a v1.0 usa o esquema pós-quântico ML-DSA-65 (FIPS 204) em vez de um esquema clássico cuja segurança pode degradar ao longo da vida útil permanente do qub.

9.2 Registro de algoritmos

sig_alg Esquema Tamanho da chave Tamanho da assinatura
0x00 Sem assinatura (não assinado)
0x01 ML-DSA-65 (FIPS 204) 1.952 bytes 3.309 bytes

Os leitores DEVEM rejeitar valores sig_alg desconhecidos.

9.3 Construção da pré-imagem assinada

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)

Separador de domínio: "QUB_AUTHOR_SIG_V1" corresponde a 17 bytes ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. Sem padding.

Byte final: o 91º byte da pré-imagem DEVE ser 0x00. A implementação de referência o expõe como a constante ORG_ID_PRESENT_INDIVIDUAL = 0x00 em crates/qub-core/src/signing.rs; leitores que reconstroem sig_input para verificação DEVEM emitir o mesmo byte.

Âmbito da assinatura — o que está e o que não está coberto. sig_input se compromete com quatro campos do envelope: version, qub_id, body_hash, unlock_at (mais o separador de domínio fixo e o byte org_id_present). Três desses quatro são invariantes estruturais: qub_id em si é derivado de version, content_type, created_at, unlock_at, outcome_at, drand_round e body_hash via a pré-imagem do §4.1, então qualquer alteração nesses campos produz um qub_id diferente e invalida a assinatura transitivamente. A superfície diretamente autenticada é, portanto:

Campo Autenticado pela assinatura Como
version Entrada direta de sig_input
qub_id Entrada direta
body_hash Entrada direta
unlock_at Entrada direta
content_type Transitivamente, via pré-imagem de qub_id
created_at Transitivamente, via pré-imagem de qub_id
outcome_at Transitivamente, via pré-imagem de qub_id
drand_round Transitivamente, via pré-imagem de qub_id (V1.2)
body Transitivamente, via body_hash = SHA3-256(body)
author_pubkey — (implícito) A chave que verificou a assinatura é, por definição, o autor
sender_label Texto somente para exibição; mutável sem quebrar a assinatura
reply_to Ponteiro de threading; mutável sem quebrar a assinatura
cosigner_pubkey / cosigner_signature Assinados independentemente sobre o mesmo sig_input (ver §9.7)
drand_chain_id, tlock_ciphertext, visibility Campos do SealedQub externo, fora do envelope — cobertos por seus próprios invariantes estruturais (consistência de round / chain), mas não pela assinatura do autor. (drand_round agora está vinculado transitivamente via a pré-imagem de qub_id — ver acima.)

Implicações de segurança dos campos não autenticados.

Implementações que exibem sender_label ou reply_to para utilizadores finais DEVEM expor a identidade autenticada (impressão digital da pubkey, atestação) como sinal primário de identidade, não o rótulo.

9.4 Procedimento de verificação

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

A verificação de assinatura é a operação mais cara (especialmente ML-DSA-65). DEVERIA ser realizada após todas as verificações mais baratas (hash, qub_id, unlock_at) terem passado.

9.5 Atestações de identidade

As atestações de identidade — o mapeamento de author_pubkey para reivindicações de identidade reconhecíveis por humanos, como um handle qub, endereço de e-mail, handle social ou credencial passkey — são um aprimoramento progressivo do lado do leitor e não são exigidas para a verificação da assinatura. Os leitores que resolvem atestações para uma identidade exibida DEVEM aplicar a precedência:

handle > email > social > fingerprint

O fallback de fingerprint é o hex minúsculo de SHA3-256(author_pubkey); está sempre disponível para qualquer qub assinado. Os leitores PODEM abreviá-lo para exibição — o leitor de referência renderiza qub: seguido dos primeiros e últimos quatro bytes (qub:<8 hex>…<8 hex>).

Um verificador conforme pode concluir todas as verificações em §9.4 sem contatar a API qub, sem qualquer rede além do armazenamento permanente e do drand, e sem qualquer consulta do lado do servidor. A resolução de atestação é uma etapa separada de melhor esforço, realizada apenas após o sucesso da verificação da assinatura.

9.6 Impacto de tamanho

Ed25519 ML-DSA-65
Assinatura 64 bytes 3.309 bytes
Chave pública 32 bytes 1.952 bytes
Total por qub 96 bytes 5.261 bytes
Delta de custo de armazenamento (a ~$5/MB) ~$0,0005 ~$0,026

Para um qub de texto de 500–2.000 bytes, ML-DSA-65 aproximadamente triplica o tamanho armazenado. O custo absoluto é desprezível.

9.7 Verificação de co-assinante (acordos bilaterais de pacto)

Para acordos bilaterais (content_type = 0x03), uma segunda camada de assinatura prova que ambas as partes consentiram com os mesmos termos.

Campos do envelope:

Ambos os campos DEVEM estar presentes juntos ou ambos ausentes. Se exatamente um estiver presente, os leitores DEVEM relatar um erro de integridade.

Procedimento de verificação:

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

Propriedades:

Gate de vinculação de e-mail (operacional). Quando um pacto em staging carrega um contacto de e-mail de Parte B (§6.1), o serviço de upload do qub DEVE recusar a solicitação de co-assinatura, a menos que exista um marcador de verificação de e-mail de curta duração que combine tanto o id de staging quanto o hash de e-mail normalizado desse contacto. O marcador é gravado por /api/v1/auth/verify quando o token do magic-link carrega um staging_id e o endereço verificado coincide com SHA-256(normalise_email(party_b.contact)) — onde normalise_email(addr) preserva a caixa da parte local e converte para minúsculas apenas a parte do domínio (conforme RFC 5321 §2.3.11), e SHA-256 aqui é o hash NIST FIPS 180-4 (distinto do SHA3-256 usado nas derivações do §4) — e expira 900 segundos (15 minutos) após a emissão. Este é um gate operacional anti-impersonação, NÃO parte da prova on-chain do qub — um verificador de terceiros que reproduz o §11 precisa apenas do armazenamento permanente e do drand, sem qualquer consulta do lado do servidor. O marcador existe apenas no servidor e nunca faz parte do corpo assinado.

Impacto de tamanho (autor + co-assinante ML-DSA-65):

Componente Tamanho
Assinatura do autor 3.309 bytes
Chave pública do autor 1.952 bytes
Assinatura do co-assinante 3.309 bytes
Chave pública do co-assinante 1.952 bytes
Sobrecarga cripto total 10.522 bytes
Delta de custo de armazenamento ~$0,05

10. Renderização e sanitização de Markdown

Esta seção é crítica para a segurança. O leitor renderiza qubs de texto (content_type = 0x01) usando um subconjunto restrito de Markdown.

10.1 Elementos permitidos

10.2 Elementos proibidos

Elemento Tratamento
HTML bruto (<div>, <script>, etc.) Removido por completo. Nenhum HTML passa adiante.
Imagens (![alt](url)) Removidas. A sintaxe de imagem é eliminada da saída.
Links ([text](url)) A URL é renderizada como texto puro visível. Não auto-vinculada. Não clicável sem ação explícita do utilizador.
Esquemas de URL perigosos javascript:, data:, vbscript:, file: — removidos.
Iframes, embeds, objetos Removidos.
Entidades HTML Decodificadas para caracteres exibidos apenas se forem seguras.

10.3 Implementação

As implementações DEVEM usar um parser de allowlist estrito, não uma blocklist. A abordagem recomendada:

  1. Fazer o parse do Markdown usando pulldown-cmark (ou equivalente).
  2. Percorrer a AST e descartar qualquer nó fora da allowlist (§10.1).
  3. Para nós de link: emitir a URL como texto visível, não como elemento <a> clicável.
  4. Converter a AST filtrada em uma representação intermediária tipada (por exemplo, um enum MarkdownNode com apenas variantes seguras). O HTML bruto é estruturalmente irrepresentável nesta IR.
  5. Renderizar a partir da IR tipada para a camada de view de destino (por exemplo, componentes de view reativos, nós DOM). Nunca há concatenação de strings HTML ou innerHTML em ponto algum.

Abordagens de blocklist são frágeis porque novas extensões de Markdown ou peculiaridades de parser podem introduzir elementos não filtrados. A abordagem de AST tipada torna o XSS estruturalmente impossível — não há variante que possa carregar HTML arbitrário.

10.4 Limites de tamanho e estrutura


11. Verificação por terceiros

Qualquer terceiro pode verificar um qub público sem cooperação do qub. O procedimento de verificação:

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.

O que a verificação prova:

Prova O que estabelece
Compromisso O ciphertext existia até o timestamp do bloco de armazenamento.
Integridade O corpo em texto puro corresponde ao hash comprometido e não foi alterado.
Temporização O conteúdo era ilegível até o round drand, que corresponde ao instante de desbloqueio escolhido (sujeito às hipóteses de segurança do tlock e drand).

O que a verificação NÃO prova:

Não-prova Por quê
Autoria O sender_label é decorativo. Sem sig_alg0x01, qualquer pessoa poderia ter selado este conteúdo.
Intenção O qub prova conteúdo e temporização, não o que o criador queria dizer subjetivamente.
Temporização pré-evento A inclusão no bloco de armazenamento pode atrasar o upload real em minutos. O timestamp do compromisso é o instante do bloco, não o momento em que o utilizador clicou em "selar".

12. Versionamento

12.1 Versão do protocolo

O campo version (u8) tanto em SealedQub quanto em QubEnvelope identifica a versão maior do protocolo.

12.2 Histórico de versões

Versão Valor Descrição
v1 0x01 Qubs de texto público (content_type 0x01), acordos bilaterais de pacto (0x03, esquema structured/v1, autor + co-assinante ML-DSA-65), tlock, SHA3-256

12.3 Compatibilidade futura

Um leitor v1 que encontrar um QubEnvelope com chaves de mapa CBOR opcionais desconhecidas (chaves não presentes na ordem canónica do §3.2) DEVERIA ignorar essas chaves e prosseguir com a verificação usando os campos conhecidos. Isso permite adições menores futuras (por exemplo, novos metadados) sem exigir um bump de versão maior.

Um leitor v1 que encontre sig_alg = 0x01 (ML-DSA-65) mas não tenha suporte de verificação ML-DSA-65 DEVERIA exibir o conteúdo do qub com um aviso "assinatura presente, mas não verificável", em vez de rejeitar o qub por completo. A implementação de referência hoje rejeita todo valor de sig_alg exceto 0x00 e 0x01 porque o registro v1 não contém outro algoritmo válido — rejeição estrita e soft-fail são observacionalmente idênticas até que um terceiro algoritmo seja registrado. O comportamento de soft-fail acima passa a ser carregador uma vez que §9.2 admita uma nova entrada, e o leitor de referência será atualizado para soft-fail nesse momento.

12.4 Versão do wrapper externo

O OuterWrapper descrito em §13 carrega seu próprio byte version, independente de SealedQub.version e QubEnvelope.version. Os dois espaços de versão evoluem separadamente: uma futura substituição simétrica resistente ao quântico aumenta o byte do wrapper sem tocar na versão interna do protocolo, e uma futura adição na camada do protocolo (por exemplo, um novo campo do envelope) aumenta a versão interna sem tocar no byte do wrapper.

OUTER_WRAPPER_VERSION_* Valor Algoritmo Status
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM com nonce de 12 bytes, tag de autenticação de 16 bytes, AAD vinculada a qub_id padrão v1
0x020xFF Reservado Futuro

Os leitores DEVEM rejeitar versões de wrapper desconhecidas com um erro claro. O protocolo intencionalmente mantém o espaço de versões de wrapper estreito até que um motivador concreto de migração apareça (por exemplo, orientação NIST a favor de outro AEAD); um slot 0x02 será alocado na mesma revisão que introduzir o algoritmo.


13. Wrapper externo de cifragem

13.1 Justificativa

As camadas do protocolo (QubEnvelope → tlock → SealedQub) tornam um qub selado com bloqueio temporal: o corpo é ilegível até unlock_at e até que a assinatura do round drand tenha sido publicada. Após o desbloqueio, no entanto, a assinatura do round é pública e o formato canónico CBOR de SealedQub é reconhecível, então um harvester que indexasse transações de armazenamento poderia decifrar em massa todo o corpus de qubs.

O wrapper externo de cifragem fecha esse canal interpondo uma camada AEAD simétrica adicional entre o SealedQubCbor canónico e os bytes carregados ao armazenamento permanente. A chave de 256 bits K vive apenas no fragmento de URL do link de entrega e nos dispositivos do utilizador; navegadores não transmitem fragmentos de URL para servidores, então qub.social, todo gateway de armazenamento e toda CDN à frente de qualquer um deles é observacionalmente cego a K. Cada qub no armazenamento permanente é, portanto, um ciphertext opaco cujo texto puro é irrecuperável sem a URL que o criador escolheu partilhar.

Efeito líquido:

13.2 Camadas

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)

Selamento e desbloqueio na camada do protocolo (§7, §8) permanecem inalterados abaixo da fronteira do wrapper; o wrapper se conecta no ponto de chamada de seal() e se desconecta no ponto de chamada de unlock().

13.3 Estrutura de dados do 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
}

Invariantes dos campos.

Codificação CBOR. CBOR canónico conforme §3, com a mesma regra de ordenação de chaves (ordenado por comprimento codificado em bytes ascendente, depois lexicograficamente). As quatro chaves são:

Chave Bytes codificados Ordem
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

O primeiro byte do CBOR do OuterWrapper é, portanto, o cabeçalho de mapa de comprimento definido para um mapa de 4 entradas (0xA4).

13.4 Vinculação AAD ao qub_id

O wrapper vincula qub_id como dado autenticado adicional do AEAD. Esta é a defesa estrutural carregadora contra três classes de ataque:

Ataque Defesa
Mover ciphertext sob um campo qub_id diferente no wrapper Incompatibilidade de AAD → autenticação AEAD falha
Misturar o fragmento de URL do qub A com os bytes de armazenamento do qub B Incompatibilidade de AAD → autenticação AEAD falha
Adulterar o campo qub_id do wrapper após o upload Incompatibilidade de AAD → autenticação AEAD falha

Carregar qub_id no plaintext do wrapper não enfraquece a imunidade à enumeração de forma significativa — qub_id é em si um hash SHA3-256 da pré-imagem do §4.1 sem pré-imagem recuperável a partir do digest, e um enumerador que já tenha colhido os bytes do wrapper não aprende nada com o qub_id visível que não pudesse inferir da própria existência do upload.

13.5 Algoritmos de wrap e 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

Colapso de modos de falha. K errado, nonce errado, incompatibilidade de AAD e ciphertext adulterado produzem todos o mesmo erro DECRYPT_FAILED. Esta é uma propriedade AEAD deliberada: distinguir o modo de falha criaria um canal lateral que um atacante remoto poderia sondar enviando wrappers malformados e cronometrando a resposta. Implementações de referência DEVEM colapsar todas as falhas AEAD em um único formato de erro.

13.6 Material de chave e distribuição

A chave de empacotamento K é um valor uniforme aleatório de 256 bits gerado por qub por um CSPRNG. As implementações de referência o obtêm de:

Distribuição: K DEVE ser codificado como base64 URL-safe (RFC 4648 §5, sem padding) e anexado à URL de entrega como o componente de fragmento:

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

O fragmento nunca é transmitido a qualquer servidor por um navegador conforme. Canais de recuperação (índice de histórico do servidor, envio automático opt-in por e-mail) que persistem o link de entrega completo — incluindo o fragmento — além do dispositivo do utilizador são uma troca explícita contra a postura padrão de crypto-shredding e DEVEM ser condicionados a consentimento explícito do utilizador.

Perda de fragmento. Se um utilizador perder o fragmento da URL e não tiver canal de recuperação, o qub fica ilegível. Esta é a troca carregadora do design e DEVE ser divulgada ao utilizador no momento do selamento. O MVP reforça a divulgação no momento do selamento com uma cópia explícita "salve esta URL" e um canal de recuperação por e-mail verificado para utilizadores que optarem por participar.

13.7 Fora do âmbito desta seção

13.8 Qubs públicos (omissão do wrapper)

O wrapper externo é opcional na camada de entrega. Um criador pode selar um qub como público, caso em que o SealedQubCbor canónico é gravado no armazenamento permanente diretamente, sem camada OuterWrapper e sem chave K:

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

Um qub público tem bloqueio temporal, mas não controlo por link: permanece ilegível até que seu round drand seja publicado (a camada tlock não muda), mas após o desbloqueio qualquer um que tenha o arweave_tx_id pode decifrá-lo — nenhum fragmento de URL é necessário, porque não há K. Esta é a troca deliberada para as superfícies que o servidor precisa conduzir: e-mails de notificação de revelação, embeds de terceiros e um SEO pós-revelação mais rico precisam todos de um link que funcione sem um segredo que o servidor nunca detém (§13.6).

Consequências que um produtor DEVE considerar:

Privado (empacotado) permanece o padrão; público é uma escolha explícita do criador por qub.


14. Vetores de teste

14.1 Derivação de 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

As implementações DEVEM produzir valores idênticos de body_hash e qub_id para esta entrada. Este vetor de teste DEVERIA ser o primeiro teste unitário escrito. Os valores canónicos acima foram calculados pela implementação de referência e DEVEM corresponder bit a bit. Disposições históricas da pré-imagem (pré-lançamento — nenhum qub em produção dependia destas): o qub_id V1.0 de 92 bytes era 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; o qub_id V1.1 de 100 bytes (após integrar outcome_at_or_zero) era b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. A V1.2 integra drand_round e eleva o separador de domínio para QUB_ID_V2.

14.2 Mapeamento entre instante de desbloqueio e 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 de CBOR canónico

As implementações DEVEM verificar que serialize(parse(serialize(qub))) == serialize(qub) para todas as entradas válidas. Este é um teste de propriedade, não um vetor único.

14.4 PactTerms CBOR (content_type 0x03)

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

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

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

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

Os bytes CBOR canónicos e o body_hash SHA3-256 são calculados pela implementação de referência. As implementações DEVEM produzir CBOR idêntico em bytes para esta entrada.

As implementações TAMBÉM DEVEM verificar que serialize(parse(serialize(pact))) == serialize(pact) para todas as entradas PactTerms válidas (teste de propriedade).

14.5 Vetores cross-language do wrapper externo

O wrapper externo (§13) tem uma fixture canónica separada em crates/qub-core/tests/vectors/wrapper_v1.json. Cada caso fixa uma tupla (key, nonce, qub_id, sealed_cbor) como entradas hex opacas e afirma uma saída específica expected_wrapper_hex. Ambas as implementações de referência consomem o mesmo ficheiro JSON:

A fixture atualmente fixa três casos:

Caso Cobertura
basic-text-public Forma realista mais simples de SealedQub; nenhum campo opcional. Estabelece a forma canónica do wrapper para um qub típico v1.0.
with-recipient-pubkey SealedQub com recipient_pubkey definido (caminho da Fase 2). Conjunto de chaves CBOR interno diferente, qub_id diferente.
longer-body Corpo de ~4 KiB — exercita prefixos de comprimento CBOR de múltiplos bytes tanto no envelope interno quanto no ciphertext externo.

As implementações DEVEM produzir expected_wrapper_hex byte-idêntico para as entradas registradas. A regeneração da fixture requer QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors e está reservada para mudanças deliberadas de formato.


15. Governança do perfil de cifragem (futuro)

Esta seção é informativa para a v1 e torna-se normativa na primeira vez que um segundo algoritmo entrar em qualquer uma das primitivas criptográficas do qub.

15.1 Postura atual

O protocolo v1 vincula exatamente um algoritmo por primitiva:

Os verificadores atualmente fixam no código os comprimentos de chave e de assinatura por primitiva. Nenhuma superfície de agilidade é exposta pelo formato de fio.

15.2 Forma pretendida

Quando um segundo algoritmo entrar no protocolo, o verificador será configurado para um CryptoProfile nomeado (por exemplo, ExqubV1) que lista o conjunto exato de valores permitidos por primitiva — sig_algs, chains drand, versões de wrapper, tipos de conteúdo. O perfil é fixado no momento da verificação, nunca negociado em banda. Qualquer valor fora do perfil ativo é rejeitado.

Isso garante que adicionar ML-DSA-87 ou ativar Ed25519 não pode enfraquecer retroativamente configurações de verificador existentes: um verificador v1 permanece um verificador v1 mesmo depois que um perfil v2 é publicado.

15.3 Condições de ativação

Promova o §15 a status normativo quando qualquer uma das seguintes situações for proposta:

Até então, o §15 é um marcador que fixa a forma de migração para que PRs futuros aterrissem contra um alvo conhecido em vez de re-litigar a superfície de negociação do zero.