Spécification du protocole qub

qub est un protocole d’engagement temporel cryptographique : un système pour sceller des mots à une date future et prouver, le moment venu, exactement ce qui a été dit et quand.

Trois primitives le rendent possible. drand est une balise de hasard décentralisée — la date de dévoilement est imposée par la physique, pas par la bonne volonté d’une partie. Le stockage public permanent est un magasin public inviolable — aucune partie ne peut modifier ou supprimer un qub une fois scellé. ML-DSA-65 est une signature numérique post-quantique — chaque qub est rattaché à une paire de clés dont le secret ne quitte jamais l’appareil de l’auteur.

Ensemble, ces primitives produisent une déclaration verrouillée dans le temps, infalsifiable et attribuable — un reçu dont la valeur croît à mesure que la capacité du monde à fabriquer le passé s’améliore.

La suite de ce document est la spécification normative requise pour des implémentations interopérables.


Spécification du protocole qub

Champ Valeur
Version 1.0 (version du protocole 0x01, version de l’enveloppe externe 0x01)
Date 2026-05-01
Statut Brouillon
Vérifié jusqu’à 2026-05-01

Ce document est la spécification normative du protocole pour le système d’engagement temporel qub. Il définit les structures de données, les règles de sérialisation, les formules de dérivation et les procédures de vérification requises pour des implémentations interopérables.

Portée : la couche protocole est intentionnellement neutre vis-à-vis de la langue — le corps d’un qub est un texte/markdown/octets de pacte opaque, et le rendu localisé relève du lecteur (application web qub.social, iframe <qub-embed>, clients MCP, etc.).


1. Notation et conventions

Notation Signification
u8, u64, i64 Entiers non signés/signés de la largeur indiquée
[u8; N] Tableau d’octets de longueur fixe N
Vec<u8> Tableau d’octets de longueur variable
Option<T> Valeur de type T, ou absente
String Chaîne UTF-8, normalisée NFC
`
SHA3-256(x) Hachage NIST SHA3-256 de la chaîne d’octets x (FIPS 202)
ceil(x) Fonction plafond : plus petit entier ≥ x
CBOR Concise Binary Object Representation (RFC 8949)
big-endian Octet de poids fort en premier

Tous les entiers dans les pré-images sont encodés en tableaux d’octets big-endian de largeur fixe (i64 → 8 octets, u8 → 1 octet) sauf indication contraire.

Tous les horodatages sont en secondes Unix UTC.


2. Structures de données

2.1 ComposeQub (état créateur en mémoire)

Non sérialisé en CBOR. Non écrit sur le stockage permanent. Local à l’application créateur.

ComposeQub {
    draft_id:       [u8; 16],        // Aléatoire, généré localement
    created_at:     i64,             // Secondes Unix UTC
    unlock_at:      Option<i64>,     // Secondes Unix UTC ; None pendant la rédaction
    visibility:     u8,              // 0x01 = public (seule valeur en MVP)
    content_type:   u8,              // 0x01 = texte (seule valeur en MVP)
    plaintext:      Vec<u8>,         // Corps du qub UTF-8
    sender_label:   Option<String>,  // Nom décoratif ; non authentifié
    status:         DraftStatus,     // Composing | Sealed | Uploaded | Failed
}

2.2 QubEnvelope (charge utile déchiffrée)

Sérialisé en CBOR canonique (§3). Chiffré dans le SealedQub. C’est la structure qui prouve l’intégrité du contenu après déchiffrement.

QubEnvelope {
    version:             u8,              // Version majeure du protocole (0x01 pour v1)
    qub_id:              [u8; 32],        // Dérivé (voir §4.1)
    content_type:        u8,              // Registre des types de contenu (voir §6)
    created_at:          i64,             // Secondes Unix UTC
    unlock_at:           i64,             // Secondes Unix UTC
    outcome_at:          Option<i64>,     // V1.1 — quand la réalité rend son jugement (verdict-uplift-plan §3.1)
    sender_label:        Option<String>,  // Décoratif ; non authentifié en MVP
    reply_to:            Option<[u8; 32]>,// qub_id parent pour les fils ; pas dans la pré-image qub_id ; non signé (voir §9.3)
    body:                Vec<u8>,         // Charge utile (UTF-8 pour le texte, CBOR pour le pacte)
    body_hash:           [u8; 32],        // SHA3-256(body) (voir §4.2)
    sig_alg:             u8,              // Algorithme de signature (voir §9.2)
    author_signature:    Option<Vec<u8>>, // Présent quand sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Présent quand sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Présent pour les pactes contresignés
    cosigner_signature:  Option<Vec<u8>>, // Présent pour les pactes contresignés
}

Configuration de référence (qub texte non signé) : version = 0x01, content_type = 0x01, sig_alg = 0x00, tous les champs Option absents.

Autres configurations v1 : content_type = 0x03 (corps de pacte, voir §6.1) ; sig_alg = 0x01 (ML-DSA-65) avec author_signature et author_pubkey présents (voir §9.3) ; cosigner_pubkey et cosigner_signature présents ensemble pour les pactes contresignés (voir §9.7) ; reply_to défini sur le qub_id du qub parent pour les fils de réponses (voir §9.3 pour les implications quant à la portée de la signature).

2.3 SealedQub (format de fil canonique)

Sérialisé en CBOR canonique (§3). Écrit sur le stockage permanent. C’est l’artefact on-chain.

SealedQub {
    version:           u8,              // Version majeure du protocole (0x01 pour v1)
    qub_id:            [u8; 32],        // Identique à QubEnvelope.qub_id
    visibility:        u8,              // 0x01 = public ; les lecteurs v1 rejettent les autres valeurs
    unlock_at:         i64,             // Secondes Unix UTC
    outcome_at:        Option<i64>,     // V1.1 — exposé sur l’appel à action de suivi
                                        //   du verdict avant dévoilement ; reflète
                                        //   QubEnvelope.outcome_at ; lié à qub_id via
                                        //   la pré-image de §4.1.
    drand_chain_id:    String,          // Hachage de la chaîne drand (chaîne hex)
    drand_round:       u64,             // Numéro du tour drand cible
    tlock_ciphertext:  Vec<u8>,         // Octets CBOR du QubEnvelope chiffrés par tlock
    recipient_pubkey:  Option<[u8; 32]>,// Champ réservé ; accepté par le CBOR canonique
                                        //   mais non interprété par le lecteur de référence v1
    title:             Option<String>,  // Titre en clair affiché sur le compte à
                                        //   rebours du lecteur avant le dévoilement.
                                        //   Lié à qub_id via title_hash (§4.1).
                                        //   1..=100 codepoints NFC, sans caractères de contrôle.
}

2.4 RevealedQub (état de l’application lecteur)

Non sérialisé en CBOR. Local à l’application lecteur. Construit après déchiffrement et vérification réussis.

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 — repris depuis QubEnvelope.outcome_at / SealedQub.outcome_at ; pilote le bloc de suivi du verdict sur la page de dévoilement (verdict-uplift-plan §5.1)
    drand_chain_id:      String,
    drand_round:         u64,
    sender_label:        Option<String>,
    title:               Option<String>,    // Repris depuis SealedQub.title
    reply_to:            Option<[u8; 32]>,
    body:                Vec<u8>,
    body_hash:           [u8; 32],
    body_hash_verified:  bool,
    author_signature:    Option<Vec<u8>>,
    author_pubkey:       Option<Vec<u8>>,
    signature_verified:  Option<bool>,
    cosigner_pubkey:     Option<Vec<u8>>,
    cosigner_signature:  Option<Vec<u8>>,
    cosigner_verified:   Option<bool>,
}

3. Profil CBOR canonique

Toute sérialisation de SealedQub et QubEnvelope DOIT respecter ce profil. Deux implémentations à qui l’on fournit la même structure logique DOIVENT produire des octets identiques.

3.1 Règles d’encodage

Règle Spécification
Norme RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
Ordre des clés de map Trié par longueur d’octets encodés d’abord (les plus courts avant les plus longs), puis lexicographiquement (octet par octet pour les clés de même longueur)
Encodage des entiers Forme la plus courte : 0–23 dans l’octet initial ; 24–255 sur 2 octets ; 256–65535 sur 3 octets ; etc.
Encodage des longueurs Longueurs définies uniquement. Pas de tableaux, maps, chaînes d’octets ou de texte de longueur indéfinie (additional info = 31 interdit).
Tags Pas de tags CBOR (le major type 6 est interdit).
Virgule flottante Pas de flottants (les valeurs major type 7 0xF9–0xFB sont interdites).
Chaînes de texte Encodées en UTF-8, normalisées NFC (Unicode Normalization Form C).
Chaînes d’octets Octets bruts. Pas d’encodage base64 au niveau CBOR.
Clés en double Rejet avec erreur. Les analyseurs NE DOIVENT PAS accepter silencieusement des clés en double.
Clés inconnues Rejet avec erreur. Les analyseurs NE DOIVENT PAS tolérer de clés de map hors de l’ensemble de clés canonique du type — deux chaînes d’octets canoniques distinctes ne doivent jamais se décoder vers la même valeur (encode(decode(x)) == x), et pour les charges utiles signées, une clé supplémentaire serait du contenu caché sur lequel les deux signatures s’engagent. L’évolution du schéma passe par version, jamais par des clés supplémentaires.
Valeurs simples Seules true (0xF5), false (0xF4) et null (0xF6) sont autorisées.
Champs optionnels Les champs optionnels absents sont omis entièrement de la map CBOR (non encodés en null). Les champs optionnels présents sont inclus dans l’ordre de tri des clés.

3.2 Ordres de clés canoniques vérifiés

Ces ordres de clés sont normatifs. Les implémentations DOIVENT émettre les clés exactement dans cet ordre. Les assertions de débogage DEVRAIENT vérifier l’ordre dans les builds non-release.

QubEnvelope (version 0x01, non signé, tous les champs optionnels absents) :

"body"                (5 octets encodés)
"qub_id"              (7 octets encodés)
"sig_alg"             (8 octets encodés)
"version"             (8 octets encodés)
"reply_to"            (9 octets encodés)   ← uniquement si présent (fils de réponses)
"body_hash"           (10 octets encodés)
"unlock_at"           (10 octets encodés)
"created_at"          (11 octets encodés)
"outcome_at"          (11 octets encodés)  ← uniquement si présent (mécanique de verdict V1.1)
"content_type"        (13 octets encodés)
"sender_label"        (13 octets encodés)  ← uniquement si présent
"author_pubkey"       (14 octets encodés)  ← uniquement si présent
"cosigner_pubkey"     (16 octets encodés)  ← uniquement si présent (contre-signature de pacte)
"author_signature"    (17 octets encodés)  ← uniquement si présent
"cosigner_signature"  (19 octets encodés)  ← uniquement si présent (contre-signature de pacte)

Dérivation de l’ordre des clés QubEnvelope : chaque clé est une chaîne de texte CBOR. Longueur encodée = 1 octet d’en-tête + longueur de la chaîne (pour les chaînes inférieures à 24 octets). Trier d’abord par longueur encodée totale, puis lexicographiquement pour les clés de même longueur.

SealedQub (version 0x01, public, sans destinataire) :

"title"             (6 octets encodés)   ← uniquement si présent
"qub_id"            (7 octets encodés)
"version"           (8 octets encodés)
"unlock_at"         (10 octets encodés)
"outcome_at"        (11 octets encodés)  ← uniquement si présent (mécanique de verdict V1.1)
"visibility"        (11 octets encodés)
"drand_round"       (12 octets encodés)
"drand_chain_id"    (15 octets encodés)
"recipient_pubkey"  (17 octets encodés)  ← uniquement si présent
"tlock_ciphertext"  (17 octets encodés)

PactTerms (corps de pacte, content_type 0x03) :

"notes"         (6 octets encodés)  ← uniquement si présent
"terms"         (6 octets encodés)
"title"         (6 octets encodés)
"party_a"       (8 octets encodés)
"party_b"       (8 octets encodés)
"pact_version"  (13 octets encodés)

PactTerm (ligne du tableau terms) :

"key"    (4 octets encodés)
"value"  (6 octets encodés)

PartyIdentifier (map party_a / party_b) :

"label"    (6 octets encodés)
"contact"  (8 octets encodés)  ← uniquement si présent

3.3 Référence d’encodage des octets

Type Encodage CBOR Exemple
Hachage SHA3-256 (32 octets) 0x58 0x20 + 32 octets body_hash, qub_id
Horodatages (i64) Major type 0 (positif) ou 1 (négatif), encodage le plus court secondes Unix
Version (u8, valeur 1) 0x01 (un seul octet)
Type de contenu (u8, valeur 1) 0x01 (un seul octet)
sig_alg (u8, valeur 0) 0x00 (un seul octet)
Signature ML-DSA-65 (3 309 octets) 0x59 0x0C 0xED + 3 309 octets author_signature, cosigner_signature
Clé publique ML-DSA-65 (1 952 octets) 0x59 0x07 0xA0 + 1 952 octets author_pubkey, cosigner_pubkey

4. Dérivations normatives

4.1 qub_id

Le qub_id identifie de manière unique un qub et lie le QubEnvelope au SealedQub. Il est dérivé de manière déterministe du contenu de l’enveloppe.

qub_id = SHA3-256(
    "QUB_ID_V2"    ||    // séparateur de domaine : octets ASCII [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 octets) + remplissage 0x00 (1 octet) = 10 octets
    version        ||    // u8 (1 octet)
    content_type   ||    // u8 (1 octet)
    created_at     ||    // i64 big-endian (8 octets)
    unlock_at      ||    // i64 big-endian (8 octets)
    outcome_at_or_zero || // i64 big-endian (8 octets ; 0 quand outcome_at est absent)
    drand_round    ||    // u64 big-endian (8 octets)
    body_hash      ||    // [u8; 32] (32 octets)
    title_hash           // [u8; 32] (32 octets ; sentinelle d’absence = [0u8; 32])
)
// Pré-image totale : 108 octets → sortie de 32 octets

Encodage du séparateur de domaine : la chaîne "QUB_ID_V2" fait 9 octets ASCII. Un seul octet de remplissage 0x00 est ajouté pour atteindre 10 octets pour l’alignement. Les implémentations DOIVENT utiliser exactement ces 10 octets : [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

Encodage d’outcome_at : la V1.1 a étendu la pré-image de 92 à 100 octets afin d’intégrer le champ optionnel outcome_at dans la liaison. Un outcome_at absent est encodé en 8 octets nuls ; les validateurs du protocole rejettent outcome_at <= 0 partout, de sorte que cette sentinelle ne peut pas entrer en collision avec une valeur légitime. Voir §3.2 (format de fil) et le document interne tasks/verdict-uplift-plan.md pour la mécanique de verdict qui motive ce champ.

Encodage de drand_round : la V1.2 a étendu la pré-image de 100 à 108 octets afin d’intégrer drand_round (le tour drand cible, §4.3) dans la liaison, et a fait passer le séparateur de domaine à QUB_ID_V2. Cela lie le tour de verrou temporel à l’identité du qub : une passerelle ne peut pas relier le texte chiffré à un tour différent (par exemple déjà passé) de celui qu’implique l’unlock_at affiché. La procédure de déverrouillage (§8) vérifie en outre que le tour intégré à la strophe du texte chiffré tlock correspond à unlock_round(unlock_at), de sorte que l’heure de déverrouillage affichée est, de façon prouvable, le tour qui conditionne le déchiffrement.

Propriétés :

4.2 body_hash

body_hash = SHA3-256(body)

body est la charge utile brute Vec<u8>. Pour les qubs texte, c’est le corps du qub encodé en UTF-8.

4.2.1 title_hash

title_hash = SHA3-256(NFC(title).utf8_bytes)   si le titre est présent
title_hash = [0u8; 32]                         si le titre est absent

title est le titre optionnel en clair affiché sur le compte à rebours du lecteur avant le dévoilement (voir §3.2). La normalisation NFC se fait au moment du hachage afin que le condensat soit stable entre des séquences de codepoints visuellement équivalentes. La sentinelle entièrement à zéro est réservée au cas absent ; une chaîne vide est rejetée à la frontière du CBOR canonique comme un encodage non canonique de « absent » (l’encodage canonique omet entièrement le champ).

4.3 Correspondance déverrouillage-tour

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
Paramètre Source Exemple
unlock_at Secondes Unix UTC choisies par l’utilisateur 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time Infos de chaîne drand (genesis_time) 1595431050
chain_period_seconds Infos de chaîne drand (period) 30

L’opération ceil() sélectionne le premier tour drand dont l’heure de dévoilement est ≥ unlock_at. Cela garantit que le qub ne devient pas déchiffrable avant l’heure de déverrouillage choisie.

Cas limite : si (unlock_at - chain_genesis_time) est exactement divisible par chain_period_seconds, le résultat est ce tour précis — le qub se déverrouille pile à l’heure de dévoilement de ce tour.

Validation : unlock_at DOIT être dans le futur au moment du scellement. unlock_at NE DOIT PAS dépasser created_at + 10 ans (pour limiter le risque de dépendance drand à long horizon ; l’interface DEVRAIT avertir pour les dévoilements au-delà de 2 ans).


5. Newtypes du format de fil

Les newtypes du format de fil offrent une sécurité à la compilation contre la confusion des octets CBOR avec du JSON, du texte brut ou d’autres encodages d’octets.

Type Contient Produit par Consommé par
SealedQubCbor CBOR canonique du SealedQub serialize_sealed_qub() Envoi vers le stockage permanent, lecture par le lecteur
QubEnvelopeCbor CBOR canonique du QubEnvelope serialize_qub_envelope() Entrée chiffrement tlock, sortie déchiffrement tlock

5.1 Règles de construction

// Code de production — uniquement via les sérialiseurs CBOR :
let sealed = SealedQubCbor::from_encoded(cbor_bytes);

// Il n’y a délibérément AUCUNE implémentation From<Vec<u8>>.
// Vous ne pouvez pas envelopper accidentellement des octets arbitraires dans un type de format de fil.

// Accès aux octets bruts :
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();

5.2 Validation à la construction

from_encoded() DEVRAIT valider que l’entrée commence par un en-tête de map CBOR valide. La validation structurelle complète a lieu au moment de l’analyse, pas à la construction, pour éviter une double analyse.


6. Registre des types de contenu

Valeur Type Taille de corps maximale Notes
0x00 Réservé (invalide) NE DOIT PAS être utilisé
0x01 Texte brut (UTF-8, Markdown restreint) 50 Ko payant / 10 Ko gratuit Voir §10 pour les règles de rendu. Le partage gratuit/payant est appliqué par le service d’envoi ; le plafond strict de la couche protocole est de 50 Ko.
0x02 Réservé (futur) Alloué pour un futur type de contenu ; non valide en v1. Les lecteurs DOIVENT le rejeter conformément à la règle ci-dessous.
0x03 Pacte (accord bilatéral, corps CBOR) 100 Ko Le corps est un PactTerms CBOR canonique (§6.1). Signature du contre-signataire selon §9.7.
0x04 Verdict (auto-évaluation du créateur, corps CBOR) 8 Ko Le corps est un VerdictBody CBOR canonique (§6.2). Émis uniquement par l’intent verdict côté système. La relation au parent est portée par l’étiquette Arweave Parent-Tx-Id, et non par le corps. Voir verdict-uplift-plan §3.4.

Les lecteurs DOIVENT rejeter les types de contenu inconnus avec une erreur claire visible par l’utilisateur. Les lecteurs NE DOIVENT PAS tenter d’afficher des types inconnus en tant que texte.

6.1 Corps de pacte (content_type = 0x03)

Un corps de pacte est l’encodage CBOR canonique d’une valeur PactTerms :

PactTerms {
    pact_version:  u8,                    // 0x01 pour structured/v1
    title:         String,                // ≤ 200 octets, NFC
    terms:         Vec<PactTerm>,         // ≤ 20 lignes
    party_a:       PartyIdentifier,       // initiateur
    party_b:       PartyIdentifier,       // contre-signataire
    notes:         Option<String>,        // ≤ 5 000 octets, NFC ; clé absente si aucune
}

PactTerm       { key: String (≤ 100), value: String (≤ 2 000) }   // NFC des deux côtés
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }

Les ordres de clés CBOR canoniques pour les trois maps sont donnés en §3.2. Le CBOR de pacte sérialisé total NE DOIT PAS dépasser 100 Ko (correspond à §6).

Discriminateur de schéma. La première ligne de terms pour un pacte structured/v1 DOIT être { key: "pact_schema", value: "structured/v1" }. Les lignes sans ce marqueur sont des pactes « personnalisés » et ne reçoivent ni validation structurée ni rendu sensible au schéma.

Emplacements d’accusé de réception figés. Les pactes structured/v1 portent exactement quatre lignes d’accusé de réception sous ces clés :

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

La value de chacune est l’une des huit chaînes anglaises figées choisies par la paire (role, kind), où role ∈ { seller, buyer, provider, client } et kind ∈ { standard, capacity }. Ces chaînes sont elles-mêmes des données de protocole normatives — les signatures ML-DSA-65 des deux parties s’engagent sur les octets exacts via body_hash. Elles NE SONT PAS localisées ; le corps signé est neutre vis-à-vis de la langue. Toute modification de formulation requiert une nouvelle version de schéma (structured/v2).

Les huit chaînes, leur lookup (acknowledgement_for(role, kind)) et le motif de chacune sont fixés par l’implémentation de référence. Les implémentations conformes DOIVENT émettre des valeurs d’accusé de réception identiques au niveau octet ; les tests de fixtures fixées sur le SHA3-256 du body_hash couvrant les quatre combinaisons de rôle attrapent toute dérive.

Ordre d’affichage dans le lecteur. Les chaînes d’accusé de réception contiennent des phrases telles que « described above » (« décrit ci-dessus »), qui supposent que les lignes de description / périmètre s’affichent avant les accusés de réception. Les lecteurs DOIVENT afficher le tableau terms dans l’ordre CBOR ; un réordonnancement casse la sémantique de la prose.

Contact de la contrepartie. Lorsque le contact de Party B est une adresse e-mail valide, le service d’envoi qub envoie automatiquement un e-mail d’invitation à examen / contre-signature au moment de l’émission et lie la contre-signature ultérieure à la vérification de cette même adresse (§9.7). Les pactes dont le contact de Party B est absent peuvent toujours être contresignés, mais uniquement par un canal hors-bande — le service refuse les requêtes de contre-signature qui ne peuvent pas produire un marqueur de vérification d’e-mail correspondant de 15 minutes.

6.2 Corps de verdict (content_type = 0x04)

Un corps de verdict est l’encodage CBOR canonique d’une valeur VerdictBody :

VerdictBody {
    verdict_version: u8,                  // 0x01 pour structured/v1
    outcome:         u8,                  // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
    reflection:      Option<String>,      // ≤ 2 000 octets NFC ; « ce qui a changé, ce que vous avez appris »
    evidence_url:    Option<String>,      // ≤ 2 048 octets ; HTTPS uniquement ; clé absente si omise
}

Ordre canonique des clés CBOR :

"outcome"          (8 octets encodés)
"reflection"       (11 octets encodés)  ← uniquement si présent
"evidence_url"     (13 octets encodés)  ← uniquement si présent
"verdict_version"  (16 octets encodés)

Le CBOR de verdict sérialisé total NE DOIT PAS dépasser 8 Ko (correspond à la ligne du registre ci-dessus).

Énumération du résultat. L’octet sur le fil est neutre vis-à-vis de l’intent ; les quatre catégories Right / Partial / Wrong / Unfalsifiable couvrent l’espace des résultats de toute intent porteuse de verdict. Les libellés par intent (« Vu juste » / « Tenu » / « Livré » / « Confirmée » pour Right, etc.) sont une préoccupation de rendu côté lecteur, résolue par rapport à l’intent du qub parent — le fil reste neutre en langue et en intent. Les valeurs hors de 1..=4 DOIVENT être rejetées au décodage.

Liaison au parent. Un qub de verdict ne porte PAS la référence au parent dans son corps. L’identifiant de transaction Arweave du qub parent est émis comme étiquette de stockage Parent-Tx-Id au moment de l’envoi (§7, couche d’étiquettes de stockage). Cela garde le corps comme une déclaration signée autonome d’auto-évaluation ; la chaîne d’audit (« avoir raison à propos de quoi ? ») est établie par la recherche d’étiquettes Arweave.

Sécurité de l’URL de preuve (normatif). Lorsque evidence_url est présente, les validateurs (côté composition, côté fil, à la périphérie du Worker) DOIVENT appliquer :

  1. HTTPS uniquement. La chaîne DOIT commencer par la séquence d’octets https://. Tout autre schéma — http, ftp, javascript, data, file, etc. — est rejeté.
  2. Plafond de longueur. ≤ 2 048 octets (limite pratique des URL dans le navigateur).
  3. NFC + contrôle des codepoints hostiles. Même règle que title et reflection — les codepoints de bidi-override / largeur nulle / bloc tag / BOM / C0 / C1 sont rejetés. La définition correspond à la fonction Rust crate::handle::contains_hostile_text_codepoint et à la fonction TS workers/api/src/utils/unicode.ts::isHostileCodepoint (à garder en synchronisation).
  4. Aucun blanc, aucun caractère de contrôle ASCII. Tout blanc / DEL / octet sous 0x20 n’importe où dans l’URL est rejeté — referme le vecteur d’injection \n / \t que la règle bidi ne couvre pas.
  5. Segment d’hôte non vide. Tout ce qui se trouve entre https:// et le premier /, ? ou # DOIT être non vide.

Aucune récupération côté serveur. Le Worker NE DOIT PAS proxifier, récupérer ou prévisualiser l’URL. Le protocole stocke une chaîne ; le rendu se fait côté lecteur avec rel="nofollow noopener noreferrer" target="_blank" et l’hôte affiché visiblement à côté du texte du lien.

Réflexion. Texte de réflexion facultatif rédigé par le créateur (« ce qui a changé, ce que vous avez appris »). Même validation NFC + codepoints hostiles que title. Une saisie vide ou ne contenant que des blancs est repliée à « absente » au moment de la construction.

Version de schéma. La v1 ne prend en charge que verdict_version = 0x01. Les futures révisions de schéma incrémentent cet octet et arrivent en même temps qu’une nouvelle version de protocole selon §12.


7. Protocole de scellement

Séquence complète de scellement. Chaque étape est normative.

 1. L’utilisateur compose le texte brut et les métadonnées dans ComposeQub.
 2. Validation :
    a. body est non vide.
    b. taille de body ≤ max pour content_type et le niveau utilisateur (voir §6).
    c. unlock_at est dans le futur.
    d. unlock_at ≤ created_at + 10 ans.
    e. content_type est une valeur connue et prise en charge.
 3. Calculer body_hash = SHA3-256(body).
 4. Définir created_at = secondes Unix UTC actuelles.
 5. Sélectionner la chaîne drand. Charger chain_genesis_time et chain_period_seconds, et
    calculer drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
    (Calculé ici, avant qub_id, car drand_round est lié dans la pré-image du qub_id
    — §4.1, V1.2.)
 6. Calculer qub_id (voir §4.1), en y intégrant drand_round de l’étape 5.
 7. Construire QubEnvelope avec tous les champs.
 8. Sérialiser QubEnvelope en CBOR canonique → octets B.
    Asserter : la sortie sérialisée correspond au profil canonique (§3).
 9. Calculer C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construire SealedQub avec tlock_ciphertext = C, et qub_id, version,
    unlock_at, drand_chain_id, drand_round correspondants.
12. Sérialiser SealedQub en CBOR canonique → SealedQubCbor.
12a. Générer K = 32 octets aléatoires (CSPRNG) et N = 12 octets aléatoires (CSPRNG).
     Calculer W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
     selon §13. Les octets envoyés vers le stockage permanent sont le CBOR W de l’OuterWrapper,
     jamais le SealedQubCbor nu. K ne quitte l’appareil que sous forme de
     fragment d’URL à l’étape 16.
13. Afficher la mise en garde au moment du scellement. L’utilisateur confirme.
14. Valider l’éligibilité de l’envoi via le service d’envoi qub (détection de bot, droits, limites de débit).
15. Soumettre W (les octets de l’OuterWrapper) au service d’envoi qub ; le service
    signe et envoie vers le stockage permanent. Le service est aveugle aux octets du
    SealedQubCbor interne et ne reçoit jamais K.
16. Recevoir arweave_tx_id du service. Construire l’URL de remise sous la forme
    `<origin>/c/<arweave_tx_id>#<base64url(K)>` (ou `<origin>/s/<short_code>#<base64url(K)>`
    quand un code court est attribué). Les navigateurs ne transmettent pas
    les fragments d’URL aux serveurs, donc K n’est jamais observé par
    qub.social ni par aucune passerelle de stockage.

Couche de tags de stockage (hors-bande). Le service d’envoi qub attache un ensemble délibérément réduit de tags de transaction de stockage à côté de la charge utile enveloppée. Content-Type=application/octet-stream est requis normativement. Le service de référence attache en plus trois tags optionnels lorsque le créateur choisit de les exposer : Intent (intention de composition validée par allowlist — par exemple, quote, reply, commitment), Author (empreinte de la clé publique §9.3 du créateur sous forme hex minuscule de 64 caractères) et Parent-Tx-Id (ID de transaction de stockage du qub parent pour les fils de réponses, base64url de 43 caractères).

Le tag Author est opt-in par qub : l’application créateur de référence ne l’attache que lorsque l’utilisateur active explicitement l’attribution publique au moment du scellement. Quand l’interrupteur est désactivé — par défaut — aucun tag Author n’est écrit et le qub n’est pas attribué sur la chaîne : rien sur le stockage permanent ne lie l’envoi à l’identifiant, à l’adresse e-mail ou aux autres qubs d’un créateur. Quand l’interrupteur est activé, l’empreinte Author se résout au @handle choisi par le créateur via la chaîne d’attestation §9.5. Les relations de fils de réponses et Intent ne sont pas identifiantes. L’enveloppe externe (§13) protège le corps interne contre la corrélation de chiffrés — empêchant un agrégateur de reconnaître et de déchiffrer en masse les envois en forme de qub après la publication de leur tour drand.

Le service de référence n’attache intentionnellement PAS de tags App-Name, App-Version ou Type : tout filtre à valeur unique de ce genre renverrait l’ensemble du corpus qub à une requête GraphQL, ce qui est incompatible avec la portée de confidentialité « corps uniquement » de l’enveloppe.

Un vérificateur conforme NE DOIT PAS dépendre d’un quelconque tag de stockage pour la vérification tierce §11 ; le hachage de corps / qub_id / signature ne s’engagent que sur le CBOR interne, jamais sur l’ensemble des tags.


8. Protocole de déverrouillage

Séquence complète de déverrouillage. Chaque étape est normative.

 1. Le lecteur ouvre l’URL de remise. Extraire arweave_tx_id du chemin ET
    K = base64url_decode(fragment) du fragment d’URL. Si le fragment
    est absent ou malformé → afficher « cette URL ne contient pas sa clé
    de déchiffrement » et arrêter ; le lecteur NE DOIT PAS contacter la
    passerelle de stockage sans K, puisque récupérer des octets enveloppés
    que le lecteur ne peut pas déchiffrer ne sert à rien et ne fait que
    fuiter la tentative d’accès.
 2. Vérifier la liste de blocage. Si tx_id est sur la liste → afficher le message de blocage. Arrêter.
 3. Récupérer les octets de l’OuterWrapper depuis le stockage permanent (avec repli multi-passerelles).
 3a. Désenvelopper : analyser les octets en OuterWrapper (§13), vérifier
    l’octet `version` du wrapper = `0x01`, et calculer SealedQubCbor =
    unwrap_sealed_qub(OuterWrapper, key=K). Tout échec d’authentification
    AEAD (mauvaise K, chiffré altéré, qub_id-as-AAD échangé, nonce
    échangé) → afficher « la clé de déchiffrement de cette URL ne
    correspond pas au qub stocké » et arrêter. Les échecs
    d’authentification sont indissociables pour le lecteur (§13.5).
 4. Analyser SealedQubCbor → SealedQub.
 5. Valider : SealedQub.version est connue (0x01). Rejeter les versions inconnues.
 6. Si l’heure actuelle < SealedQub.unlock_at → afficher le compte à rebours. Sonder ou attendre.
 6a. Vérification de liaison au tour (V1.2). Recalculer expected_round =
    ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
    Rejeter sauf si SealedQub.drand_round == expected_round ET le tour intégré
    à la strophe du texte chiffré tlock (lu via l’en-tête age/tlock, sans
    signature requise) == expected_round. Le tour de la strophe est celui qui
    conditionne réellement le déchiffrement ; sans cette vérification, un créateur
    malveillant pourrait relier le texte chiffré à un tour déjà passé tout en
    affichant un compte à rebours futur, de sorte que quiconque lit les octets
    stockés pourrait déchiffrer avant unlock_at. Les implémentations sans
    identité de chaîne (mocks de test) ignorent cette vérification.
 7. Une fois l’heure actuelle ≥ SealedQub.unlock_at :
    a. Récupérer la signature de tour drand pour SealedQub.drand_round depuis le réseau drand.
    b. Calculer B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
 8. Analyser B → QubEnvelope.
 9. Valider QubEnvelope.version est connue.
10. Vérifier : SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
    Échec → erreur d’intégrité.
11. Vérifier : QubEnvelope.qub_id == SealedQub.qub_id.
    Échec → erreur d’intégrité.
12. Vérifier : QubEnvelope.unlock_at == SealedQub.unlock_at.
    Échec → erreur d’intégrité.
13. Vérifier : QubEnvelope.content_type est connue et affichable.
    Valeurs connues : 0x01 (texte), 0x03 (pacte). Inconnue → afficher une erreur.
14. Si QubEnvelope.sig_alg != 0x00 → vérifier la signature de l’auteur (voir §9.4).
15. Si cosigner_pubkey ou cosigner_signature présents → vérifier le contre-signataire (voir §9.7).
16. Afficher le contenu via le rendu approprié (voir §10 pour le texte, §6 pour le pacte).
17. Construire RevealedQub pour l’affichage.

9. Signature d’auteur

9.1 Motivation

Les qubs sont stockés sur un stockage permanent. Les signatures d’auteur doivent rester infalsifiables indéfiniment, c’est pourquoi v1.0 utilise le schéma post-quantique ML-DSA-65 (FIPS 204) plutôt qu’un schéma classique dont la sécurité pourrait se dégrader pendant la durée de vie permanente du qub.

9.2 Registre d’algorithmes

sig_alg Schéma Taille de clé Taille de signature
0x00 Pas de signature (non signé)
0x01 ML-DSA-65 (FIPS 204) 1 952 octets 3 309 octets

Les lecteurs DOIVENT rejeter les valeurs sig_alg inconnues.

9.3 Construction de la pré-image signée

Deux versions de pré-image ont existé. Toutes les signatures DOIVENT utiliser V2, et les vérificateurs DOIVENT accepter uniquement V2. L’ancienne pré-image V1 (documentée ci-dessous à titre de référence historique) était acceptée comme repli de vérification uniquement pendant la migration vers V2 ; ce repli a été retiré et une signature V1 uniquement est désormais rejetée.

V2 (actuelle — produite par toute nouvelle signature d’auteur, ainsi que par les deux signatures du flux de préparation / contre-signature de pacte) :

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V2"  ||    // séparateur de domaine (17 octets)
    version              ||    // u8 (1 octet)
    qub_id               ||    // [u8; 32] (32 octets)
    body_hash            ||    // [u8; 32] (32 octets)
    unlock_at            ||    // i64 big-endian (8 octets)
    0x00                 ||    // u8 (1 octet) : DOIT être 0x00 en v1.x
    sender_label_hash    ||    // [u8; 32] : SHA3-256(NFC(sender_label)),
                               //   ou 32 octets nuls si absent
    reply_to_or_zero           // [u8; 32] : qub_id parent, ou 32 octets
                               //   nuls si absent
)

// Pré-image totale : 155 octets → hachage de 32 octets

signature = Sign(author_secret_key, sig_input)

sender_label_hash suit la même convention de sentinelle d’absence que title_hash (§4.2.1) : 32 octets nuls ne constituent pas une sortie SHA3-256 valide, de sorte que « absent » ne peut jamais entrer en collision avec une étiquette présente. Tous les champs sont de largeur fixe, donc la pré-image est sans ambiguïté sans préfixes de longueur.

V1 (ancienne — RETIRÉE ; n’est plus produite ni acceptée à la vérification) :

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V1"  ||    // séparateur de domaine (17 octets)
    version              ||    // u8 (1 octet)
    qub_id               ||    // [u8; 32] (32 octets)
    body_hash            ||    // [u8; 32] (32 octets)
    unlock_at            ||    // i64 big-endian (8 octets)
    0x00                       // u8 (1 octet) : DOIT être 0x00 en v1.0
)

// Pré-image totale : 91 octets → hachage de 32 octets

La pré-image V1 omettait sender_label et reply_to. Elle était acceptée comme repli de vérification uniquement pendant la migration vers V2 ; ce repli a depuis été retiré — les vérificateurs DOIVENT accepter uniquement la pré-image V2. La définition est conservée ici à titre de référence historique et pour expliquer le séparateur de domaine ci-dessous. Une signature qui ne se vérifie que contre V1 DOIT être traitée comme un échec de vérification.

Séparateurs de domaine : "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" font 17 octets ASCII chacun ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Pas de remplissage. Le séparateur différent sépare par domaine les deux constructions, de sorte qu’une signature sur une pré-image ne peut jamais se vérifier comme l’autre.

Octet org_id_present : l’octet suivant unlock_at DOIT être 0x00. L’implémentation de référence l’expose sous forme de constante ORG_ID_PRESENT_INDIVIDUAL = 0x00 dans crates/qub-core/src/signing.rs ; les lecteurs reconstruisant sig_input pour vérification DOIVENT émettre le même octet.

Portée de la signature — ce qui est et n’est pas couvert. La sig_input V2 s’engage directement sur version, qub_id, body_hash, unlock_at, sender_label et reply_to (plus le séparateur de domaine fixe et l’octet org_id_present). qub_id est lui-même dérivé de version, content_type, created_at, unlock_at, outcome_at, drand_round et body_hash via la pré-image §4.1, donc tout changement de ces champs produit un qub_id différent et invalide la signature transitivement. La surface authentifiée est donc :

Champ Authentifié par la signature Comment
version Entrée directe de sig_input
qub_id Entrée directe
body_hash Entrée directe
unlock_at Entrée directe
sender_label Entrée directe via sender_label_hash (pré-image V2 — la seule forme acceptée)
reply_to Entrée directe via reply_to_or_zero (pré-image V2 — la seule forme acceptée)
content_type Transitivement, via la pré-image qub_id
created_at Transitivement, via la pré-image qub_id
outcome_at Transitivement, via la pré-image qub_id
drand_round Transitivement, via la pré-image qub_id (V1.2)
body Transitivement, via body_hash = SHA3-256(body)
author_pubkey — (implicite) La clé qui a vérifié la signature est l’auteur, par définition
cosigner_pubkey / cosigner_signature Signés indépendamment sur la même sig_input (voir §9.7)
drand_chain_id, tlock_ciphertext, visibility Champs du SealedQub externe, pas dans l’enveloppe — couverts par leurs propres invariants structurels (cohérence tour / chaîne) mais pas par la signature de l’auteur. (drand_round est désormais lié transitivement via la pré-image du qub_id — voir ci-dessus.)

Pourquoi V2 est la seule pré-image acceptée.

Les implémentations qui affichent sender_label ou reply_to aux utilisateurs finaux DOIVENT mettre en avant l’identité authentifiée (empreinte de clé publique, attestation) comme signal d’identité principal, pas l’étiquette.

9.4 Procédure de vérification

1. Lire sig_alg depuis QubEnvelope.
2. Si sig_alg == 0x00 → non signé. Pas de vérification. Afficher « qub non signé. »
3. Si sig_alg est inconnu → rejeter. Afficher « schéma de signature non reconnu. »
4. Extraire author_signature et author_pubkey. Si l’un est absent → erreur d’intégrité.
5. Reconstruire sig_input à partir des champs de QubEnvelope (formule V2, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). La pré-image V2 est la
   seule forme acceptée — l’ancien repli V1 est retiré (§9.3), donc une
   signature qui ne se vérifie pas contre V2 échoue, point final.
7. Si la vérification réussit → afficher « signé par [empreinte de clé]. »
8. Si la vérification échoue → afficher « échec de la vérification de signature. »

La vérification de signature est l’opération la plus coûteuse (en particulier ML-DSA-65). Elle DEVRAIT être effectuée après que toutes les vérifications moins coûteuses (hachage, qub_id, unlock_at) soient passées.

9.5 Attestations d’identité

Les attestations d’identité — la correspondance entre author_pubkey et des revendications d’identité reconnaissables (identifiant qub, adresse e-mail, identifiant social, identifiant de passkey) — sont une amélioration progressive côté lecteur et ne sont pas requises pour la vérification de signature. Les lecteurs qui résolvent des attestations en identité affichée DOIVENT appliquer la précédence :

handle > email > social > fingerprint

Le repli sur l’empreinte est l’hex minuscule de SHA3-256(author_pubkey) ; il est toujours disponible pour tout qub signé. Les lecteurs PEUVENT l’abréger pour l’affichage — le lecteur de référence rend qub: suivi des quatre premiers et des quatre derniers octets (qub:<8 hex>…<8 hex>).

Un vérificateur conforme peut effectuer toutes les vérifications de §9.4 sans contacter l’API qub, sans aucun réseau au-delà du stockage permanent et de drand, et sans aucune recherche côté serveur. La résolution d’attestation est une étape distincte au mieux, effectuée seulement après le succès de la vérification de signature.

9.6 Impact en taille

Ed25519 ML-DSA-65
Signature 64 octets 3 309 octets
Clé publique 32 octets 1 952 octets
Total par qub 96 octets 5 261 octets
Surcoût de stockage (à ~5 $/Mo) ~0,0005 $ ~0,026 $

Pour un qub texte de 500–2 000 octets, ML-DSA-65 triple à peu près la taille stockée. Le coût absolu est négligeable.

9.7 Vérification du contre-signataire (pactes bilatéraux)

Pour les accords bilatéraux (content_type = 0x03), une seconde couche de signature prouve que les deux parties ont consenti aux mêmes termes.

Champs de l’enveloppe :

Les deux champs DOIVENT être présents ensemble ou tous deux absents. Si exactement un est présent, les lecteurs DOIVENT signaler une erreur d’intégrité.

Procédure de vérification :

1. Si cosigner_pubkey absent et cosigner_signature absent → pas de contre-signataire. Terminé.
2. Si exactement un est présent → erreur d’intégrité.
3. Vérifier cosigner_pubkey != author_pubkey (empêche l’auto-contresignature).
   Échec → afficher « la clé du contre-signataire doit différer de celle de l’auteur. »
4. Reconstruire sig_input avec la même formule qu’en §9.3 (V2 uniquement — l’ancien
   repli V1 est retiré ; tous les clients de pacte produisent des signatures V2).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Succès → afficher « contresigné par [empreinte du contre-signataire]. »
7. Échec → afficher « échec de la vérification de la contre-signature. »

Propriétés :

Verrou de liaison par e-mail (opérationnel). Lorsqu’un pacte émis porte un contact e-mail Party B (§6.1), le service d’envoi qub DOIT refuser la requête de contre-signature à moins qu’il n’existe un marqueur de vérification d’e-mail à courte durée correspondant à la fois à l’id d’émission et au hachage de l’e-mail normalisé de ce contact. Le marqueur est écrit par /api/v1/auth/verify quand le jeton du lien magique porte un staging_id et que l’adresse vérifiée correspond à SHA-256(normalise_email(party_b.contact)) — où normalise_email(addr) préserve la casse de la partie locale et met en minuscules uniquement la partie domaine (selon RFC 5321 §2.3.11), et SHA-256 ici est le hachage NIST FIPS 180-4 (distinct du SHA3-256 utilisé en §4) — et expire 900 secondes (15 minutes) après émission. C’est un verrou opérationnel anti-usurpation, PAS une partie de la preuve qub on-chain — un vérificateur tiers rejouant §11 n’a besoin que du stockage permanent et de drand, sans aucune recherche côté serveur. Le marqueur n’existe que côté serveur et ne fait jamais partie du corps signé.

Impact en taille (auteur ML-DSA-65 + contre-signataire) :

Composant Taille
Signature de l’auteur 3 309 octets
Clé publique de l’auteur 1 952 octets
Signature du contre-signataire 3 309 octets
Clé publique du contre-signataire 1 952 octets
Surcoût cryptographique total 10 522 octets
Surcoût de stockage ~0,05 $

10. Rendu et assainissement Markdown

Cette section est sensible à la sécurité. Le lecteur affiche les qubs texte (content_type = 0x01) à l’aide d’un sous-ensemble Markdown restreint.

10.1 Éléments autorisés

10.2 Éléments interdits

Élément Traitement
HTML brut (<div>, <script>, etc.) Entièrement retiré. Aucun HTML ne passe.
Images (![alt](url)) Retirées. La syntaxe d’image est supprimée du résultat.
Liens ([text](url)) URL affichée en texte brut visible. Pas d’auto-lien. Pas cliquable sans action explicite de l’utilisateur.
Schémas d’URL dangereux javascript:, data:, vbscript:, file: — retirés.
Iframes, embeds, objects Retirés.
Entités HTML Décodées en caractères d’affichage uniquement si elles sont sûres.

10.3 Implémentation

Les implémentations DOIVENT utiliser un analyseur strict basé sur une allowlist, pas une blocklist. L’approche recommandée :

  1. Analyser le Markdown avec pulldown-cmark (ou équivalent).
  2. Parcourir l’AST et retirer tout nœud absent de l’allowlist (§10.1).
  3. Pour les nœuds liens : émettre l’URL en texte visible, pas en élément <a> cliquable.
  4. Convertir l’AST filtré en représentation intermédiaire typée (par exemple un enum MarkdownNode avec uniquement des variantes sûres). Le HTML brut est structurellement non représentable dans cette IR.
  5. Rendre depuis l’IR typée vers la couche d’affichage cible (par exemple composants de vue réactifs, nœuds DOM). Aucune concaténation de chaînes HTML ni innerHTML à aucun moment.

Les approches blocklist sont fragiles parce que de nouvelles extensions Markdown ou des particularités d’analyseur peuvent introduire des éléments non filtrés. L’approche AST typée rend les XSS structurellement impossibles — il n’y a aucune variante capable de transporter du HTML arbitraire.

10.4 Limites de taille et de structure


11. Vérification tierce

N’importe quel tiers peut vérifier un qub public sans la coopération de qub. La procédure de vérification :

1. Obtenir arweave_tx_id (à partir de l’URL de remise ou par connaissance directe).
2. Récupérer SealedQubCbor depuis n’importe quelle passerelle de stockage.
3. Confirmer l’inclusion dans un bloc de stockage (hauteur de bloc, horodatage du bloc).
4. Analyser SealedQubCbor → SealedQub.
5. Récupérer la signature de tour drand pour SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → octets CBOR du QubEnvelope.
7. Analyser → QubEnvelope.
8. Vérifier SHA3-256(body) == body_hash.
9. Vérifier QubEnvelope.qub_id == SealedQub.qub_id.
10. Vérifier QubEnvelope.unlock_at == SealedQub.unlock_at.
11. Si sig_alg != 0x00 : vérifier author_signature (voir §9.4).
12. Toutes les vérifications passent → le qub est vérifié.

Ce que la vérification prouve :

Preuve Ce qu’elle établit
Engagement Le chiffré existait au moment de l’horodatage du bloc de stockage.
Intégrité Le corps en clair correspond au hachage engagé et n’a pas été altéré.
Synchronisation Le contenu était illisible jusqu’au tour drand, qui correspond à l’heure de déverrouillage choisie (sous réserve des hypothèses de sécurité de tlock et de drand).

Ce que la vérification NE prouve PAS :

Non-preuve Pourquoi
Authorship Le sender_label est décoratif. Sans sig_alg0x01, n’importe qui aurait pu sceller ce contenu.
Intention Le qub prouve le contenu et la synchronisation, pas ce que le créateur entendait subjectivement.
Synchronisation pré-événement L’inclusion dans un bloc de stockage peut accuser un retard de quelques minutes par rapport à l’envoi réel. L’horodatage d’engagement est l’heure du bloc, pas le moment où l’utilisateur a appuyé sur « sceller ».

12. Versionnement

12.1 Version du protocole

Le champ version (u8) dans SealedQub et QubEnvelope identifie la version majeure du protocole.

12.2 Historique des versions

Version Valeur Description
v1 0x01 qubs texte publics (content_type 0x01), pactes bilatéraux (0x03, schéma structured/v1, ML-DSA-65 auteur + contre-signataire), tlock, SHA3-256

12.3 Compatibilité ascendante

Un lecteur v1 rencontrant un QubEnvelope avec des clés de map CBOR inconnues (clés absentes de l’ordre canonique §3.2) DOIT le rejeter avec une erreur de décodage (§3.1). La compatibilité ascendante repose sur le champ version, pas sur la tolérance de clés : les additions futures — même des métadonnées mineures — sont livrées sous une nouvelle valeur de version, qu’un lecteur v1 rejette avec une erreur claire « protocole plus récent » plutôt que d’écarter silencieusement du contenu sur lequel les signatures s’engagent.

Un lecteur v1 rencontrant sig_alg = 0x01 (ML-DSA-65) mais sans support de vérification ML-DSA-65 DEVRAIT afficher le contenu du qub avec un avis « signature présente mais non vérifiable », pas rejeter le qub entièrement. L’implémentation de référence rejette aujourd’hui toute valeur sig_alg autre que 0x00 et 0x01 parce que le registre v1 ne contient aucun autre algorithme valide — rejet strict et soft-fail sont observationnellement identiques jusqu’à ce qu’un troisième algorithme soit enregistré. Le comportement soft-fail ci-dessus devient porteur dès que §9.2 admet une nouvelle entrée, et le lecteur de référence sera mis à jour pour faire un soft-fail à ce moment-là.

12.4 Version de l’enveloppe externe

L’OuterWrapper décrit en §13 porte son propre octet version, indépendant de SealedQub.version et QubEnvelope.version. Les deux espaces de version évoluent séparément : un futur remplacement symétrique post-quantique sûr fait évoluer l’octet du wrapper sans toucher à la version interne du protocole, et une future addition à la couche protocole (par exemple, un nouveau champ d’enveloppe) fait évoluer la version interne sans toucher à l’octet du wrapper.

OUTER_WRAPPER_VERSION_* Valeur Algorithme Statut
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM avec nonce de 12 octets, tag d’authentification de 16 octets, AAD lié à qub_id défaut v1
0x020xFF Réservé Futur

Les lecteurs DOIVENT rejeter les versions de wrapper inconnues avec une erreur claire. Le protocole garde intentionnellement un espace de version de wrapper étroit jusqu’à ce qu’un moteur de migration concret apparaisse (par exemple, des recommandations NIST en faveur d’un AEAD différent) ; un emplacement 0x02 sera attribué dans la même révision qui introduit l’algorithme.


13. Enveloppe de chiffrement externe

13.1 Motivation

Les couches du protocole (QubEnvelope → tlock → SealedQub) rendent un qub scellé verrouillé dans le temps : le corps est illisible jusqu’à ce que unlock_at et la signature de tour drand aient été publiés. Après le déverrouillage, cependant, la signature de tour est publique et la forme CBOR canonique de SealedQub est reconnaissable, donc un agrégateur ayant indexé les transactions de stockage permanent pourrait déchiffrer en masse l’ensemble du corpus qub.

L’enveloppe de chiffrement externe ferme ce canal en interposant une couche AEAD symétrique additionnelle entre le SealedQubCbor canonique et les octets écrits sur le stockage permanent. La clé 256 bits K ne vit que dans le fragment d’URL de l’URL de remise et sur les appareils des utilisateurs ; les navigateurs ne transmettent pas les fragments d’URL aux serveurs, donc qub.social, toute passerelle de stockage et tout CDN devant l’un ou l’autre sont observationnellement aveugles à K. Chaque qub sur le stockage permanent est donc un chiffré opaque dont le texte clair est irrécupérable sans l’URL que le créateur a choisi de partager.

Effet net :

13.2 Empilement

corps en clair                       ← QubEnvelope.body (§2.2)
  ↓ CBOR canonique (§3)
CBOR de l’enveloppe
  ↓ chiffrement tlock vers le tour drand (§7 étape 10)
tlock_ciphertext (à l’intérieur de SealedQub) (§2.3)
  ↓ CBOR canonique (§3)
octets SealedQubCbor                  ← artefact de fil interne
  ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 étape 12a, cette section)
octets CBOR OuterWrapper              ← envoyés vers le stockage permanent (§7 étape 15)

Le scellement et le déverrouillage à la couche protocole (§7, §8) sont inchangés sous la frontière du wrapper ; le wrapper s’attache au site d’appel de seal() et se détache au site d’appel de unlock().

13.3 Structure de données OuterWrapper

struct OuterWrapper {
    version:    u8,           // 0x01, voir §12.4
    qub_id:     [u8; 32],     // copié depuis le SealedQub interne ; AAD AEAD
    nonce:      [u8; 12],     // nonce AEAD 96 bits
    ciphertext: Vec<u8>,      // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || tag de 16 octets
}

Invariants des champs.

Encodage CBOR. CBOR canonique selon §3, avec la même règle d’ordre des clés (triées par longueur d’octets encodés croissante, puis lexicographiquement). Les quatre clés sont :

Clé Octets encodés Ordre
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

Le premier octet du CBOR de l’OuterWrapper est donc l’en-tête de map à longueur définie pour une map à 4 entrées (0xA4).

13.4 Liaison AAD à qub_id

Le wrapper lie qub_id comme données authentifiées additionnelles AEAD. C’est la défense structurelle porteuse contre trois classes d’attaques :

Attaque Défense
Déplacer le chiffré sous un autre champ qub_id dans le wrapper Mismatch AAD → l’authentification AEAD échoue
Mélanger le fragment d’URL du qub A avec les octets de stockage permanent du qub B Mismatch AAD → l’authentification AEAD échoue
Falsifier le champ qub_id du wrapper après envoi Mismatch AAD → l’authentification AEAD échoue

Porter qub_id dans le texte clair du wrapper n’affaiblit pas l’immunité à l’énumération de manière significative — qub_id est lui-même un hachage SHA3-256 de la pré-image §4.1 sans pré-image récupérable depuis le condensat, et un énumérateur qui a déjà récolté les octets du wrapper n’apprend rien du qub_id visible qu’il ne pourrait inférer de l’existence de l’envoi lui-même.

13.5 Algorithmes de wrap et d’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 inclut le tag d’authentification de 16 octets à la fin
    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
         )
    // tout échec AEAD → DECRYPT_FAILED, indissociable pour l’appelant
    return P                            // P est le SealedQubCbor interne

Effondrement des modes d’échec. Une mauvaise K, un mauvais nonce, un mismatch AAD et un chiffré falsifié produisent tous la même erreur DECRYPT_FAILED. C’est une propriété AEAD délibérée : distinguer le mode d’échec créerait un canal latéral qu’un attaquant distant pourrait sonder en envoyant des wrappers malformés et en chronométrant la réponse. Les implémentations de référence DOIVENT effondrer tous les échecs AEAD sur une forme d’erreur unique.

13.6 Matériel de clé et distribution

La clé d’enveloppement K est une valeur aléatoire uniforme de 256 bits générée par qub par un CSPRNG. Les implémentations de référence la sourcent depuis :

Distribution : K DOIT être encodée en base64 URL-safe (RFC 4648 §5, sans padding) et ajoutée à l’URL de remise comme composant fragment :

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

Le fragment n’est jamais transmis à un serveur par un navigateur conforme. Les canaux de récupération (index d’historique côté serveur, envoi automatique d’e-mail opt-in) qui persistent l’URL de remise complète — fragment compris — au-delà de l’appareil de l’utilisateur sont un compromis explicite contre la posture par défaut de crypto-déchirure et DOIVENT être verrouillés par un consentement utilisateur explicite.

Perte du fragment. Si un utilisateur perd le fragment d’URL et n’a pas de canal de récupération, le qub est illisible. C’est le compromis porteur de la conception et DOIT être divulgué à l’utilisateur au moment du scellement. Le MVP renforce la mise en garde au scellement avec une copie explicite « enregistrez cette URL » et un canal de récupération par e-mail vérifié pour les utilisateurs qui s’y inscrivent.

13.7 Hors de la portée de cette section

13.8 Les qubs publics (omission du wrapper)

Le wrapper externe est facultatif à la couche de remise. Un créateur peut sceller un qub comme public, auquel cas le SealedQubCbor canonique est écrit sur le stockage permanent directement, sans couche OuterWrapper ni clé K :

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

Un qub public est verrouillé dans le temps mais non protégé par lien : il reste illisible jusqu’à ce que son tour drand soit publié (la couche tlock est inchangée), mais après le déverrouillage, quiconque dispose de l’arweave_tx_id peut le déchiffrer — aucun fragment d’URL n’est requis, car il n’y a pas de K. C’est le compromis délibéré pour les surfaces que le serveur doit piloter : les e-mails de notification de dévoilement, les intégrations tierces et un référencement post-dévoilement plus riche ont tous besoin d’un lien qui fonctionne sans un secret que le serveur ne détient jamais (§13.6).

Conséquences qu’un producteur DOIT prendre en compte :

Le privé (enveloppé) reste la valeur par défaut ; le public est un choix explicite du créateur, par qub.


14. Vecteurs de test

14.1 Dérivation de qub_id

Entrée :
  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, paramètres drand mainnet §14.2)
  body         = "Hello, future."  (UTF-8, 14 octets)
  title        = absent

Intermédiaire :
  body_hash  = SHA3-256("Hello, future.")
             = 76ab8b3f843c6ed4f2d0fd75b9f457b4
               ad49dd4450f9c22723ae430e3af3211d
  title_hash = [0u8; 32]   (title absent — sentinelle §4.2.1)

Séparateur de domaine (10 octets) :
  [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]

Pré-image (108 octets — V1.2) :
  domain_separator   ||  // 10 octets
  0x01               ||  // version
  0x01               ||  // content_type
  0x0000000067748580 ||  // created_at en i64 big-endian (1735689600)
  0x00000000677DC000 ||  // unlock_at en i64 big-endian (1736294400)
  0x0000000000000000 ||  // outcome_at_or_zero (outcome_at absent)
  0x000000000047A595 ||  // drand_round en u64 big-endian (4695445)
  body_hash          ||  // 32 octets
  title_hash             // 32 octets (sentinelle tout-à-zéro ; title absent)

Sortie attendue :
  qub_id = SHA3-256(preimage)
         = 3a9fcb31b750d985c262fada6d4f777f
           d6a28be831d941d85c131f5a4bbaf8a4

Les implémentations DOIVENT produire des valeurs body_hash et qub_id identiques pour cette entrée. Ce vecteur de test DEVRAIT être le premier test unitaire écrit. Les valeurs canoniques ci-dessus ont été calculées par l’implémentation de référence et DOIVENT correspondre bit pour bit. Dispositions historiques de la pré-image (pré-lancement — aucun qub en production ne dépendait de celles-ci) : le qub_id V1.0 sur 92 octets était 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0 ; le qub_id V1.1 sur 100 octets (après intégration d’outcome_at_or_zero) était b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. La V1.2 intègre drand_round et fait passer le séparateur de domaine à QUB_ID_V2.

14.2 Correspondance déverrouillage-tour

Entrée :
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

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

drand_round = 4675285

14.3 Aller-retour CBOR canonique

Les implémentations DOIVENT vérifier que serialize(parse(serialize(qub))) == serialize(qub) pour toutes les entrées valides. C’est un test de propriété, pas un vecteur unique.

14.4 PactTerms CBOR (content_type 0x03)

Entrée :
  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

Ordre canonique des clés CBOR (PactTerms) :
  "notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)

Ordre canonique des clés CBOR (PactTerm) :
  "key"(4) < "value"(6)

Ordre canonique des clés CBOR (PartyIdentifier) :
  "label"(6) < "contact"(8)

Les octets CBOR canoniques et le body_hash SHA3-256 sont calculés par l’implémentation de référence. Les implémentations DOIVENT produire un CBOR identique au niveau octet pour cette entrée.

Les implémentations DOIVENT également vérifier que serialize(parse(serialize(pact))) == serialize(pact) pour toutes les entrées PactTerms valides (test de propriété).

14.5 Vecteurs cross-langage de l’enveloppe externe

L’enveloppe externe (§13) a une fixture canonique distincte à crates/qub-core/tests/vectors/wrapper_v1.json. Chaque cas fixe un tuple (key, nonce, qub_id, sealed_cbor) en entrées hex opaques et asserte une sortie expected_wrapper_hex spécifique. Les deux implémentations de référence consomment le même fichier JSON :

La fixture fixe actuellement trois cas :

Cas Couverture
basic-text-public La forme SealedQub réaliste la plus petite ; aucun champ optionnel. Établit la forme canonique du wrapper pour un qub typique de v1.0.
with-recipient-pubkey SealedQub avec recipient_pubkey défini (chemin Phase 2). Ensemble de clés CBOR interne différent, qub_id différent.
longer-body Corps d’environ 4 KiB — exerce les préfixes de longueur CBOR multi-octets à l’intérieur de l’enveloppe interne et du chiffré externe.

Les implémentations DOIVENT produire un expected_wrapper_hex identique au niveau octet pour les entrées enregistrées. La régénération de la fixture nécessite QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors et est réservée aux changements de format délibérés.


15. Gouvernance du profil cryptographique (futur)

Cette section est informative pour la v1 et devient normative dès la première fois qu’un second algorithme entre dans l’une des primitives cryptographiques de qub.

15.1 Posture actuelle

Le protocole v1 lie exactement un algorithme par primitive :

Les vérificateurs codent actuellement en dur les longueurs de clé et de signature par primitive. Le format de fil n’expose aucune surface d’agilité.

15.2 Forme prévue

Lorsqu’un second algorithme entrera dans le protocole, le vérificateur sera configuré pour un CryptoProfile nommé (par exemple, ExqubV1) énumérant l’ensemble exact des valeurs autorisées par primitive — sig_algs, chaînes drand, versions de wrapper, types de contenu. Le profil est fixé au moment de la vérification, jamais négocié en bande. Toute valeur en dehors du profil actif est rejetée.

Cela garantit que l’ajout de ML-DSA-87 ou l’activation d’Ed25519 ne peut pas affaiblir rétroactivement les configurations de vérificateur existantes : un vérificateur v1 reste un vérificateur v1 même après la publication d’un profil v2.

15.3 Conditions de déclenchement

Promouvoir §15 au statut normatif dès que l’une des propositions suivantes est faite :

Jusque-là, §15 est un emplacement réservé qui fixe la forme de migration afin que les futures PR atterrissent sur une cible connue plutôt que de re-débattre de la surface de négociation à partir de zéro.


16. Journal de transparence et niveaux de durabilité (Conception — revue achevée)

Statut. Cette section est une spécification de conception. Les formats de fil, le hachage et le modèle de confiance ci-dessous sont normatifs pour l’implémentation, mais aucun code de journal de transparence n’a encore été livré. La revue externe W5 est achevée : §16.15 consigne les décisions résolues et les contraintes de lancement contraignantes qui en sont issues. L’implémentation peut procéder sous ces contraintes. §16 reste tournée vers l’avenir au même sens que §15 — elle fixe la cible afin que l’implémentation atterrisse sur une conception arrêtée plutôt que de re-dériver le modèle de confiance lors de la revue de code. Elle est strictement additive — chaque qub existant conserve sa transaction de stockage permanent individuelle et il n’y a aucun changement au format de fil SealedQub / QubEnvelope.

16.1 Justification et niveaux de durabilité

Aujourd’hui, la durabilité d’un qub et son engagement temporel reposent tous deux sur une unique transaction de stockage permanent par qub (§11). Cela couple la latence de scellement à la finalité du stockage permanent, fait de l’upload par qub un plafond de coût produit (ARWEAVE_DAILY_CEILING), et n’offre aucun ordonnancement à inviolabilité prouvable entre qubs. Le journal de transparence ajoute deux couches en dessous et autour de ce niveau unique :

Niveau Nom Garantie Quand
T1 Accusé synchrone R2-d’abord Plancher de durabilité — les octets scellés sont écrits sur un stockage durable avant le retour du scellement (< 300 ms p95). Chaque qub, de manière synchrone (§16.10).
T2 Inclusion par lots au journal de transparence Engagement universel en ajout seul, à inviolabilité prouvable + ordre total, ancré au stockage permanent. Chaque qub, différé + par lots (§16.5–16.7).
T3 Permanence de stockage permanent par qub Une transaction de stockage permanent individuelle pour le qub. Vente incitative payante, et le repli en cas d’indisponibilité du stockage permanent (§16.8).

T2 fait du stockage permanent par qub un choix (T3) plutôt que l’unique chemin de durabilité. ARWEAVE_DAILY_CEILING est retiré comme plafond produit et rétrogradé en disjoncteur sur le seul portefeuille d’ancrage dédié (§16.7) ; les scellements des utilisateurs ne sont jamais rejetés pour l’avoir dépassé.

Honnêteté sur la durabilité (résolu — §16.15 Q6). La durabilité ne régresse pas : l’écriture T1 sur R2 est synchrone et à écriture unique, donc un qub de palier gratuit qui n’a pas acheté T3 est pleinement durable à l’instant où le scellement retourne. Ce qui se grossit, c’est le temps d’engagement borne-supérieure prouvable : pour un qub gratuit, il devient l’heure du bloc d’ancrage plutôt que l’heure du bloc d’une transaction par qub. À faible volume — l’état réaliste du lancement précoce et des heures creuses — la cadence quotidienne complète est le plancher typique, pas un cas limite rare. Le cadrage produit est donc une borne supérieure sans aucune latence numérique engagée — « scellé et durable maintenant ; un horodatage public indépendant est ajouté au prochain ancrage de journal (en général quotidien) » — et la preuve d’engagement à l’heure exacte est une propriété T3 payante, divulguée sur la surface de comparaison des niveaux et dans les conditions (§16.11, §16.15 Q6). Toute borne temporelle est un SLO interne seulement, jamais un SLA commercialisé.

16.2 Structure du LogLeaf (deux formes engagées)

Une entrée de journal est un LogLeaf, encodé en CBOR canonique écrit à la main sous le profil §3.1 (longueur définie, sans tags, sans flottants, entiers en forme la plus courte, texte NFC, champs optionnels omis lorsqu’ils sont absents, clés triées par longueur d’octets encodés croissante puis lexicographiquement). Le garde-fou canonique §3.1 analyser → ré-encoder → comparer est appliqué sur le chemin d’encodage avant le hachage (et pas seulement au décodage), de sorte que deux implémentations ne peuvent diverger sur les octets de la feuille par une différence de largeur d’entier ou d’ordre de clés. Tous les entiers sont des u8 / u64 / i64 ; tous les condensats sont des chaînes d’octets de 32 octets (bstr[32]). Un identifiant de transaction de stockage permanent enregistré est un condensat SHA-256 brut de 32 octets porté comme bstr[32], jamais une chaîne de texte base64url (conforme à §3.3).

La feuille a deux formes sélectionnées par un octet kind, parce que sur le chemin d’upload par défaut le Worker est aveugle aux octets : POST /api/v1/upload ne reçoit que qub_id et unlock_at comme assertions client non fiablesbody_hash, drand_round, created_at et drand_chain_version sont tous scellés à l’intérieur de l’enveloppe externe §13, dont le Worker ne détient jamais la clé. Seul le chemin de scellement côté serveur (POST /api/v1/seal) dérive body_hash / drand_round du texte clair. Une forme de feuille unique portant body_hash + drand_round engagerait donc des valeurs que l’opérateur n’a jamais vérifiées pour la majorité des qubs réels. La séparation maintient chaque valeur engagée honnête :

Clé Long. enc. Type Présence Signification
seq 4 u64 requis Index global de feuille à base 0 ; la position à laquelle la preuve d’inclusion s’engage.
kind 5 u8 requis 0x01 attesté (scellement-serveur) ou 0x02 asserté (scellement-client / upload aveugle aux octets).
ref 4 bstr[32] requis Identifiant de référence de la feuille. Attesté → qub_id brut. Asserté → l’identifiant aveuglé SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1).
chash 6 bstr[32] requis Adresse de contenu SHA3-256(stored_bytes) — le seul lien de contenu que le Worker peut toujours calculer honnêtement, sur les deux chemins.
unlock_at 10 i64 requis Copié (attesté) ou asserté (asserté) ; validé > 0 avant d’entrer dans la feuille.
received_at 12 i64 requis Horloge murale du Worker à l’accusé R2. Non probant (asserté par l’opérateur ; §16.6). Présent pour l’auto-description, jamais une preuve. Validé > 0.
body_hash 10 bstr[32] kind=0x01 seulement Omis sur 0x02 — le Worker ne le détient pas sous §13.
drand_round 12 u64 kind=0x01 seulement Omis sur 0x02.

Une feuille kind=0x02 n’engage délibérément ni body_hash ni drand_round : elle atteste l’engagement et l’ordonnancement d’un chiffré opaque à l’adresse de contenu chash, revendiquant qub_id et unlock_at — pas son texte clair ni son tour. Les jambes texte-clair/tour d’un qub asserté proviennent de la vérification de bundle .qub §11 existante, pas du journal (§16.11). drand_chain_version n’est pas dans la feuille (il est à l’intérieur du wrapper sur le chemin par défaut) ; la granularité de chaîne réside sur l’ancre (§16.7). Discipline de l’encodeur : rejeter un ref ou chash tout-à-zéro, et rejeter un unlock_at / received_at non positif, reflétant le garde sentinelle outcome_at > 0 dans cbor.rs.

16.2.1 Aveuglement des qubs privés

Le journal ne doit pas devenir l’oracle d’énumération que l’enveloppe externe §13 existe précisément pour empêcher (§13.1). Pour un qub privé (enveloppé), la feuille asserted engage l’identifiant aveuglé SHA3-256(qub_id ‖ log_blind_secret), où log_blind_secret est un secret détenu par le serveur, et omet body_hash. Un tiers ne peut pas lier une telle feuille à un qub_id spécifique ; le détenteur du qub, qui a l’URL de remise et donc qub_id, peut recalculer l’aveuglement pour confirmer sa propre inclusion. Un qub public (déjà énumérable, portant déjà l’étiquette de stockage permanent Visibility: public selon §13.8) engage le qub_id brut. C’est le seul endroit où la vérifiabilité autonome cède délibérément à un invariant de confidentialité porteur ; le lien autonome pour les qubs privés est chash (§16.9).

Garde de log_blind_secret (résolu — §16.15 Q4). L’aveuglement protège la non-liabilité des feuilles, pas la confidentialité du texte clair (le wrapper §13 assure celle-ci indépendamment). En cas de compromission de log_blind_secret, pour tout qub_id que l’adversaire détient déjà ou peut reconstruire (chaque qub dont il a le bundle/l’URL, plus tout qub_id à faible entropie ou public), il recalcule la ref de la feuille en un seul hachage et la lie — c’est une liaison directe d’une population connue, pas une recherche par force brute sur un espace inconnu. Classer log_blind_secret comme un secret de niveau corrélation/Sybil dans le même palier de garde que les autres secrets serveur, et le faire tourner uniquement vers l’avant (une rotation ré-aveugle les feuilles futures ; elle ne peut pas dé-lier rétroactivement celles déjà ancrées).

16.3 Hachage des feuilles et des nœuds

Hachage à séparation de domaine RFC 6962 §2.1, SHA-256 remplacé par SHA3-256 :

leaf_hash      = SHA3-256(0x00 || canonical_cbor(LogLeaf))
node_hash(l,r) = SHA3-256(0x01 || l || r)
empty tree     = SHA3-256("")          // défini mais jamais ancré

Les octets de préfixe de domaine 0x02 (chaîne d’entrées, §16.4) et 0x03 (hash STH, §16.6) sont réservés et disjoints de ceux-ci. Ce sont des octets uniques et ne peuvent donc pas entrer en collision avec les séparateurs de domaine ASCII de 10 octets existants (QUB_ID_V2, etc.). L’arbre est l’arbre RFC 6962 déséquilibré complet à gauche (chaque scission interne au plus grand puissance de deux strictement inférieure au nombre de feuilles du sous-arbre), ce qui permet aux preuves d’inclusion et de cohérence de partager un unique algorithme de chemin d’audit. La spécification de référence porte un pseudocode explicite de dérivation gauche/droite et fixe un vecteur de test non-puissance-de-deux (5 feuilles) afin que le cas de promotion de bord droit — qu’un vecteur à 4 feuilles masque — soit exercé.

16.4 Chaînage de hachage (interne)

Le LogDO maintient une chaîne d’entrées interne à des fins de cohérence-après-crash uniquement. Elle n’est jamais publiée et jamais exposée au vérificateur :

entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1]  = SHA3-256("QUB_TLOG_GENESIS_V1")

L’autorité en ajout seul publiée est la racine de Merkle cumulative + son ancre (§16.5–16.6), jamais l’ordre brut dans lequel l’opérateur se trouve servir les feuilles : la chaîne se recalcule pour tout ordre servi, donc seule la racine ancrée fixe la position canonique.

16.5 Arbre de Merkle cumulatif et regroupement par lots

Il y a un unique arbre RFC 6962 en croissance perpétuelle sur toutes les feuilles dans l’ordre seq — pas des arbres isolés par lot. (Une construction par chaînage de feuilles-de-report par lot a été rejetée : ce n’est pas une vraie relation de préfixe, donc ses « preuves de cohérence » sont infondées.) L’arbre cumulatif donne de véritables preuves de cohérence RFC 9162 et permet à une seule ancre récente de prouver l’inclusion de tout qub plus ancien.

Le Durable Object LogDO est l’unique rédacteur (blockConcurrencyWhile, à l’image de QuotaDO / EntitlementDO) — ajouter à un journal partagé est une opération lecture-modification-écriture sur un état partagé et DOIT donc passer par un DO, jamais par KV. Il met en cache la frontière du bord droit de l’arbre (O(log n) hachages) afin que la clôture d’un lot soit en O(batch). Un lot est l’ensemble des feuilles ancrées ensemble ; ses déclencheurs sont configurables, non figés dans le protocole : une avance de tree_size d’au moins LOG_BATCH_MAX_LEAVES (défaut 4096), ou un âge atteignant la cadence d’ancrage, ou un vidage forcé lorsqu’un scellement T3 payant atterrit. root_i est le hash d’arbre de Merkle cumulatif sur les feuilles 0 .. tree_size_i.

16.6 Tête d’arbre signée via ancre de stockage permanent

La transaction d’ancrage de stockage permanent est la tête d’arbre signée (Signed Tree Head) et remplace une signature de l’opérateur pour la tête d’arbre elle-même : l’ancre quotidienne n’a besoin d’aucune clé qub parce que le champ owner de la transaction de stockage permanent est la signature. La thèse du fossé tient — le substrat immuable, et non un secret détenu par qub, est porteur pour la racine ancrée.

Il existe exactement une clé de signature qub à chaud dans la conception, et elle est épinglée : la clé de reçu par scellement (§16.10). Sa clé publique est engagée dans le LogProfile (distribué avec le vérificateur) et contre-signée par anchor_owner, de sorte qu’un vérificateur valide un reçu contre la même racine épinglée que l’ancre. C’est la résolution de §16.15 Q2 — une clé de reçu non épinglée et rotatable par l’opérateur serait répudiable (l’opérateur pourrait nier que la clé était la sienne), ce qui annulerait la valeur de responsabilité du reçu face à l’adversaire de niveau opérateur que le reçu existe pour dissuader. Donc : qub ne détient aucune clé de signature de journal non épinglée ; la clé de reçu est épinglée et contre-signée par anchor_owner.

Le SignedTreeHead est du CBOR canonique (clés par longueur encodée) : size:u64, root:bstr[32], batch:u64, prev:bstr[32] (le sth_hash précédent ; genèse = 32 octets nuls), log_id:bstr[32], first_seq:u64, anchored_at:i64. Son hash est sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).

Racine de confiance épinglée. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Un vérificateur conforme DOIT exiger anchor_tx.owner == LogProfile.anchor_owner, où anchor_owner (et la clé publique de reçu) est intégré dans qub_core comme le LogProfile — aux côtés des constantes quicknet déjà présentes dans DrandTimelockProvider::quicknet() — et distribué avec le binaire du vérificateur. Le vérificateur DOIT aussi vérifier la liaison données → tx_id de la transaction de stockage permanent localement plutôt que de faire confiance à une réponse /raw/ de passerelle. Cela ferme la faille d’équivocation par portefeuille pirate : « ancré sur le stockage permanent » est dénué de sens tant que le vérificateur n’épingle pas quel portefeuille.

La rotation est une extension de la gouvernance §15, pas une réutilisation (résolu — §16.15 Q3). La surface de profil de §15.2 énumère actuellement seulement les sig_algs / chaînes drand / versions de wrapper / types de contenu, et la liste de déclencheurs de §15.3 n’en contient aucun — LogProfile / anchor_owner n’est pas encore dans la surface de §15. La gouvernance de rotation doit donc être construite : §15.3 est étendue (ci-dessous) pour ajouter le déclencheur LogProfile, et une rotation est un changement signé de LogProfile livré dans une mise à jour du vérificateur. Une rotation planifiée porte une contre-signature sortante → entrante ; une rotation motivée par une compromission ne le peut pas (la clé sortante est non fiable/indisponible précisément alors) et se replie sur le changement gouverné par §15, la vérification de fourche de l’ancre précédente (ci-dessous) bornant les dommages dans l’intervalle.

Fenêtre d’équivocation (paramètre de confiance de premier rang). Une feuille n’est résistante à l’équivocation qu’une fois son ancre couvrante confirmée sur le stockage permanent. La fenêtre est received_at → confirmation de l’ancre (≤ cadence + finalité du stockage permanent). Pendant celle-ci, les seules garanties sont le reçu de scellement épinglé (§16.10) et l’intégrité opérationnelle de qub. Trois artefacts de responsabilité rendent cela honnête plutôt qu’élusif (le modèle de témoin est la résolution de §16.15 Q2) :

  1. Reçu de scellement signé épinglé — l’analogue du SCT retourné dans la réponse d’upload (§16.10), signé par la clé de reçu épinglée et contre-signée par anchor_owner. Une feuille abandonnée avant son ancre laisse à la victime un reçu non répudiable à publier, fermant la faille d’omission silencieuse.
  2. Méthodologie de surveillance publiée + parcours de la chaîne prev — la chaîne prev de l’ancre est parcourue de la tête vers la genèse ; une fourche (deux ancres à un même size avec un root différent, ou un prev rompu) est une preuve publiable d’inconduite. La détection d’équivocation est un engagement opérationnel déclaré, pas une hypothèse silencieuse.
  3. Têtes auto-publiées en double — chaque nouvelle tête {sth_hash, tree_size} est postée sur un dépôt GitHub public en ajout seul dédié, détenu par qub (la jambe porteuse d’auto-publication à inviolabilité prouvable), avec une publication sociale comme corroboration de meilleur effort seulement. Un échec de publication DOIT déclencher une alerte (pas échouer silencieusement).

Borne d’honnêteté (contrainte contraignante). Parce que qub contrôle les deux surfaces de publication, c’est auto-publié, non témoigné de manière indépendante. Aucune surface produit, marketing ou juridique ne peut prétendre que le journal est « témoigné de manière indépendante » ; l’affirmation permise est que l’équivocation est détectable et laisse un reçu non répudiable. Un véritable témoin tiers indépendant est différé à un futur changement de gouvernance §15.

received_at est asserté par l’opérateur et aucune affirmation ne peut s’appuyer dessus — il n’est jamais présenté comme preuve ni comme corroboration de litige sur quelque surface produit / juridique / API / de rendu de preuve que ce soit. L’heure du bloc d’ancrage de stockage permanent T est le seul horodatage sans confiance (une borne supérieure sur « journalisé le »). Toute vérification de cohérence de surveillance sur received_at DOIT comparer à T, pas au champ STH anchored_at contrôlé par l’opérateur ; une telle vérification est un garde-fou contre un bug d’horloge d’un opérateur honnête seulement, pas un contrôle de responsabilité contre un opérateur malveillant (§16.15 Q5).

16.7 Format et cadence de la transaction d’ancrage

L’AnchorBundle est le corps de transaction de stockage permanent en CBOR canonique, écrit via le bundler §16.8 : ver:u8, sth:bstr (octets SignedTreeHead canoniques), prev_anchor:bstr (identifiant de transaction de l’ancre précédente en octets bruts ; omis à la genèse), chain_hash:tstr (la chaîne drand en vigueur — quicknet), et le flux de feuilles-CBOR du lot dans l’ordre seq afin que l’ancre soit autonome : un surveillant re-dérive root à partir du corps sans aucune dépendance à qub. (Si le flux de feuilles devient volumineux à haut volume, une révision future pourra n’engager qu’une plage de feuilles par référence ; noté, non adopté en v1.)

Les étiquettes de stockage permanent sont intentionnellement énumérables — le journal est fait pour être trouvé, contrairement aux qubs privés : App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. Les étiquettes sont des indices non fiables ; le corps CBOR est l’unique autorité.

Cadence : quotidienne par défaut, révisée selon le volume (le déclencheur de taille raccourcit automatiquement la cadence effective sous charge) ; un scellement T3 payant force une ancre afin que les clients payants n’attendent jamais un jour. Le portefeuille d’ancrage est dédié et à faible vélocité, séparé du portefeuille d’upload — il DOIT être son propre JWK (une clé distincte, pas un rôle logique sur le portefeuille d’upload) afin qu’une compromission du portefeuille d’upload ne puisse pas falsifier des ancres — avec un budget strict de transactions d’ancrage par jour (l’ARWEAVE_DAILY_CEILING rétrogradé). La posture de garde est énoncée sans détour : une clé à chaud à portée étroite avec un disjoncteur serré et un solde faible, pas « à froid » — un portefeuille qui auto-signe quotidiennement ne peut pas être à froid, et la spécification ne prétend pas le contraire.

16.8 Bundler ANS-104

Un encodeur de DataItem ANS-104 et un signeur de deep-hash maison, environ 300 lignes, Web Crypto uniquement, zéro dépendance npm (les deux SDK Turbo échouent au garde-fou de chaîne d’approvisionnement npm ci --ignore-scripts). Disposition des octets du DataItem :

signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data

La signature est le deepHash Arweave — un condensat SHA-384 récursif (exigence de fil d’Arweave, crypto.subtle.digest("SHA-384")) sur ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — puis RSA-PSS sur le deep-hash avec le JWK du portefeuille via crypto.subtle ; id = base64url(SHA-256(signature)). Le SHA-384 ici est mis en quarantaine comme primitive Arweave-fil-seulement, jamais une primitive de confiance qub (§15 consigne la cloison ; le hachage de confiance qub est SHA3-256 de bout en bout).

Un unique chemin de code sert trois consommateurs : la permanence T3 par qub payante, le repli en cas d’indisponibilité du stockage permanent (mettre le DataItem en file, retourner l’accusé R2-d’abord quoi qu’il arrive — cela ferme l’impasse ARWEAVE_UNAVAILABLE 503 actuelle), et l’écriture de l’AnchorBundle. Schéma de signature (résolu — §16.15 Q8) : v1 signe avec RSA-PSS (type de signature 1) en réutilisant le mécanisme de JWK de portefeuille de stockage permanent existant (zéro nouvelle garde de clé à longue durée de vie, servant la thèse « un secret de moins ») ; Ed25519 est différé au chemin de migration PQ de §15.

Le deep-hash écrit à la main est le code le plus à risque et le moins naturellement couvert de W5, donc son verrouillage est non négociable (§16.15 Q8) :

  1. La fixture cross-langage tlog_v1.json (Rust + TS, le motif wrapper_v1.json de §14.5) couvre le deep-hash, les octets + l’id du DataItem, les hachages de feuilles, une racine + chemin d’audit à 5 feuilles, un hash STH, une preuve d’inclusion et une preuve de cohérence — dans les deux directions, signature et vérification (la direction vérification importe parce que la vérification locale tx → tx_id de §16.6 tire le deep-hash dans chaque vérificateur autonome, pas seulement chez le rédacteur).
  2. Un aller-retour d’interopérabilité ponctuel à travers un bundler ANS-104 de référence, consommé comme données de test statiques uniquement — jamais une dépendance npm d’exécution (la posture Web-Crypto-uniquement / sans scripts d’installation tient).
  3. Le chemin deep-hash + RSA-PSS doit faire un aller-retour à travers les mêmes primitives crypto.subtle que la production utilise, afin que l’encodeur maison soit compatible octet pour octet.
  4. Un surveillant d’acceptation post-bundle continu confirme que chaque DataItem d’ancre / de repli atteint effectivement l’acceptation sur le stockage permanent, avec une alarme + un disjoncteur — parce que le deep-hash sert aussi la file de repli en cas d’indisponibilité du stockage permanent, donc une régression silencieuse remplirait cette file d’éléments rejetés par le réseau pendant la panne même qu’elle existe pour couvrir.

16.9 Preuves d’inclusion et de cohérence

Les deux sont du RFC 9162, SHA3-256, servies en CBOR canonique.

InclusionProofGET /api/v1/qub/:tx_id/proof : ver:u8, leaf:bstr (le CBOR exact de la feuille — le vérificateur recalcule leaf_hash lui-même et ne fait jamais confiance à un hash fourni), index:u64, size:u64, audit:[bstr[32]], root:bstr[32], anchor:{ txid:bstr[32], batch:u64, sth:bstr[32], log_id:bstr[32], block_height?:u64, anchored_at?:i64 }.

ConsistencyProofGET /api/v1/log/consistency?first=<size_a>&second=<size_b> : ver:u8, first_size:u64, second_size:u64, first_root:bstr[32], second_root:bstr[32], nodes:[bstr[32]], first_anchor, second_anchor. Une unique liste de clés non ambiguë, fixée par vecteur de test.

Vérification autonome (sans serveur qub, étend §11) :

1.  Analyser le bundle .qub → SealedQub ; recalculer qub_id (§4.1).
2.  Lire leaf.kind.
3a. kind=0x01 (attesté) :
      asserter leaf.ref == qub_id
      asserter leaf.body_hash   == SHA3-256(body)
      asserter leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserté) :
      asserter leaf.ref == SHA3-256(qub_id || blind)   // le détenteur fournit blind
      OU traiter ref comme opaque et lier via leaf.chash == SHA3-256(stored_bytes)
4.  Recalculer leaf_hash = SHA3-256(0x00 || leaf) ; replier `audit` selon RFC 6962
    en utilisant index/size ; exiger racine dérivée == proof.root.
5.  Récupérer anchor.txid depuis n’importe quelle passerelle ; vérifier la liaison
    données → tx_id de la transaction (ne pas faire confiance à une réponse /raw/ de
    passerelle) ; EXIGER anchor_tx.owner == LogProfile.anchor_owner.
6.  Analyser AnchorBundle ; exiger racine engagée == proof.root et size ==
    proof.size ; lire l’heure du bloc de stockage permanent T.
7.  Émettre l’affirmation cadrée par leaf.kind (§16.11).

Le stockage de service de preuves DOIT être clé par coordonnée (résolu — §16.15 Q7, précondition bloquante). La génération de preuves pour feuilles froides est neutre en correction seulement si le matériel d’audit R2 est un magasin persistant de nœuds de Merkle clé par coordonnée absolue de l’arbre (level, index) — pas des deltas de nœuds par batch. Avec un magasin clé par coordonnée, tout chemin d’audit (feuille i, taille N) est un ensemble de O(log N) GET R2 directs sans recalcul à travers les frontières de lots ; avec un magasin clé par lot, ce ne l’est pas, ce qui est la lacune de disposition de stockage que cette résolution comble. Les corps de feuilles sont de même adressables par contenu via seq. Un vecteur de test W5 DOIT prouver une feuille froide de l’ère genèse contre une racine bien plus tardive en utilisant uniquement R2 + le stockage permanent avec le stockage du LogDO effacé, afin que l’affirmation de sûreté de récupération de §16.13 soit étayée plutôt qu’assertée. Les GET R2 séquentiels en O(log N) n’ont leur place que sur le point de terminaison de preuve asynchrone — jamais sur le chemin chaud du scellement (§16.10) ni sur un cron par tick.

16.10 Ordonnancement de l’accusé R2-d’abord

La séquence de POST /api/v1/upload devient :

  1. Gardes de première moitié (auth, validation, clé de partition d’idempotence) — inchangées.
  2. De manière synchrone await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes) — le plancher de durabilité ; cela ferme aussi la course de pré-cache de W1 (auparavant un ctx.waitUntil après l’envoi sur le stockage permanent).
  3. De manière synchrone await LogDO.append(leaf) — un RPC DO en colo ; l’unique rédacteur assigne seq, étend la chaîne d’entrées et met à jour la frontière. (LME sur état partagé → DO, jamais KV.) Le RPC append ne fait que cela — le travail de clôture de lot Merkle en O(batch) s’exécute hors de ce RPC sur l’alarme du LogDO, sinon le p95 de l’append flambe à chaque LOG_BATCH_MAX_LEAVES-ième scellement.
  4. Retourner l’accusé maintenant — avec le reçu de scellement (signé par la clé de reçu épinglée, §16.6) et { tx_id, log_seq, anchor_status: "pending" }. L’éventail multi-secondes du stockage permanent est retiré du chemin critique.
  5. Un ctx.waitUntil met en file le travail différé : l’envoi par qub sur le stockage permanent (désormais meilleur effort / payant ; en cas d’échec il route vers la file de repli du bundler plutôt que de renvoyer un 503 à l’utilisateur) plus les écritures de méta provisoires existantes. La clôture de lot et l’ancrage s’exécutent indépendamment depuis l’alarme du LogDO et le cron d’ancrage quotidien. Pas de ctx.waitUntil à l’intérieur d’une boucle ; la clé de partition d’idempotence existante est préservée.

Budget de latence (résolu — §16.15 Q7). La cible < 300 ms p95 est un garde de lancement mesuré, pas une hypothèse. Le chemin critique honnête est les lectures KV de première moitié + un PUT R2 + deux Durable Objects sérialisés — le débit de quota de scellement QuotaDO existant et l’append du nouveau LogDO — donc le budget doit tenir compte de deux allers-retours DO en colo, pas d’un seul. Livrer une alarme de latence LogDO reflétant celle de QuotaDO et traiter une régression du p95 comme un bloqueur de publication.

16.11 Modèle de confiance — l’affirmation précise, cadrée par le kind de feuille

Pour kind=0x01 (attesté) : « Ce contenu — corps correspondant à body_hash, identifié par qub_id — a été engagé dans le journal en ajout seul de qub à la position seq et existait au plus tard à l’heure du bloc de stockage permanent T ; il était cryptographiquement illisible jusqu’au tour drand R = unlock_round(unlock_at). » C’est le triplet complet {liaison de tour tlock + inclusion de Merkle + racine ancrée}.

Pour kind=0x02 (asserté, le défaut) : « Un chiffré opaque d’adresse de contenu chash, revendiquant qub_id et unlock_at, a été engagé dans le journal en ajout seul à la position seq et existait au plus tard à l’heure du bloc de stockage permanent T. » Les jambes du tour et du corps sont fournies par la vérification de bundle .qub §11 existante (qub_core::unlock), pas par le journal ; ce que le journal ajoute par rapport à une simple transaction par qub est un ordonnancement à inviolabilité prouvable, un temps d’engagement borne-supérieure sans confiance, et une résistance à l’équivocation.

Les deux affirmations excluent, selon §11 : l’authorship sans sig_alg ≥ 0x01, l’intention, et la synchronisation sous la granularité de l’ancre. Aucune ne laisse une affirmation s’appuyer sur received_at.

Plafond d’affirmation (contrainte de lancement contraignante — résolu §16.15 Q1). Pour les qubs gratuits / par défaut (kind=0x02), l’affirmation cadrée kind=0x02 ci-dessus est le plafond de ce que toute surface produit, marketing, conditions ou de rendu de preuve peut asserter. Aucune surface ne peut déclarer ou laisser entendre que le journal prouve le contenu ou le tour de déverrouillage d’un qub par défaut — le journal prouve l’ordonnancement + un temps d’engagement borne-supérieure sans confiance d’un chiffré opaque. La preuve de contenu et de tour provient exclusivement de la vérification de bundle .qub §11 existante, qui est indépendante du journal. C’est un bloqueur de lancement strict sur la copie, pas une préférence stylistique ; c’est la résolution qui maintient honnête le chemin par défaut aveugle aux octets.

16.12 Versionnement et coordination W3

Il n’y a aucun saut de fil SealedQub et donc aucun saut de version de protocole (§12.1) : le journal est un side-car qui s’engage sur des champs et des octets existants, donc il n’entre pas dans l’historique de versions de protocole §12.2. Le drand_chain_version optionnel de W3 est intact et reste le seul champ SealedQub optionnel. Le journal introduit à la place ses propres espaces de version indépendants — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — reflétant l’indépendance de version de wrapper de §12.4 (le wrapper porte un octet de version indépendant de la version de protocole, et les versions de journal suivent la même séparation).

La livraison de preuve est récupérée par défaut, avec une option de ride-along. Une preuve ne peut pas exister au moment du scellement (l’ancre n’a pas encore été écrite), donc le bundle .qub au moment du scellement reste sans preuve. Le vérificateur de W7 récupère GET …/proof une fois, ou en mode entièrement hors ligne reconstruit la preuve à partir de l’AnchorBundle public via une requête de stockage permanent sur Log-Id. Le bundle .qub (W7) réserve un membre inclusion_proof optionnel — absent au scellement, peuplé par une ré-exportation post-ancrage pour l’archivage froid — suivant le même motif « optionnel, omis par défaut, additif » que le drand_chain_version de W3.

16.13 Conservation

Les fenêtres de conservation pour la queue ouverte du LogDO, le substrat R2 de service de preuves, les compteurs de disjoncteur d’ancrage et la file de repli du bundler sont spécifiées dans docs/DATA-RETENTION.md. Principe : le stockage chaud par entrée du journal (LogDO) est récupérable après ancrage ; son matériel d’audit — le magasin de nœuds de Merkle clé par coordonnée (level, index) + les corps de feuilles adressés par seq (§16.9) + les ancres de stockage permanent — est permanent. Récupérer une feuille froide du DO n’invalide jamais une preuve émise, parce qu’une preuve se résout contre ce magasin de nœuds R2 permanent et l’ancre de stockage permanent, pas contre le DO (et le vecteur de test à DO effacé de §16.9 le prouve).

16.14 Vecteurs de test

W5 livre la fixture cross-langage tlog_v1.json (§16.8) plus des vecteurs travaillés : une feuille kind=0x01 et une feuille kind=0x02leaf_hash ; la racine cumulative à 5 feuilles ; une preuve d’inclusion ; une preuve de cohérence ; un AnchorBundle ; et un id de DataItem. Ceux-ci vivent aux côtés des vecteurs d’enveloppe externe de §14.5 et sont exercés par les deux implémentations, Rust (qub-core) et TypeScript (Worker).

16.15 Décisions de revue (W5 — résolues)

La revue externe W5 (une passe de conception adversariale + l’aval du propriétaire) est achevée. Chaque décision ci-dessous est arrêtée et reflétée dans le texte de §16 ci-dessus ; les contraintes de lancement contraignantes sont reprises à la fin. L’implémentation peut procéder sous celles-ci.

  1. Honnêteté de la feuille du chemin par défaut (kind=0x02) — RÉSOLU. Livrer la séparation à deux kinds de feuille comme spécifié : kind=0x02 n’engage ni body_hash ni drand_round. Aucun champ *_body_hash sur le chemin aveugle aux octets (ce serait le plus lisible des faux signaux « vérifié » pour les intégrateurs et une commodité que §11 fournit déjà depuis le bundle). Ne pas exiger le scellement-serveur pour les qubs journalisés-attestés (cela forcerait le texte clair à travers le Worker et détruirait le fossé de crypto-déchirure). Tout court-circuit auto-descriptif relève du bundle .qub / de l’enveloppe de preuve comme champ recalculé par le vérificateur, jamais un champ de feuille. Plafond d’affirmation confirmé par le propriétaire : §16.11.
  2. Responsabilité d’équivocation / d’omission — RÉSOLU. La clé de reçu de scellement est épinglée dans LogProfile + contre-signée par anchor_owner (fermant la contradiction antérieure « pas de clé de signature » ; §16.6). Modèle de témoin au lancement : reçu épinglé + méthodologie de surveillance + parcours de la chaîne prev + têtes auto-publiées en double (dépôt GitHub public détenu par qub, social en meilleur effort), commercialisé comme détectable + reçu, jamais témoigné de manière indépendante. Un véritable témoin tiers est différé à un changement de gouvernance §15.
  3. Racine de confiance anchor-owner épinglée + rotation — RÉSOLU. Adopter l’épinglage LogProfile (§16.6) ; le vérificateur contrôle anchor_tx.owner == anchor_owner et vérifie la liaison données → tx_id de la transaction localement. La gouvernance de rotation est une extension à construire de §15 (déclencheur §15.3 ajouté), pas une réutilisation ; les rotations planifiées se contre-signent, les rotations motivées par compromission se replient sur le changement §15 avec la vérification de fourche bornant les dommages.
  4. Aveuglement de la feuille des qubs privés — RÉSOLU. Garder l’aveuglement pour les qubs privés (ref = SHA3-256(qub_id ‖ log_blind_secret)), qub_id brut pour les qubs publics (déjà §16.2.1), chash comme lien autonome. log_blind_secret est un secret de niveau corrélation/Sybil, rotation vers l’avant uniquement (§16.2.1).
  5. received_at — RÉSOLU. Le garder dans la feuille, engagé mais explicitement non probant ; jamais présenté comme preuve ni comme corroboration de litige sur quelque surface que ce soit. Toute vérification de cohérence de surveillance compare à l’heure du bloc de stockage permanent T, pas à l’anchored_at contrôlé par l’opérateur (§16.6).
  6. Synchronisation prouvable du palier gratuit — RÉSOLU (aval du propriétaire). La durabilité ne régresse pas ; seul le temps d’engagement borne-supérieure prouvable se grossit jusqu’à l’heure du bloc d’ancrage. La copie du palier gratuit n’utilise aucun SLA numérique (« …ajouté au prochain ancrage de journal, en général quotidien ») ; la preuve à l’heure exacte est une propriété T3 payante, divulguée sur la surface de comparaison des niveaux + dans les conditions (§16.1).
  7. Arbre cumulatif sur Workers — RÉSOLU. Unique arbre RFC 9162 cumulatif + LogDO à rédacteur unique à frontière en cache (marge confortable face au plafond DO d’environ 1 k écritures/s ; différer le partitionnement Merkle-de-racines-de-partition jusqu’à s’en approcher). Précondition bloquante : magasin de nœuds R2 clé par coordonnée (level, index) + le vecteur de test de feuille froide à DO effacé (§16.9) ; < 300 ms est un garde de lancement mesuré sur deux DO sérialisés (§16.10).
  8. Schéma de signature ANS-104 + deep-hash — RÉSOLU. RSA-PSS (type de sig 1, réutilisant le JWK du portefeuille d’ancrage dédié) ; Ed25519 différé au chemin PQ §15. Le deep-hash SHA-384 écrit à la main est verrouillé sur la fixture cross-impl dans les deux directions, une vérification d’interopérabilité de bundler de référence statique uniquement, l’aller-retour à crypto.subtle partagé, et le surveillant d’acceptation post-bundle sur le stockage permanent (§16.8).

Contraintes de lancement contraignantes (à reporter dans l’implémentation + la revue produit/juridique) :