Spécification du protocole qub
qub est un protocole d’engagement temporel cryptographique : un système pour sceller des mots à une date future, puis vérifier exactement ce qui a été scellé, quel tour drand en a conditionné la publication et — lorsqu’une transaction de stockage ou une preuve du journal de transparence est disponible — une borne supérieure horodatée indépendamment du moment où le texte chiffré a été engagé.
Trois primitives le rendent possible. drand est une balise de hasard décentralisée — la date de dévoilement est imposée cryptographiquement plutôt que par la bonne volonté de qub. Un stockage durable associé à un journal de transparence en ajout seul conserve les octets scellés et ancre les engagements groupés dans le stockage public permanent ; le chemin payant T3 écrit également une transaction individuelle dans le stockage permanent. ML-DSA-65 est une signature numérique post-quantique — lorsque la paternité est activée, le 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 et infalsifiable, attribuable de manière facultative et horodatable indépendamment — 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 |
|---|---|
| Release du document | 1.0.0 (protocol-v1.0.0) |
| Protocole de fil | 0x01 |
| Enveloppe externe | 0x01 |
| Date d’effet | 2026-09-23 |
| Statut | Actuel |
| Vérifié jusqu’au | 2026-09-23 |
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], // Random, generated locally
created_at: i64, // Unix seconds UTC
unlock_at: Option<i64>, // Unix seconds UTC; None while composing
visibility: u8, // 0x00 = private; 0x01 = public
content_type: u8, // 0x01 text; 0x03 pact; 0x04 verdict
plaintext: Vec<u8>, // Raw body bytes (UTF-8 for text)
sender_label: Option<String>, // Display name; V2-signed when authorship is enabled
title: Option<String>, // Plaintext countdown title; bound via title_hash
reply_to: Option<[u8; 32]>,// Parent qub_id; V2-signed when authorship is enabled
outcome_at: Option<i64>, // Optional future judgment time; bound to qub_id
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, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Derived (see §4.1)
content_type: u8, // Content type registry (see §6)
created_at: i64, // Unix seconds UTC
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // When reality renders judgment; bound to qub_id
sender_label: Option<String>, // Not in qub_id; V2-signed when authorship is enabled
reply_to: Option<[u8; 32]>,// Parent qub_id; not in qub_id; V2-signed when present
body: Vec<u8>, // UTF-8 text or canonical CBOR pact/verdict body
body_hash: [u8; 32], // SHA3-256(body) (see §4.2)
sig_alg: u8, // Signature algorithm (see §9.2)
author_signature: Option<Vec<u8>>, // Set when sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Set when sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
cosigner_signature: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}
Configuration de référence (qub texte non signé) : version = 0x01, content_type = 0x01, sig_alg = 0x00 ; les champs de signature et de contresignataire sont absents. D’autres champs de métadonnées facultatifs peuvent être présents.
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). Il s’agit de l’artefact filaire interne : la livraison publique stocke ces octets nus, tandis que la livraison privée les enveloppe dans OuterWrapper avant stockage (§13).
SealedQub {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Same as QubEnvelope.qub_id
visibility: u8, // 0x00 = private/wrapped; 0x01 = public/bare
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // Surfaced on the verdict-watch CTA
// before reveal; mirrors QubEnvelope.outcome_at;
// bound to qub_id via the §4.1 preimage.
drand_chain_id: String, // drand chain hash (hex string)
drand_round: u64, // Target drand round number
drand_chain_version: Option<u8>, // W3 — chain-migration version. Absent / 0 = quicknet
// (the only chain today). Lets a future chain swap
// be expressed on the wire without a breaking format
// change. NOT part of the §4.1 qub_id preimage, so its
// addition never alters an existing qub's identity.
tlock_ciphertext: Vec<u8>, // tlock-encrypted QubEnvelope CBOR bytes
recipient_pubkey: Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
// but not interpreted by the v1 reference viewer
title: Option<String>, // Plaintext title surfaced on the viewer
// countdown before reveal. Bound to qub_id
// via title_hash (§4.1). 1..=100 NFC code
// points, no hostile/control code points.
}
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>, // Carried from both wire layers; drives the verdict-watch block
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Carried forward from SealedQub.title
reply_to: Option<[u8; 32]>,
body: Vec<u8>,
body_hash: [u8; 32],
body_hash_verified: bool,
author_signature: Option<Vec<u8>>,
author_pubkey: Option<Vec<u8>>,
signature_verified: Option<bool>,
cosigner_pubkey: Option<Vec<u8>>,
cosigner_signature: Option<Vec<u8>>,
cosigner_verified: Option<bool>,
}
3. Profil CBOR 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 encoded bytes)
"qub_id" (7 encoded bytes)
"sig_alg" (8 encoded bytes)
"version" (8 encoded bytes)
"reply_to" (9 encoded bytes) ← only if present (reply chains)
"body_hash" (10 encoded bytes)
"unlock_at" (10 encoded bytes)
"created_at" (11 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (verdict mechanic)
"content_type" (13 encoded bytes)
"sender_label" (13 encoded bytes) ← only if present
"author_pubkey" (14 encoded bytes) ← only if present
"cosigner_pubkey" (16 encoded bytes) ← only if present (pact cosign)
"author_signature" (17 encoded bytes) ← only if present
"cosigner_signature" (19 encoded bytes) ← only if present (pact cosign)
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 encoded bytes) ← only if present
"qub_id" (7 encoded bytes)
"version" (8 encoded bytes)
"unlock_at" (10 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (verdict mechanic)
"visibility" (11 encoded bytes)
"drand_round" (12 encoded bytes)
"drand_chain_id" (15 encoded bytes)
"recipient_pubkey" (17 encoded bytes) ← only if present
"tlock_ciphertext" (17 encoded bytes)
"drand_chain_version" (20 encoded bytes) ← only if present (W3; absent = quicknet)
PactTerms (corps de pacte, content_type 0x03) :
"notes" (6 encoded bytes) ← only if present
"terms" (6 encoded bytes)
"title" (6 encoded bytes)
"party_a" (8 encoded bytes)
"party_b" (8 encoded bytes)
"pact_version" (13 encoded bytes)
PactTerm (ligne du tableau terms) :
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier (map party_a / party_b) :
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 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" || // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
version || // u8 (1 byte)
content_type || // u8 (1 byte)
created_at || // i64 big-endian (8 bytes)
unlock_at || // i64 big-endian (8 bytes)
outcome_at_or_zero || // i64 big-endian (8 bytes; 0 when outcome_at is absent)
drand_round || // u64 big-endian (8 bytes)
body_hash || // [u8; 32] (32 bytes)
title_hash // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output
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 : une révision de l’implémentation antérieure à la release 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 : une révision ultérieure de l’implémentation antérieure à la release 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 :
- Modifier n’importe quel champ lié par la pré-image —
version,content_type,created_at,unlock_at,outcome_at,drand_round, les octets bruts debody(viabody_hash) outitle(viatitle_hash) — produit unqub_iddifférent. - Le qub_id est calculé avant le chiffrement. QubEnvelope et SealedQub portent le même qub_id. Le lecteur vérifie qu’ils correspondent après déchiffrement.
qub_idne dépend pas desender_label,reply_to, des octets de signature ou des clés publiques de signature. Dans la construction de signature V2 actuelle,sender_labeletreply_tosont toutefois authentifiés directement parsender_label_hashetreply_to_or_zero(§9.3) dès lors que des signatures sont présentes.- Modifier le
titledu SealedQub (tout le reste fixé) modifiequb_idviatitle_hash. Une passerelle ne peut donc pas échanger le titre en clair affiché sur le compte à rebours sans invalider l’identité du qub. - Modifier l’
outcome_atdu SealedQub (tout le reste fixé) modifiequb_idvia la pré-image. Une passerelle ne peut pas échanger la date de verdict pré-dévoilement affichée sur le compte à rebours sans invalider l’identité du qub. - Modifier
drand_round(tout le reste fixé) modifiequb_idvia la pré-image. Une passerelle ne peut pas relier le texte chiffré du verrou temporel à un tour différent sans invalider l’identité du qub ; combiné à la vérification du tour de strophe au déverrouillage (§8), l’unlock_ataffiché est le tour qui conditionne réellement le déchiffrement.
4.2 body_hash
body_hash = SHA3-256(body)
Où 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) if title is present
title_hash = [0u8; 32] if title is absent
Où 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 = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
| 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 |
Il s’agit de la correspondance tlock de référence, celle de CurrentRound de drand. drand publie le tour N à chain_genesis_time + (N - 1) * chain_period_seconds. La formule sélectionne donc le tour actuel à unlock_at — le tour dont la signature est la première qu’un lecteur arrivant à unlock_at peut utiliser.
Propriété d’alignement — le cas important en pratique : lorsque (unlock_at - chain_genesis_time) est exactement divisible par chain_period_seconds, la signature du tour sélectionné est publiée exactement à unlock_at, jamais avant. Cela est toujours vrai pour le déploiement de référence : l’heure de genèse de quicknet (1692803367) est divisible par sa période de 3 secondes, et les applications de référence calent les heures de déverrouillage sur des minutes entières. Pour un unlock_at non aligné, la signature du tour sélectionné est publiée strictement moins d’une période avant unlock_at — la précision temporelle de l’engagement est d’une période de balise.
Ancienne correspondance antérieure à la release et tolérance au déverrouillage : la correspondance d’origine était ceil((unlock_at - chain_genesis_time) / chain_period_seconds). Dans le cas aligné ci-dessus, elle sélectionnait le tour publié une période entière avant unlock_at, rendant le texte chiffré déchiffrable exactement une période trop tôt. Les deux correspondances diffèrent exactement de +1 lorsque la différence est divisible par la période, et coïncident sinon. Comme drand_round est intégré à la pré-image immuable de qub_id (§4.1), les artefacts scellés avec l’ancienne correspondance ne peuvent pas être dérivés à nouveau ; les vérificateurs qui effectuent le contrôle croisé du tour à l’étape 6a du §8 DOIVENT donc accepter un drand_round stocké égal soit au tour dérivé, soit au tour dérivé moins un, et DOIVENT exiger que le tour de la strophe tlock soit exactement égal au tour stocké. La tolérance avance au plus d’une période la première signature de conditionnement. Le service de préparation des pactes applique la même tolérance lorsqu’il dérive à nouveau le qub_id d’un pacte préparé : si le tour de la correspondance actuelle ne reproduit pas le qub_id engagé et que la différence est divisible par la période, il réessaie avec le tour moins un et scelle le pacte finalisé sur le tour auquel le qub_id est effectivement lié — jamais aveuglément sur le tour recalculé, ce qui rendrait l’artefact définitivement impossible à dériver.
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() |
Artefact filaire interne ; stocké nu pour la livraison publique ou enveloppé pour la livraison privée, puis récupéré 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
// Production code — only through CBOR serialisers:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);
// There is deliberately NO From<Vec<u8>> implementation.
// You cannot accidentally wrap arbitrary bytes in a wire format type.
// Accessing raw bytes:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();
5.2 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 for structured/v1
title: String, // ≤ 200 bytes, NFC
terms: Vec<PactTerm>, // ≤ 20 rows
party_a: PartyIdentifier, // initiator
party_b: PartyIdentifier, // counter-signer
notes: Option<String>, // ≤ 5,000 bytes, NFC; absent key if none
}
PactTerm { key: String (≤ 100 bytes), value: String (≤ 2,000 bytes) } // NFC
PartyIdentifier{ label: String (≤ 100 bytes), contact: Option<String (≤ 320 bytes)> }
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 for structured/v1
outcome: u8, // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
reflection: Option<String>, // ≤ 2,000 bytes NFC; "what changed, what did you learn"
evidence_url: Option<String>, // ≤ 2,048 bytes; HTTPS only; absent key when omitted
}
Ordre canonique des clés CBOR :
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
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 :
- 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é. - Plafond de longueur. ≤ 2 048 octets (limite pratique des URL dans le navigateur).
- NFC + contrôle des codepoints hostiles. Même règle que
titleetreflection— les codepoints de bidi-override / largeur nulle / bloc tag / BOM / C0 / C1 sont rejetés. La définition correspond à la fonction Rustcrate::handle::contains_hostile_text_codepointet à la fonction TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(à garder en synchronisation). - Aucun blanc, aucun caractère de contrôle ASCII. Tout blanc / DEL / octet sous
0x20n’importe où dans l’URL est rejeté — referme le vecteur d’injection\n/\tque la règle bidi ne couvre pas. - 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. User composes plaintext and metadata in ComposeQub.
2. Validate:
a. body is non-empty.
b. body size ≤ max for content_type and user tier (see §6).
c. unlock_at is in the future.
d. unlock_at ≤ created_at + 10 years.
e. content_type is a known, supported value.
f. visibility is 0x00 (private) or 0x01 (public).
3. Compute body_hash = SHA3-256(body).
4. Set created_at = current Unix seconds UTC.
5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
compute drand_round = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
(§4.3). (Computed here, before qub_id, because drand_round is bound into the
qub_id preimage—§4.1.)
6. Compute qub_id (see §4.1), folding in drand_round from step 5.
7. Construct QubEnvelope with all fields.
8. Serialise QubEnvelope using canonical CBOR → bytes B.
Assert: serialised output matches canonical profile (§3).
9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C and matching qub_id, version,
visibility, unlock_at, drand_chain_id, and drand_round.
11. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12. Select the delivery shape from visibility:
a. Private (0x00): generate K = 32 random bytes and N = 12 random bytes
using a CSPRNG. Compute W = wrap_sealed_qub(SealedQubCbor,
qub_id=qub_id, key=K, nonce=N) per §13. Upload payload = W.
b. Public (0x01): upload payload = bare SealedQubCbor; do not generate K.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit the selected upload payload to the qub upload service. For a private
browser seal, the service is byte-blind to the inner SealedQubCbor and never
receives K. The Builder `/api/v1/seal` route is an explicit exception: it
receives plaintext and caller-supplied K in memory, then persists neither.
16. Receive arweave_tx_id from the service. For private delivery, construct
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (or the equivalent short-code
path). For public delivery, omit the fragment. Browsers do not transmit URL
fragments to servers, so K from the browser-seal path is not observed by
qub.social or any storage gateway.
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 d’envoi sélectionné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 — exactement l’une de announcement, thesis, prediction, letter, secret, commitment, proof ou verdict émis par le système), 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. Pour une remise privée, l’enveloppe externe (§13) chiffre l’artefact interne SealedQub reconnaissable ; récolter les enveloppes stockées et obtenir les signatures drand publiques ne suffit donc toujours pas à récupérer le corps sans K. Les tags de stockage restent délibérément des métadonnées publiques.
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. Viewer opens delivery URL. Extract arweave_tx_id from the path and retain
the optional URL fragment. Do not assume a missing fragment is an error:
public/bare delivery intentionally has no K.
2. Check denylist. If tx_id is denylisted → display block message. Stop.
3. Fetch the stored bytes (with multi-gateway fallback).
3a. Resolve the delivery shape structurally:
a. If the bytes parse as OuterWrapper, require a well-formed 32-byte K in
the URL fragment, require wrapper version 0x01, and unwrap per §13.
Any missing/malformed K or AEAD failure is a terminal error.
b. Otherwise require the bytes to parse as bare SealedQubCbor; no K is
required. If neither shape parses, report an integrity error.
4. Parse SealedQubCbor → SealedQub.
5. Validate: SealedQub.version is known (0x01), visibility is known, and the
delivery shape matches it (wrapped = 0x00 private; bare = 0x01 public).
Reject any mismatch or unknown value.
6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
6a. Round-binding check. Recompute expected_round from
SealedQub.unlock_at per §4.3. Reject unless SealedQub.drand_round ==
expected_round OR SealedQub.drand_round == expected_round - 1 (the
legacy pre-release mapping—see §4.3), AND the round baked into the tlock
ciphertext stanza (read via the age/tlock header, no signature required)
== SealedQub.drand_round exactly. The stanza round is the one that
actually gates decryption; without this check a malicious creator could
bind the ciphertext to an already-past round while displaying a future
countdown, so anyone reading the stored bytes could decrypt before
unlock_at. Implementations with no chain identity (test mocks) skip this
check.
7. Once current time ≥ SealedQub.unlock_at:
a. Fetch drand round signature for SealedQub.drand_round from drand network.
b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
8. Parse B → QubEnvelope.
9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
Fail → integrity error.
12a. Verify: QubEnvelope.outcome_at == SealedQub.outcome_at (both absent, or
both present and equal). Fail → integrity error.
12b. Content re-derivation. Recompute qub_id per §4.1 from the decrypted
fields — (QubEnvelope.version, content_type, created_at, unlock_at,
outcome_at, SealedQub.drand_round, QubEnvelope.body_hash,
title_hash(SealedQub.title)) — and verify it equals SealedQub.qub_id.
Fail → integrity error. The pairwise checks in steps 10-12a only prove
the two layers agree with EACH OTHER; a forger who rewrites a bound
field consistently on both surfaces (a pre-reveal title swap, or a
post-round body swap with a recomputed body_hash re-encrypted to the
same round under the same qub_id) passes them all. Only re-deriving
the identity from content closes this.
13. Verify: QubEnvelope.content_type is known and renderable.
Known values: 0x01 (text), 0x03 (pact), 0x04 (verdict).
Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using the appropriate renderer (see §10 for text and §6 for pact/verdict).
17. Construct RevealedQub for display.
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 | Statut |
|---|---|---|---|---|
0x00 |
Pas de signature (non signé) | — | — | Actif |
0x01 |
ML-DSA-65 (FIPS 204) | 1 952 octets | 3 309 octets | Actif |
0x02 |
Ed25519 | 32 octets | 64 octets | Constante réservée ; non prise en charge dans le protocole v1 |
Les lecteurs du protocole v1 DOIVENT rejeter toute valeur hors de {0x00, 0x01}, y compris la valeur réservée 0x02. La réservation empêche une réutilisation accidentelle ; elle ne constitue pas une activation. Son activation exige la modification gouvernée décrite au §15.
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" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 || // u8 (1 byte): MUST be 0x00 in v1.x
sender_label_hash || // [u8; 32]: SHA3-256(NFC(sender_label)),
// or 32 zero bytes when absent
reply_to_or_zero // [u8; 32]: parent qub_id, or 32 zero
// bytes when absent
)
// Total preimage: 155 bytes → 32-byte hash
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" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 // u8 (1 byte): MUST be 0x00 in v1.0
)
// Total preimage: 91 bytes → 32-byte hash
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 |
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.
- Sous la pré-image V1 retirée, une partie ayant un accès en écriture aux octets stockés pouvait échanger
sender_label(« Alice » → « Mallory ») ou re-rattacherreply_to— et re-chiffrer après le tour — sans invalider la signature de l’auteur, car aucun de ces champs n’était dans la pré-image signée. V2 couvre les deux, de sorte que tout changement de l’un ou l’autre champ fait basculer la vérification à « échec ». Comme les vérificateurs n’acceptent désormais que V2, cet échange est fermé pour chaque signature : une signature qui ne lie aucun des deux champs (c.-à-d. qui ne se vérifie que contre V1) est rejetée d’emblée plutôt que dégradée vers celle-ci. - Le
author_pubkeyà l’intérieur de l’enveloppe demeure le véritable ancrage d’identité — les lecteurs DOIVENT dériver l’identité affichée à partir deauthor_pubkey(via la couche d’attestation §9.5) plutôt que de faire confiance àsender_label.
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. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
only accepted form — the legacy V1 fallback is retired (§9.3), so a
signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
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 :
cosigner_pubkey: clé publique ML-DSA-65 du contre-signataire (Party B).cosigner_signature: signature sur la mêmesig_inputque l’auteur (§9.3).
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. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3 (V2 only — the
legacy V1 fallback is retired; all pact clients produce V2 signatures).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."
Propriétés :
- Le contre-signataire signe la
sig_inputidentique à celle de l’auteur — les deux parties s’engagent sur les mêmesqub_id,body_hashetunlock_at(et, sous V2, les mêmessender_label_hashetreply_to_or_zero). - Pour permettre au contre-signataire de reconstruire la pré-image V2 sans accès aux octets bruts de l’enveloppe, le service d’émission impose au moment de l’émission que le
sender_labeld’une enveloppe de pacte soit égal àpact_terms.party_a.labelet quereply_tosoit absent. Les deux conditions sont vraies pour tout pacte de client de référence ; les enveloppes qui les violent sont rejetées à l’émission. - La dérivation de
qub_id(§4.1) n’inclut PAS les champs du contre-signataire. Ajouter un contre-signataire à une enveloppe existante ne change pas lequb_id. - Un pacte peut être uniquement signé par l’auteur (engagement unilatéral), uniquement contresigné (inhabituel), ou les deux (preuve bilatérale complète).
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
- Titres :
#à####(pas#####ni######) - Emphase : gras (
**), italique (*), barré (~~) - Listes : ordonnées (
1.) et non ordonnées (-,*) - Citations (
>) - Code : spans en ligne (```) et blocs encadrés (`````)
- Lignes horizontales (
---) - Sauts de ligne (deux espaces en fin de ligne ou ligne vide)
- Paragraphes
10.2 Éléments interdits
| Élément | Traitement |
|---|---|
HTML brut (<div>, <script>, etc.) |
Entièrement retiré. Aucun HTML ne passe. |
Images () |
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 :
- Analyser le Markdown avec
pulldown-cmark(ou équivalent). - Parcourir l’AST et retirer tout nœud absent de l’allowlist (§10.1).
- Pour les nœuds liens : émettre l’URL en texte visible, pas en élément
<a>cliquable. - Convertir l’AST filtré en représentation intermédiaire typée (par exemple un enum
MarkdownNodeavec uniquement des variantes sûres). Le HTML brut est structurellement non représentable dans cette IR. - 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
- Profondeur de titre maximale rendue :
####(H4).#####et plus profond sont rendus en gras. - Pas de limite sur le nombre de paragraphes (les limites de taille de corps en §6 sont la contrainte).
- Blocs de code encadrés : pas de coloration syntaxique en MVP. Rendus en texte préformaté monospace.
11. Vérification tierce
Tout tiers qui détient les octets stockés — et K dans le cas d’un qub privé/enveloppé — peut vérifier l’artefact cryptographique sans la coopération de qub. Une affirmation d’existence horodatée indépendamment exige en outre soit l’inclusion vérifiée du qub dans le stockage permanent, soit une preuve vérifiée du journal de transparence du §16.
1. Obtain the stored bytes. For a private delivery, also obtain K from the
delivery-link fragment.
2. Resolve the delivery shape (§8 step 3a): unwrap OuterWrapper with K, or
accept bare SealedQubCbor. Require shape/visibility agreement.
3. Parse SealedQubCbor → SealedQub; validate protocol version, visibility,
content type, chain identity, and structural bounds.
4. Recompute expected_round from unlock_at (§4.3); require the stored round
(allowing the documented legacy minus-one case) and ciphertext-stanza round
to agree exactly as §8 step 6a specifies.
5. Obtain the drand signature for SealedQub.drand_round and verify its BLS
signature against the pinned chain public key.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify envelope/sealed equality for qub_id, unlock_at, and outcome_at.
10. Recompute qub_id from the decrypted fields, sealed drand_round, and title;
require it to equal the carried qub_id (§4.1).
11. If sig_alg != 0x00, verify the V2 author signature (§9.4). If cosigner
fields are present, verify their pairing, key separation, and signature
(§9.7).
12. For an existence-time claim, independently verify either:
a. the permanent-storage transaction's data-to-id binding, owner, block
inclusion, and block timestamp; or
b. a §16 inclusion proof through its pinned anchor and anchor block time.
13. Report each verified claim separately; do not collapse an absent storage
or anchor proof into a successful timing verdict.
Ce que la vérification prouve :
| Entrée de preuve | Ce qu’elle établit |
|---|---|
| Bundle ou artefact scellé valide + signature drand | Le corps récupéré correspond à body_hash ; les métadonnées liées dans qub_id sont intactes ; le texte chiffré est lié au tour drand déclaré ; et ce tour est écoulé. Cela n’établit pas quand le texte chiffré a été créé. |
| Signature V2 valide de l’auteur ou du contresignataire | Le ou les détenteurs des clés secrètes correspondantes ont authentifié la surface signée du §9.3. |
| Transaction de stockage vérifiée indépendamment pour ce qub | Le texte chiffré stocké exact existait au plus tard à l’horodatage de son bloc. |
| Preuve valide du journal de transparence ancré | L’affirmation propre au type de feuille du §16.11, y compris une borne supérieure pour l’heure d’engagement tirée du bloc d’ancrage. |
Ce que la vérification NE prouve PAS :
| Non-preuve | Pourquoi |
|---|---|
| Authorship | Le sender_label est décoratif. Sans sig_alg ≥ 0x01, n’importe qui aurait pu sceller ce contenu. |
| Intention | L’artefact prouve des octets et des relations cryptographiques, pas ce que le créateur entendait subjectivement. |
Engagement préexistant à partir du seul .qub |
Un créateur peut assembler un bundle valide après l’écoulement du tour lié. La signature drand intégrée prouve que le tour est écoulé, pas que le texte chiffré existait auparavant. |
| Heure exacte du bouton de scellement | L’horodatage d’un bloc de stockage ou d’ancrage est une borne supérieure vérifiable indépendamment et peut être postérieur à l’action locale de l’utilisateur. Les affirmations sealed_at / received_at ne constituent pas une preuve. |
Le journal de transparence implémenté (§16) étend la vérification à plusieurs qubs en ajoutant un ordre infalsifiable et une borne supérieure de l’heure d’engagement sans confiance — l’heure du bloc d’ancrage — cadrés par le type de feuille (§16.11). Il n’ajoute ni paternité ni intention ; pour le chemin de téléversement aveugle aux octets par défaut, il ne prouve pas lui-même body_hash ou drand_round, qui continuent de provenir des contrôles de l’artefact.
12. Versionnement et contrôle des releases
Les releases du document, le protocole de fil interne et l’enveloppe externe forment des espaces de version distincts. Ainsi, une clarification documentaire seule ne modifie pas silencieusement les octets, et une future migration du fil ne peut pas se faire passer pour une révision éditoriale.
12.1 Version de release du document
Cette spécification utilise des releases sémantiques du document (MAJOR.MINOR.PATCH) et un tag Git immuable nommé protocol-v<release>.
- PATCH : correction d’exactitude ou éditoriale qui ne modifie ni les octets conformes ni le comportement requis.
- MINOR : ajout normatif rétrocompatible, nouvelle entrée de registre ou nouveau format annexe versionné indépendamment.
- MAJOR : modification normative incompatible, notamment une nouvelle interprétation obligatoire du fil.
Le statut d’une release est Brouillon — pas encore normative —, Actuel — la seule cible d’implémentation recommandée — ou Remplacé — conservé pour la vérification historique. La route non versionnée /protocol affiche la release Actuelle ; le tag de release préserve sa source exacte et chaque locale publiée avec elle. Tout changement de statut ou de numéro de release exige la mise à jour de ce tableau et de l’historique de release dans la même modification revue.
| Release du document | Date d’effet | Statut | Protocole de fil | Enveloppe | Source |
|---|---|---|---|---|---|
| 1.0.0 | 2026-09-23 | Actuel | 0x01 |
0x01 |
protocol-v1.0.0 |
12.2 Version du protocole
Le champ version (u8) dans SealedQub et QubEnvelope identifie la version majeure du protocole.
- Les lecteurs DOIVENT rejeter les versions majeures inconnues avec une erreur claire.
- Au sein d’une version majeure connue, les décodeurs DOIVENT rejeter les clés de map inconnues (§3.1) — l’évolution du schéma passe par l’introduction d’une nouvelle
version, pas par l’ajout de clés que les décodeurs existants ignoreraient. (Des révisions antérieures de cette spécification permettaient de tolérer des champs optionnels inconnus ; cette clause est retirée — elle rendaitencode(decode(x))non injectif et ouvrait un vecteur de contenu signé caché sur les charges utiles des pactes.) - Les types de contenu (
content_type) et les schémas de signature (sig_alg) sont rattachés à une version : de nouvelles valeurs ne peuvent être introduites qu’avec une nouvelle version de protocole ou une mise à jour explicite du registre.
12.3 Historique des versions du protocole
| Version | Valeur | Description |
|---|---|---|
| v1 | 0x01 |
Remise privée/enveloppée et publique/non enveloppée ; corps texte (0x01), pacte (0x03) et verdict (0x04) ; signature V2 de l’auteur/du contresignataire avec ML-DSA-65 ; tlock sur drand quicknet ; SHA3-256. |
12.4 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.5 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 |
Actif pour la remise privée |
| — | 0x02–0xFF |
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.
Pour la remise privée, l’enveloppe de chiffrement externe ferme ce canal en interposant une couche AEAD symétrique additionnelle entre le SealedQubCbor canonique et les octets stockés. Dans le chemin de scellement du navigateur, la clé 256 bits K ne vit que dans le fragment 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. La représentation stockée d’un qub privé est donc un texte chiffré opaque dont le texte clair est irrécupérable sans l’URL que le créateur a choisi de partager. La remise publique omet délibérément cette couche (§13.8).
Effet net :
- Résistance à l’énumération pour la remise privée.
OuterWrapperreste un CBOR structuré reconnaissable — il n’est pas littéralement indissociable d’octets aléatoires —, mais son champ de chiffré masque la forme reconnaissable duSealedQubinterne. La stratégie d’agrégateur documentée consistant à « interroger GraphQL pour trouver les envois nus en forme de qub, puis les déchiffrer en masse avec les signatures drand publiques » n’aboutit pas au texte clair sans K. - Posture de confidentialité par crypto-déchirure pour le flux navigateur privé par défaut. qub.social ne peut pas déchiffrer ces artefacts stockés à partir de ses données serveur par défaut. La récupération explicite, la remise publique et le scellement serveur de confiance ont des frontières de confiance différentes, décrites séparément.
- Échelle de confidentialité à deux niveaux. Par défaut = accès contrôlé par lien (cette section). Les qubs privés chiffrés pour un destinataire (une fonctionnalité réservée pour la phase 2, pas encore spécifiée) se superposent en deuxième niveau.
13.2 Empilement
plaintext body ← QubEnvelope.body (§2.2)
↓ canonical CBOR (§3)
envelope CBOR
↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
↓ canonical CBOR (§3)
SealedQubCbor bytes ← inner wire artifact
├─ public (visibility=0x01) ───────────────▶ stored directly (§13.8)
└─ private (visibility=0x00)
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12, this section)
OuterWrapper CBOR bytes ← stored private payload
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, see §12.5
qub_id: [u8; 32], // copied from inner SealedQub; AEAD AAD
nonce: [u8; 12], // 96-bit AEAD nonce
ciphertext: Vec<u8>, // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}
Invariants des champs.
versionDOIT valoir0x01pour les octets de wrapper v1.0.qub_idDOIT être égal au champqub_iddu SealedQub récupéré après désenveloppement. Les fonctions de référencewrap_sealed_qubetunwrap_sealed_qubanalysent toutes deux le CBOR interne et imposent directement cette égalité ; la liaison AAD fait séparément échouer l’authentification si lequb_idexterne est altéré après l’enveloppement.nonceDOIT faire 96 bits (12 octets), généré à neuf par un CSPRNG pour chaque opération de wrap. Réutiliser un nonce sous la même clé permet des attaques par réutilisation de nonce AEAD qui récupèrent le texte clair ; les producteurs DOIVENT traiter les paires (key,nonce) comme à usage unique.ciphertextest la sortie d’AES-256-GCM : octets de chiffré concaténés avec le tag d’authentification de 16 octets.ciphertext.len() == SealedQubCbor.len() + 16exactement.
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 stockés du qub B | Mauvaise clé (et AAD lié indépendamment) → 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
I := canonical_cbor_decode(S) as SealedQub
require I.qub_id == Q // reject mismatched caller AAD
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C includes the 16-byte authentication tag at the end
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
require K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
require O.version == 0x01 // §12.5
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
S := canonical_cbor_decode(P) as SealedQub
require S.qub_id == O.qub_id // explicit inner/outer cross-check
return P // P is the validated SealedQubCbor
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 :
- Créateur WASM :
getrandom(WebCrypto sous le backendwasm_js). - Appelant de l’API de scellement côté serveur : son CSPRNG local ; l’appelant fournit et conserve
Ksous la formewrapper_key_b64url. Le Worker utiliseKen mémoire pour l’enveloppe mais NE DOIT PAS la persister. Cela permet à une nouvelle tentative idempotente de récupérer une réponse expurgée à l’aide de la capacité conservée par l’appelant, au lieu de dépendre d’un secret à usage unique généré par le serveur.
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
- La signature d’auteur (§9) est inchangée : les signatures sont calculées à l’intérieur du
QubEnvelopeinterne et sont récupérées après unwrap → déchiffrement tlock → analyse CBOR. - Le chiffrement par clé publique du destinataire — le champ réservé
recipient_pubkey— est une future fonctionnalité distincte du mode privé actuel, protégé par la capacité que constitue le lien enveloppé. - Le flux actuel de contresignature des pactes côté serveur émet un
SealedQubCborpublic/non enveloppé avec visibility0x01; il ne peut pas satisfaire le modèle de secret K réservé au navigateur, car le scellement final a lieu après la contresignature médiée par le serveur. Un futur producteur de pactes privés pourra utiliser la même enveloppe, qui ne tient pas compte du type de contenu interne.
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 entre directement dans le pipeline de stockage, sans couche OuterWrapper ni clé K :
SealedQubCbor bytes ──(public)──▶ stored as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ stored
Un qub public est verrouillé dans le temps mais non protégé par lien : il reste illisible jusqu’à la publication de son tour drand (la couche tlock est inchangée), mais après le déverrouillage, quiconque dispose de l’identifiant de transaction de stockage 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 liens oEmbed/intégration automatique sans fragment 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). Un qub privé peut toujours utiliser la forme explicite <qub-embed src="full_delivery_url"> lorsque l’éditeur fournit sa capacité complète contenant le fragment.
Conséquences qu’un producteur DOIT prendre en compte :
- Pas d’immunité à l’énumération. Les qubs publics renoncent par construction à la propriété d’immunité à l’énumération de §13.1. Le service d’upload de référence leur appose une étiquette de stockage permanent
Visibility: public(et à eux seuls) afin qu’ils soient intentionnellement découvrables ; les qubs privés ne portent aucune étiquette de ce type et conservent leur indissociabilité au niveau octet. - Titre en clair exposé au moment du scellement. Le champ
titlede §3.2 est en clair à l’intérieur duSealedQubCbor. Sous le wrapper, il est masqué jusqu’à ce qu’un lecteur fournisseK; sans le wrapper, il est lisible par tous sur le stockage permanent dès l’instant de l’upload, avant le déverrouillage. Les applications créateur conformes DOIVENT divulguer cela au moment du scellement. - La détection est structurelle et fait l’objet d’un contrôle croisé. Un lecteur ou embed conforme distingue les deux formes stockées par analyse : les octets qui s’analysent comme
OuterWrapperempruntent le chemin de déballage avecK; ceux qui s’analysent comme unSealedQubCbornon enveloppé sont acceptés directement. La valeur interne récupérée DOIT concorder (0x00pour enveloppé/privé,0x01pour non enveloppé/public).qub_idne lie pas visibility, mais les octets canoniques deSealedQubla portent ; les encodages internes public et privé ne sont donc pas identiques au niveau octet.
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
Input:
version = 0x01
content_type = 0x01
created_at = 1735689600 (2025-01-01 00:00:00 UTC)
unlock_at = 1736294400 (2025-01-08 00:00:00 UTC)
outcome_at = absent
drand_round = 4695446 (= floor((1736294400 - 1595431050) / 30) + 1, §4.3 mapping, drand mainnet params §14.2)
body = "Hello, future." (UTF-8, 14 bytes)
title = absent
Intermediate:
body_hash = SHA3-256("Hello, future.")
= 76ab8b3f843c6ed4f2d0fd75b9f457b4
ad49dd4450f9c22723ae430e3af3211d
title_hash = [0u8; 32] (title absent — §4.2.1 sentinel)
Domain separator (10 bytes):
[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]
Preimage (108 bytes—current protocol v1):
domain_separator || // 10 bytes
0x01 || // version
0x01 || // content_type
0x0000000067748580 || // created_at as i64 big-endian (1735689600)
0x00000000677DC000 || // unlock_at as i64 big-endian (1736294400)
0x0000000000000000 || // outcome_at_or_zero (outcome_at absent)
0x000000000047A596 || // drand_round as u64 big-endian (4695446)
body_hash || // 32 bytes
title_hash // 32 bytes (all-zeros sentinel; title absent)
Expected output:
qub_id = SHA3-256(preimage)
= 4a84e3dfaec32954949c30073f8e6506
fd3204c1bb97f9162b81c7587afe412e
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. Les anciennes dispositions de prototype antérieures au lancement — aucun qub en production ne dépendait des deux premières — utilisaient 92 octets avant outcome_at (3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0) et 100 octets après l’ajout d’outcome_at_or_zero (b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed). La disposition actuelle de 108 octets a ensuite ajouté drand_round et le séparateur de domaine QUB_ID_V2. Un ancien vecteur de 108 octets utilisait l’ancienne correspondance de tour par ceil (drand_round = 4695445) et produisait 3a9fcb31b750d985c262fada6d4f777fd6a28be831d941d85c131f5a4bbaf8a4 — toujours un qub_id valide pour cette entrée de tour, tandis que l’exemple ci-dessus suit la correspondance de tour actuelle du §4.3.
14.2 Correspondance déverrouillage-tour
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calculation:
(1735689600 - 1595431050) / 30 = 4675285.0
floor(4675285.0) + 1 = 4675286
drand_round = 4675286
Le tour 4675286 est publié à 1595431050 + (4675286 - 1) * 30 = 1735689600 — exactement à unlock_at, jamais avant. (L’ancienne correspondance ceil antérieure à la release donnait 4675285, publié à 1735689570 — 30 secondes trop tôt ; les vérificateurs acceptent cet ancien tour selon §4.3.)
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)
Input:
pact_version = 1
title = "Scooter deposit"
terms = [
{ key: "Item", value: "Honda Metropolitan scooter" },
{ key: "Price", value: "$100" },
{ key: "Deposit", value: "$10" }
]
party_a = { label: "Alice" }
party_b = { label: "Bob", contact: "bob@example.com" }
notes = absent
Canonical CBOR key order (PactTerms):
"notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)
Canonical CBOR key order (PactTerm):
"key"(4) < "value"(6)
Canonical CBOR key order (PartyIdentifier):
"label"(6) < "contact"(8)
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 :
- Rust :
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors). - TypeScript :
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test).
La fixture fixe actuellement trois cas d’enveloppe de bas niveau. Ils testent l’encodage déterministe d’OuterWrapper et l’interopérabilité AEAD indépendamment de l’invariant de forme de livraison de la §13.8 ; en particulier, le nom historique basic-text-public et sa valeur interne visibility = 0x01 ne font pas des octets enveloppés résultants une livraison publique conforme. Un producteur DOIT toujours stocker les octets internes publics nus et n’envelopper que les octets internes privés (0x00).
| Cas | Couverture |
|---|---|
basic-text-public |
Nom historique d’une fixture de bas niveau. La plus petite forme réaliste de SealedQub, sans champ optionnel ; teste uniquement les octets du wrapper et ne constitue pas une livraison stockée conforme à la §13.8. |
with-recipient-pubkey |
SealedQub avec recipient_pubkey défini (chemin futur réservé). Exerce un autre ensemble de clés CBOR internes ; le contenu distinct de sa fixture produit indépendamment un qub_id différent (recipient_pubkey lui-même ne fait pas partie de la préimage de la §4.1). |
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 :
- Signature : ML-DSA-65 (
sig_alg = 0x01; clé publique de 1 952 octets, signature de 3 309 octets) et non signé (sig_alg = 0x00). Le code réserve0x02à Ed25519, mais le protocole v1 ne l’active pas ; un vérificateur v1 DOIT rejeter toutsig_algen dehors de{0x00, 0x01}. - Timelock : drand quicknet uniquement — le hash de chaîne, la clé publique, l’instant de genèse et la période sont des paramètres réseau fixes portés par le
DrandTimelockProvider::quicknet()de référence (crates/qub-core/src/tlock.rs) etconfig/drand-endpoints.json. - Enveloppe externe : AES-256-GCM v1 uniquement (§13).
Les vérificateurs codent actuellement en dur les longueurs de clé et de signature de chaque primitive active. Les octets sig_alg et de version de l’enveloppe sont des sélecteurs explicites, mais la v1 n’effectue aucune négociation en bande et n’admet que les valeurs actives ci-dessus.
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 :
- Un second octet
sig_alg(activation d’Ed25519, ML-DSA-87, ou toute nouvelle entrée au registre §9). - Une seconde chaîne drand en usage en production.
- Une seconde version d’enveloppe externe.
- Une rotation de la racine de confiance du journal de transparence — l’adresse
LogProfile.anchor_ownerou la clé publique de reçu épinglée (§16.6).LogProfilerejoint la surface de profil gouvernée du §15.2 : une rotation est une mise à jour signée deLogProfilelivrée dans une mise à jour du vérificateur. Les rotations planifiées contresignent la transition sortant → entrant ; celles motivées par une compromission ne le peuvent pas et reposent sur cette mise à jour avec le contrôle de fourche de l’ancre précédente pour borner les dommages intermédiaires. Les espaces de version du journal de transparence (LOG_VERSION,ANCHOR_FORMAT) évoluent comme des espaces indépendants, tout comme la version d’enveloppe du §12.5 est indépendante de la version du protocole.
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é (Implémenté — révision terminée)
Statut. Cette section est implémenté (W5/UP-B1, Étapes 1–8), avec le producteur et la portée de la racine de confiance indiqués ici. Les formats de données, le hachage et les chemins de vérification sont actifs : les types Merkle de base + CBOR canonique (
qub-core), le miroir TypeScript + le bundler ANS-104 (workers/api/src/crypto/), l'auteur uniqueLogDO+ magasin de nœuds R2 clé-coordonnée, le/uploadtentative de journal-ajout, les cron quotidiens anchor + bundler-drain, leGET /api/v1/qub/:tx_id/proof(inclusion) etGET /api/v1/log/consistency(RFC 9162) points de terminaison de preuve, la preuve d'inclusion typée transportée dans le.qubbundle (§17.5), le vérificateur d'ancrage ANS-104 natif (tools/qub-verify), et le crochet à double autopublication (§16.6). Un succès/uploadest toujours R2-durable mais n'est couvert de logs que lorsqueLOG_DOest configuré et l'ajout en ligne réussit ; ce n'est qu'alors que sa réponse est portéelog_seq,receipt, etanchor_status. SiRECEIPT_SKest absent ou invalide, ce reçusig_b64urlest vide et ne fournit aucune non-répudiation. Le courant/sealet les chemins de publication de pact programment des transactions individuelles Arweave mais n'ajoutent pas de feuille de journal. Aucun code n'exécute actuellement le/uploadcommentaire proposé pour une réconciliation ultérieure après un échec d'ajout. L'examen externe W5 est terminé : §16.15 enregistre les décisions de conception et les contraintes de lancement, mais ces contraintes n'élargissent pas la couverture du producteur mentionnée précédemment. Trois éléments de confiance/déploiement restent verrouillés: (a) le portefeuille ancre dédié (ANCHOR_JWK;LogProfile.anchor_ownerest toujours le[0xAB; 32]espacement réservé); (b) la clé de signature de reçu et la correspondance de l'empreinte de clé publique (RECEIPT_SKest facultatif etLogProfile.receipt_pubkeyest actuellement vide) ; et (c) le dépôt GitHub self-published-heads + jeton (§16.6). Jusqu'à ce que les ancres/profils soient provisionnés, un vérificateur autonome rapporte l'état de la preuve honnêtement plutôt que de prétendre à une vérification entièrement ancrée et épinglée. La conception est strictement additive et il y a aucun changement auSealedQub/QubEnvelopeformat de câble.
16.1 Justification et niveaux de durabilité
Les voies de publication actuelles découplent la reconnaissance de la confirmation Arweave : elles dérivent et signent une transaction individuelle, conservent l'artefact et l'état exact de soumission dans R2, puis publient de manière asynchrone. Le journal de transparence ajoute une couche de classement indépendante pour le sous-ensemble général. /upload demandes dont LogDO append réussit :
| Niveau | Nom | Garantie | Quand |
|---|---|---|---|
| T1 | Accusé de réception synchrone R2-premier | Plancher de durabilité — les octets scellés et l'état exact de publication sont écrits dans un stockage durable avant que le succès ne soit renvoyé. | Mis en œuvre à travers les voies de publication actuelles. |
| T2 | Inclusion dans le journal de transparence par lots | Engagement en lecture seule, insensible aux falsifications + ordre total une fois inclus et ancré. | Producteur actuel : réussi LogDO ajoute de /upload; la réponse contient le tuple de reçu. Pas universel. |
| T3 | Permanence Per-qub Arweave | Une transaction individuelle Arweave pour le qub. | Actuellement préparé pour chaque publication acceptée et envoyé de manière asynchrone ; la transaction signée exacte reste dans la boîte d'envoi drainable jusqu'à sa livraison. |
Les niveaux décrivent des propriétés distinctes de preuve et de durabilité, pas le plan commercial actuel. Le code actuel planifie toujours une transaction Arweave individuelle pour chaque publication acceptée ; il n'expose pas T3 uniquement comme une option payante. Les plafonds de quota de clé API/compte restent des contrôles applicatifs distincts.
Durabilité honnêteté. L'écriture T1 est synchrone, donc une réponse réussie établit la durabilité au niveau de l'application sans attendre une passerelle Arweave. Elle n'établit pas elle-même un horodatage indépendant. Une transaction individuelle confirmée fournit sa limite supérieure de temps de bloc. Pour une réponse portant le tuple complet de reçu T2, le prochain ancrage confirmé peut fournir la preuve de journal décrite ci-dessous. Si le tuple est absent, aucune surface ne peut impliquer que ce qub est déjà dans le journal de transparence. La latence des ancrages et de la publication n'a aucun SLA numérique au niveau du protocole.
16.2 Structure des feuilles de journal (deux formes engagées)
Une entrée de journal est un LogLeaf, encodé en CBOR canonique manuscrit selon le profil §3.1 (longueur définie, sans tags, sans nombres à virgule flottante, entiers en forme la plus courte, texte en NFC, champs optionnels omis lorsqu'absents, clés ordonnées par longueur en octets encodée croissante puis par ordre des octets). Le §3.1 parse → re-encode → compare la garde canonique est appliquée sur le chemin de codage avant le hachage (pas seulement lors du décodage), donc deux implémentations ne peuvent pas être en désaccord sur les octets feuille à cause d'une différence de largeur d'entier ou d'ordre de clé. Tous les entiers sont u8 / u64 / i64; tous les digests sont des chaînes d'octets de 32 octets (bstr[32]). Un identifiant de transaction Arweave stocké est un condensé SHA-256 brut de 32 octets transporté comme bstr[32], jamais une chaîne de texte base64url (correspond à §3.3).
La feuille a deux formes sélectionnées par un kind octet, parce que le chemin de téléchargement général est aveugle aux octets : POST /api/v1/upload traite délibérément les deux formes de charge utile acceptées comme opaques et reçoit qub_id et unlock_at uniquement en tant que déclarations de client non fiables. Sur le chemin privé par défaut, body_hash, drand_round, created_at, et drand_chain_version sont également cachés à l'intérieur de l'enveloppe extérieure §13, dont la clé n'est jamais détenue par le Travailleur. Le système de type définit également une forme attestée pour un producteur qui dérive body_hash / drand_round lui-même. Le courant /seal la route a ces valeurs mais n'appelle pas LogDO, donc la production émet actuellement seulement des affirmations (0x02) provient des ajouts réussis de téléchargements généraux. La répartition maintient chaque valeur engagée honnête sans prétendre que le producteur attesté est connecté :
| Clé | Longueur Enc. | Type | Présence | Sens |
|---|---|---|---|---|
seq |
quatre | u64 |
Obligatoire | Indice de feuille global basé sur 0 ; la position à laquelle la preuve d'inclusion s'engage. |
kind |
cinq | u8 |
requis | 0x01 capable de l’être attesté (défini, non actuellement émis) ou 0x02 affirmé (téléversement aveugle par client-sceau / octet). |
ref |
quatre | bstr[32] |
requis | ID de référence de feuille. Attesté → brut qub_id. Affirmé → le aveuglé identifiant SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
six | bstr[32] |
requis | Adresse de contenu SHA3-256(stored_bytes) — le seul lien de contenu que le Travailleur peut toujours calculer honnêtement, sur les deux chemins. |
unlock_at |
dix | i64 |
requis | Copié (attesté) ou affirmé (affirmé) ; validé > 0 avant qu'il n'entre dans la feuille. |
received_at |
douze | i64 |
requis | Horloge murale de travail à R2-ack. Non probatoire (affirmé par l'opérateur ; §16.6). Présent pour l'auto-description, jamais une preuve. Validé > 0. |
body_hash |
dix | bstr[32] |
kind=0x01 seulement |
Omis sur 0x02 — le Travailleur ne le possède pas en vertu de l’article §13. |
drand_round |
douze | u64 |
kind=0x01 seulement |
Omis sur 0x02. |
A kind=0x02 la feuille ne commet délibérément aucun body_hash ni drand_round: il atteste l'engagement et l'organisation d'un texte chiffré opaque à l'adresse du contenu chash, affirmant qub_id et unlock_at — pas son texte en clair ou son tour. Les jambes en clair/tour pour un qub affirmé proviennent du §11 existant .qub-vérification du bundle, pas à partir du journal (§16.11). drand_chain_version n'est pas dans la feuille (il est à l'intérieur de l'enveloppe sur le chemin par défaut) ; la granularité de la chaîne se trouve sur l'ancre (§16.7). Discipline de l'encodeur : rejeter un tout-zéro ref ou chash, et rejeter les non-positifs unlock_at / received_at, reflétant le outcome_at > 0 garde sentinelle à l'intérieur cbor.rs.
16.2.1 Aveuglement Private-qub
Le journal ne doit pas devenir l'oracle d'énumération que le wrapper externe §13 est destiné à empêcher (§13.1). Pour un qub privé (enveloppé) le asserted la feuille engage le aveuglé identifier 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 associer une telle feuille à un élément spécifique qub_id; le détenteur du qub, qui possède l'URL de livraison et donc qub_id, peut recalculer l’aveugle pour confirmer leur propre inclusion. Un qub public (déjà énumérable, déjà portant le Visibility: public La balise Arweave selon §13.8) engage le brut qub_id. C'est le seul endroit où la vérifiabilité autonome cède délibérément à un invariant de confidentialité porteur de charge ; le lien autonome pour les qubs privés est chash (§16.9).
log_blind_secret garde (résolu — §16.15 Q4). L'aveugle protège l'inunlinkabilité des feuilles, pas la confidentialité du texte en clair (le wrapper §13 le garantit indépendamment). Sur un log_blind_secret compromis, pour tout qub_id l'adversaire détient déjà ou peut reconstruire (chaque qub dont il possède le paquet/URL, ainsi que tout à faible entropie ou public qub_id) il recompute la feuille ref dans un hachage et le relie — il s'agit d'une liaison directe d'une population connue, pas d'une force brute sur un espace inconnu. Classifier log_blind_secret comme un secret de corrélation/de niveau Sybil dans le même niveau de garde que les autres secrets du serveur, et ne tourner qu'en avant (une rotation ré-aveugle les feuilles futures ; elle ne peut pas dissocier rétroactivement celles déjà ancrées).
16.3 Hachage des feuilles et des nœuds
RFC 6962 §2.1 hachage séparé par domaine avec 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("") // defined but never anchored
Octets de préfixe de domaine 0x02 (chaîne d'entrée, §16.4) et 0x03 (Le hachage STH, §16.6) sont réservés et distincts de ceux-ci. Ce sont des octets uniques et ne peuvent donc pas entrer en collision avec les séparateurs de domaine ASCII existants de 10 octets (QUB_ID_V2, etc.). L'arbre est le RFC 6962 gauche-plein déséquilibré arbre (chaque division intérieure à la plus grande 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 algorithme de chemin d'audit. La spécification de référence contient un pseudocode explicite de dérivation gauche/droite et fixe un vecteur de test non puissance de deux (à 5 feuilles) Ainsi, le cas de promotion sur le bord droit — que masque un vecteur à 4 feuilles — est mis en œuvre.
16.4 Chaînage de hachage (interne)
Le LogDO maintient une chaîne d'entrées interne uniquement pour la cohérence en cas de panne. Il est jamais publié et jamais destiné aux vérificateurs:
entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1] = SHA3-256("QUB_TLOG_GENESIS_V1")
L'autorité publiée en mode ajout uniquement est la racine Merkle cumulative + son ancrage (§16.5–16.6), jamais l’ordre brut dans lequel l’opérateur sert les feuilles : la chaîne recalculera pour n’importe quel ordre servi, donc seul le pivot racine ancré fixe la position canonique.
16.5 Arbre Merkle cumulatif et regroupement
Il y a un arbre RFC 6962 en constante expansion sur toutes les feuilles dans seq ordre — pas des arbres par lot isolés. (Une construction par lot en chaîne à feuilles portées a été rejetée : ce n’est pas une véritable relation préfixe, donc ses « preuves de cohérence » ne sont pas fiables.) L’arbre cumulatif fournit de véritables preuves de cohérence RFC 9162 et permet à une seule ancre récente de prouver l’inclusion pour tout qub plus ancien.
Le LogDO L'objet durable est le écrivain unique (blockConcurrencyWhile, reflétant QuotaDO / EntitlementDO) — ajouter à un journal partagé consiste en une lecture-modification-écriture sur un état partagé et doit donc PASSER par un DO, jamais par KV. Il met en cache le front droit de l'arbre (O(log n) hachages) donc fermer un lot est O(batch). A lot est l'ensemble des feuilles ancrées ensemble ; ses déclencheurs implémentés sont un tree_size avance d'au moins LOG_BATCH_MAX_LEAVES (par défaut 4096), âge atteignant la cadence d'ancrage, ou une fermeture forcée explicite par l'administrateur/cron. root_i est le hachage de l'arbre de Merkle cumulatif sur les feuilles 0 .. tree_size_i.
16.6 Tête d'arbre signée via Ancre Arweave
La transaction d'ancrage Arweave est la tête d'arbre signée et remplace une signature d'opérateur pour la tête de l'arbre elle-même : l'ancre quotidienne n'a pas besoin de clé qub parce que la transaction Arweave owner est la signature. La thèse du fossé tient — le substrat immuable, et non un secret détenu par un qub, porte la charge de la racine ancrée.
La conception du journal prévoit un append réussi et chaud clé de reçu (§16.10), épinglé dans LogProfile et contresigné par anchor_owner. L'implémentation actuelle n'a pas encore terminé ce provisionnement de racine de confiance : RECEIPT_SK est optionnel, une clé absente/invalide produit sig_b64url: "", et le compilé LogProfile.receipt_pubkey est vide. Un tel reçu peut décrire la feuille jointe mais est pas un reçu signé non-répudiable. La revendication de conception plus forte ne s'applique qu'après qu'un vérificateur ait publié les broches et que le propriétaire de l'ancre les ait contresignées. Une réponse de publication sans le tuple complet du reçu ne fait aucune revendication d'acceptation dans le journal ; une réponse avec une signature vide fait une revendication de position d'ajout mais aucune revendication de vérification de signature.
Le SignedTreeHead est le CBOR canonique (clés par longueur encodée) : size:u64, root:bstr[32], batch:u64, prev:bstr[32] (antérieur sth_hash; genèse = 32 octets nuls), log_id:bstr[32], first_seq:u64, anchored_at:i64. Son hachage 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 réception) est intégrée dans qub_core comme le LogProfile — aux côtés des constantes quicknet déjà présentes DrandTimelockProvider::quicknet() — et distribué avec le binaire du vérificateur. Le vérificateur DOIT également vérifier la transaction Arweave lié localement à data → tx_id plutôt que de faire confiance à une passerelle /raw/ réponse. Cela ferme la faille d'équivoque des portefeuilles voyous : « ancré sur Arweave » n'a aucun sens tant que le vérificateur n'épingle pas quel portefeuille.
La rotation est une extension de gouvernance §15, pas une réutilisation (résolu — §16.15 Q3). La surface de profil de §15.2 énumère actuellement seulement sig_algs / chaînes drand / versions de wrapper / types de contenu, et la liste de déclencheurs de §15.3 ne contient aucun de ceux-ci — LogProfile / anchor_owner est pas pourtant à la surface de §15. La gouvernance de la rotation doit donc être construite : §15.3 est étendu (ci-dessous) pour ajouter le LogProfile déclencheur, et une rotation est signée LogProfile bump expédié dans une mise à jour du vérificateur. A prévu la rotation transporte une signature croisée sortante → entrante ; a axé sur le compromis la rotation ne peut pas (la clé sortante est non fiable/indisponible exactement à ce moment-là) et revient à la relance régie par le §15, avec la vérification du fork de l'ancre précédente (ci-dessous) limitant les dommages entre-temps.
Fenêtre d'équivoque (paramètre de confiance de première classe). Une feuille n'est résistante à l'équivoque que lorsque son ancrage couvrant est Arweave-confirmé. La fenêtre est received_at → anchor confirmation (cadence + finalité Arweave, sans garantie de latence du protocole). Avant la fourniture de la racine de confiance, l'implémentation actuelle fournit l'intégrité opérationnelle de qub ainsi que les métadonnées d'ajout non signées présentes ; elle ne fournit pas la garantie de non-répudiation prévue. Trois artefacts de responsabilité définissent la conception achevée (le modèle du témoin est la résolution de la question §16.15 Q2) :
- Reçu scellé (dépendant de l'approvisionnement) — l'analogue SCT retourné lorsqu'un ajout au journal d'un téléchargement réussit (§16.10). Il ne devient non répudiable que lorsque
sig_b64urln'est pas vide et La relation correspondante clé publique/propriétaire de l'ancre est épinglée dans le vérificateur. La broche du profil de production actuellement vide ne peut pas supporter ce verdict. Ce contrôle ne s'applique pas à un tuple de reçu omis ou à un reçu non signé. - Méthodologie de moniteur publiée + parcours de la chaîne précédente — l'ancre
prevla chaîne est parcourue tête→genèse ; une bifurcation (deux ancres en une)sizeavec différentroot, ou un casséprev) est une preuve publiable de mauvaise conduite. La détection de l'équivocation est un engagement opérationnel déclaré, et non une hypothèse tacite. - Têtes auto-publiées doubles — chaque nouvelle tête
{sth_hash, tree_size}est publié sur un site dédié appartenant à qub dépôt GitHub public et en ajout uniquement (le support auto-édité porte-charge à preuve de falsification), avec un post social seulement comme corroboration au meilleur effort. Une publication échouée DOIT afficher une page (pas échouer silencieusement). Mis en œuvre (Étape 8) comme lepublishHeadaccrocher au cron de l'ancre (workers/api/src/utils/heads-publish.ts): aPUTau contenu API sans unshaest uniquement ajout (un422signifie que la tête est déjà publiée, jamais une réécriture) ; optionnel / déployé avec contrôlePUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO}et inerte jusqu'à ce que le dépôt soit provisionné. Une panne grave de GitHub pages via lehealth_alertcanal et fait sauter un indicateur d'échec durable (m:tlog_publish_head_fail); l'ancre Arweave elle-même ne revient jamais en arrière en cas d'échec de publication. Le fait de « ne pas échouer silencieusement » est garanti par cette métrique durable — sur laquelle les opérations DOIVENT avoir un tableau de bord d'alerte — même si la page e-mail en meilleur effort ne peut pas être livrée. Deux limitations honnêtes découlent de « ancrer à l'avance » (la tâche cron ne publie que lorsque la taille avance) : un échec transitoire de GitHub laisse un écart dans la séquence des têtes publiées pour cette taille — bornée, non silencieuse (elle fait défiler), et parce que chaque tête engage un arbre surensemble, une preuve de cohérence §16.9 comble l'écart ; de manière cruciale, cette preuve de cohérence est calculée à partir de arbre faisant autorité ancré sur Arweave, pas à partir de la surface GitHub, donc un écart GitHub n'affaiblit jamais la vérifiabilité. Un rattrapage rétroactif qui comble les lacunes des têtes publiées est une amélioration différée.
Honnêteté liée (contrainte contraignante). Parce que qub contrôle les deux surfaces de publication prévues, c'est auto-édité, non témoigné de manière indépendante. Aucune surface de produit, de marketing ou juridique ne peut prétendre que le journal est "témoigné de manière indépendante". Après que les réceptions/profils/portes principales soient provisionnés, l'affirmation autorisée est que L'équivoque est détectable et un ajout signé avec succès laisse un reçu non répudiable. Avant cela, cette revendication n'est pas disponible. Un véritable témoin tiers indépendant est reporté à une future augmentation de gouvernance §15.
received_at est affirmé par l'opérateur et aucune réclamation ne peut s'y appuyer — il n'apparaît jamais comme preuve ni comme corroboration de litige sur aucun produit / juridique / API / surface de rendu de preuve. Le temps de bloc d'ancrage Arweave T est le seul horodatage sans confiance (une limite supérieure sur « enregistré par »). Toute vérification de cohérence du moniteur sur received_at DOIT comparer avec T, pas contre l'opérateur contrôlé anchored_at Champ STH ; une telle vérification n'est qu'une protection contre un bug d'horloge d'un opérateur honnête, pas un contrôle de responsabilité contre un opérateur malveillant (§16.15 Q5).
16.7 Format et Cadence des Transactions Anchor
Le AnchorBundle est le corps de la transaction Arweave canonique-CBOR, écrit via le bundler §16.8 : ver:u8, sth:bstr (canonique SignedTreeHead octets), prev_anchor:bstr (identifiant de transaction d'ancrage précédent en octets bruts ; omis lors de la genèse), chain_hash:tstr (la chaîne de drand en vigueur — quicknet), et le flux CBOR de feuilles de lot dans seq commande donc l'ancre est autonome : un moniteur redérive root à partir du corps sans dépendance qub. (Si le flux de feuilles devient important à volume élevé, une révision future pourrait ne commettre qu’une plage de feuilles par référence ; noté, non adopté dans la v1.)
Les tags Arweave sont intentionnellement énumérables — le journal est destiné à ê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 balises sont indices non fiables; le corps CBOR est la seule autorité.
Cadence : quotidiennement par défaut, revisité avec volume (le déclencheur de taille raccourcit automatiquement la cadence effective sous charge). Le producteur actuel n'implémente pas de crochet de force-ancrage payé. portefeuille Anchor est dédié et à faible vitesse, séparé du portefeuille de téléchargement — il DOIT être le sien posséder JWK (une clé distincte, pas un rôle logique sur le portefeuille de téléchargement) de sorte qu'une compromission du portefeuille de téléchargement ne puisse pas falsifier les ancres — avec un budget strict de transactions d'ancrage par jour. La posture de conservation est énoncée clairement : un touche de raccourci à portée étroite avec disjoncteur serré et faible solde, pas "froid" — un portefeuille qui signe automatiquement tous les jours ne peut pas être froid, et la spécification ne prétend pas le contraire.
16,8 ANS-104 Regroupeur
Un encodeur ANS-104 DataItem interne et un signataire deep-hash, environ 300 lignes, Web Crypto uniquement, zéro dépendance npm (les deux SDK Turbo échouent à npm ci --ignore-scripts porte de la chaîne d'approvisionnement). Disposition des octets de 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 Arweave deepHash — un récursif SHA-384 digest (exigence de fil d'Arweave, crypto.subtle.digest("SHA-384")) par-dessus ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — puis RSA-PSS sur le hash profond avec la JWK du portefeuille via crypto.subtle; id = base64url(SHA-256(signature)). Le SHA-384 ici est mis en quarantaine en tant que primitive uniquement filaire Arweave, jamais une primitive de confiance qub (§15 enregistre la clôture ; le hachage qub trust est SHA3-256 partout).
Le chemin de code ANS-104 sert la machinerie de repli/déversement différé et écrit AnchorBundle DataItems. Le chemin de publication ordinaire crée d'abord une transaction Arweave exactement signée et en conserve le JSON dans une boîte d'envoi durable ; la publication directe est une optimisation de latence, et le chemin de vidage réessaie la même transaction avant d'appliquer son secours de regroupement. Schéma de signature (résolu — §16.15 Q8) : v1 signe avec RSA-PSS (type de signature 1) réutilisation du mécanisme JWK du portefeuille Arweave existant (aucune nouvelle garde de clé à long terme, servant la thèse du « un secret de moins ») ; Ed25519 est différé vers le chemin de migration PQ §15.
Le hachis profond roulé à la main est le code à risque le plus élevé et à couverture naturelle la plus faible dans W5, donc son passage conditionnel est non négociable (§16.15 Q8) :
- Le dispositif inter-langues
tlog_v1.json(Rust + TS, le §14.5wrapper_v1.jsonle modèle) couvre le deep-hash, les octets + identifiant du DataItem, les hachages de feuilles, une racine de 5 feuilles + chemin d’audit, un hachage STH, une preuve d’inclusion et une preuve de cohérence — en à la fois les directions signer et vérifier (la direction de vérification est importante parce que la vérification locale tx → tx_id du §16.6 tire le hachage profond dans chaque vérificateur autonome, pas seulement dans l'écrivain). - Un aller-retour d’interopérabilité unique via un empaqueteur ANS-104 de référence, consommé comme données de test statiques uniquement — jamais une dépendance d'exécution npm (la posture Web-Crypto-only / sans scripts d'installation reste).
- Le chemin deep-hash + RSA-PSS doit faire un aller-retour via le même
crypto.subtleprimitifs usages de production, donc l'encodeur interne est compatible par octet. - En cours moniteur d'acceptation post-lot confirme que chaque DataItem d’ancrage / de secours atteint réellement l’acceptation par Arweave, avec une alarme et un disjoncteur — car le hachage profond sert également à la file d’attente de secours en cas d’indisponibilité d’Arweave, de sorte qu’une régression silencieuse remplirait cette file d’attente avec des éléments rejetés par le réseau pendant la panne exacte qu’elle est censée couvrir.
16.9 Preuves d'inclusion et de cohérence
Les deux sont RFC 9162, SHA3-256, servis comme CBOR canonique.
Preuve d'inclusion — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (le CBOR de feuille exact — le vérificateur recalculé leaf_hash lui-même et ne fait jamais confiance à un hachage 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 }.
Preuve de cohérence — GET /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 seule liste de clés non ambiguë, fixée par vecteur de test.
Vérification autonome (pas de serveur qub, prolonges §11) :
1. Parse .qub bundle → SealedQub; recompute qub_id (§4.1).
2. Read leaf.kind.
3a. kind=0x01 (attested):
assert leaf.ref == qub_id
assert leaf.body_hash == SHA3-256(body)
assert leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserted):
assert leaf.ref == SHA3-256(qub_id || blind) // holder supplies blind
OR treat ref as opaque and bind via leaf.chash == SHA3-256(stored_bytes)
4. Recompute leaf_hash = SHA3-256(0x00 || leaf); fold `audit` per RFC 6962
using index/size; require derived root == proof.root.
5. Fetch anchor.txid from any gateway; verify the tx data → tx_id binding
(do not trust a gateway /raw/ response); REQUIRE anchor_tx.owner ==
LogProfile.anchor_owner.
6. Parse AnchorBundle; require committed root == proof.root and size ==
proof.size; read the Arweave block time T.
7. Emit the claim scoped by leaf.kind (§16.11).
Le stockage servant de preuve DOIT être clé-coordinate (résolu — §16.15 Q7, condition préalable bloquante). La génération de preuves à feuilles froides est neutre en termes de correction seulement si le matériel d'audit R2 est un magasin de nœuds Merkle persistant indexé par coordonnée absolue de l'arbre (level, index) — pas par-batch deltas de nœuds. Avec un magasin indexé par coordonnées, tout (leaf i, size N) le chemin d'audit est un ensemble de O(log N) R2 direct GETavec pas de recomputation à travers les frontières de lots; avec un magasin à clé par lot, ce n'est pas le cas, ce qui est le vide dans la disposition du stockage que cette solution comble. Les corps des feuilles sont également accessibles par contenu via seq. Un vecteur de test W5 DOIT prouver un feuille froide de l'ère de la genèse contre une racine beaucoup plus tardive en utilisant seulement R2 + Arweave avec le stockage LogDO effacé, donc la revendication de sécurité de récupération dans le §16.13 est étayée plutôt que simplement affirmée. La O(log N) R2 séquentiel GETappartiennent seulement sur le point de terminaison de preuve asynchrone — jamais sur le chemin critique du scellage (§16.10) ni sur un cron par tick.
16.10 R2-Premier Ordre d'Ack
Le mis en œuvre POST /api/v1/upload la séquence est :
- Portes avant (auth, validation, clé de fragment d'idempotence) — inchangées.
- Créez, étiquetez et signez la transaction Arweave individuelle exacte. Cela dérive
tx_idlocalement, bien que la création de transaction puisse récupérer des métadonnées de récompense/ancre depuis une passerelle. Un échec de préparation entraîne toujours l'échec de la requête avant l'accusé de réception. - Synchroniquement écrire l'artéfact sélectionné à
qub-cache/<tx_id>et persister les enregistrements de création-opération/boîte de sortie stables. Ce sont le niveau de durabilité et de nouvelle tentative ; les échecs avant règlement renvoient 503. - Quand
LOG_DOest configuré, essayer simultanémentLogDO.append(leaf). L'écrivain unique attribueseq, étend la chaîne d'entrée et met à jour la frontière. L'appel RPC append fait seulement cela ; la fermeture par lot s'exécute hors chemin sur l'alarme. Une défaillance de transport/application append est actuellement défaillance douce: la réponse peut encore réussir sanslog_seq,receipt, ouanchor_status. Malgré un commentaire d'implémentation, aucune réconciliation automatique des journaux ultérieurs n'est mise en place aujourd'hui. - Retournez l'accusé de réception. Inclure
{ log_seq, anchor_status: "pending", receipt }uniquement lorsque l'append a renvoyé le tuple complet réussi.receipt.sig_b64urlest vide lorsque le signataire du reçu est indisponible ; les clients NE DOIVENT PAS considérer cette valeur comme signée ou non réfutable. L'absence du tuple signifie uniquement une publication durable, et non une acceptation dans le journal de transparence. - Utilisez une tâche différée pour publier la transaction signée exacte. Le succès supprime la boîte d'envoi ; l'échec la laisse pour le cron de vidage limité et ne doit pas modifier ce qui a déjà été reconnu
tx_id. Les métadonnées provisoires et autres composants auxiliaires réalisés selon les meilleures tentatives sont également reportés.
Limite de latence. Le chemin de la requête comprend le travail d'autorité/quota de la première moitié, la préparation/la signature des transactions, les écritures R2 durables, et (lorsqu'il est configuré) le LogDO tentative. < 300 ms apparaît dans la révision de conception comme un objectif opérationnel, et non comme une garantie de protocole ; l'étape actuelle de préparation de la transaction peut effectuer une demande de métadonnées de passerelle. Les alarmes de latence et les portes de lancement sont des contrôles opérationnels, et non des preuves disponibles pour un vérificateur.
16.11 Modèle de confiance — la revendication précise, limitée par le type de feuille
Pour kind=0x01 (attesté) : Ce contenu — correspondance du corps body_hash, identifié par qub_id — a été enregistré dans le journal append-only de qub à la position seq et existait au plus tard à l'heure du bloc Arweave T; c'était cryptographiquement illisible jusqu'au tour de drand R = unlock_round(unlock_at). Ceci est le complet {tlock round binding + Merkle inclusion + anchored root} triple.
Pour kind=0x02 (affirmé, par défaut) : Un texte chiffré opaque avec adresse de contenu chash, affirmant qub_id et unlock_at, a été enregistré dans le journal en mode ajout seul à la position seq et existait au plus tard à l'époque du bloc Arweave TLes jambes rondes et du corps sont fournies par le §11 existant .qub-vérification du bundle (qub_core::unlock), pas par le journal ; ce que le journal ajoute à une transaction per-qub nue, c'est un ordre infalsifiable, un engagement de temps maximal sans confiance et une résistance aux équivoques.
Les deux revendications excluent, selon le §11 : la paternité sans sig_alg ≥ 0x01, d'intention et de granularité de sous-ancrage temporel. Aucun ne permet à une réclamation de s'appuyer sur received_at.
Plafond de réclamation (contrainte de lancement contraignante — résolue §16.15 Q1). Pour un prétendu (kind=0x02) feuille, la revendication délimitée ci-dessus est la plafond sur quoi que ce soit produit, marketing, termes ou surface de rendu de preuve peut affirmer. Aucune surface ne peut déclarer ou insinuer que le journal prouve le contenu ou le tour de déverrouillage d’un téléchargement aveugle à des octets — le journal prouve l’ordonnancement + un engagement sans confiance à la limite supérieure d’un texte chiffré opaque. Le contenu et la preuve du tour proviennent exclusivement de l’existant §11 .qub- vérification de bundle, qui est indépendante du journal. Une publication sans ajout/réception réussi n'a aucune revendication de journal du tout.
16.12 Gestion des versions et coordination avec le W3
Il y a non SealedQub bossage de fil et donc pas de mise à jour de la version du protocole (§12.2) : le journal est un sidecar qui s'engage à respecter les champs et octets existants, donc il n'entre pas dans l'historique des versions du protocole §12.3. Optionnel selon W3 drand_chain_version est intact et reste le seul optionnel SealedQub domaine. Le journal introduit plutôt ses propres espaces de version indépendants — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — reflétant l'indépendance de la version du wrapper §12.5 (le wrapper porte un octet de version indépendant de la version du 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 d'accompagnement. Une preuve ne peut pas exister au moment du scellement (l'ancre n'a pas encore été écrite), donc le moment du scellement .qub le paquet 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 du public AnchorBundle via une requête Arweave sur Log-Id. Le .qub le package (W7) réserve un optionnel inclusion_proof membre — absent lors du sceau, peuplé par une réexportation après ancrage pour archivage à froid — suivant le même schéma « facultatif, omis par défaut, additif » que celui de W3 drand_chain_version.
16.13 Rétention
Les fenêtres de rétention pour le LogDO open tail, le substrat de service de preuves R2, les compteurs du disjoncteur d'ancrage et la file de secours du bundler sont spécifiées dans docs/DATA-RETENTION.mdPrincipe : le stockage à chaud par entrée du journal (LogDO) est récupérable après ancrage ; son matériau d'audit — la clé coordonnée (level, index) Magasin de nœuds Merkle + le seq-les corps de feuilles adressées (§16.9) + les ancres Arweave — sont permanents. La récupération d'une feuille froide depuis le DO n'invalide jamais une preuve délivrée, parce qu'une preuve se résout contre ce magasin de nœuds R2 permanent et l'ancre Arweave, pas contre le DO (et le vecteur de test §16.9 du DO effacé le prouve).
16.14 Vecteurs de test
W5 expédie le dispositif multilingue tlog_v1.json (§16.8) vecteurs supplémentaires travaillés : a kind=0x01 et un kind=0x02 feuille → leaf_hash; la racine cumulative à 5 feuilles ; une preuve d'inclusion ; une preuve de cohérence ; une AnchorBundle; et un identifiant DataItem. Ceux-ci coexistent avec les vecteurs de l'enveloppe externe §14.5 et sont utilisés à la fois par le Rust (qub-core) et les implémentations TypeScript (Worker).
16.15 Décisions d'examen (S5 — résolu)
La révision externe W5 (un passage de conception contradictoire + approbation du propriétaire) est terminée. Chaque décision ci-dessous est réglée et reflétée dans le texte §16 ci-dessus ; le contraintes de lancement obligatoires sont réaffirmés à la fin. La mise en œuvre peut se poursuivre selon eux.
- Chemin par défaut (
kind=0x02) honnêteté de la feuille — RÉSOLU. Expédiez le type à deux feuilles divisé comme spécifié :kind=0x02ne s'engage nibody_hashnidrand_round. Non*_body_hashchamp sur le chemin aveugle aux octets (ce serait le signal faux « vérifié » le plus lisible pour les intégrateurs et c’est une commodité que le §11 fournit déjà depuis le paquet). Faire pas exiger un sceau de serveur pour les qubs attestés par journal (ce qui obligerait le texte en clair à passer par le Worker et détruirait le fossé de destruction cryptographique). Tout court-circuit auto-descriptif appartient dans le.qubpaquet / enveloppe de preuve en tant que champ recalculé par le vérificateur, jamais un champ feuille. Plafond de réclamation confirmé par le propriétaire : §16.11. - Responsabilité pour équivocation / omission — CONCEPTION RÉSOLUE, APPROVISIONNEMENT INCOMPLET. La conception exige que la clé de réception du sceau soit verrouillée en place
LogProfileet contresigné paranchor_owner, plus la méthodologie de surveillance, le parcours de la chaîne précédente et les têtes auto-publiées doubles. Le profil compilé et les hooks de déploiement sont encore des espaces réservés/options comme détaillé au §16.6, donc la revendication plus forte détectable + reçue n'est pas actuelle tant que ces étapes ne sont pas clôturées. Il ne doit jamais être commercialisé comme témoigné de manière indépendante. Un véritable témoin tiers est renvoyé à une mise à jour de gouvernance §15. - Racine de confiance du propriétaire de l'ancre épinglée + rotation — RÉSOLU. Adopter le
LogProfilegoupille (§16.6) ; le vérificateur vérifieanchor_tx.owner == anchor_owneret vérifie localement la liaison des données tx → tx_id. La gouvernance de rotation est un §15 extension à construire (§15.3 déclencheur ajouté), pas une réutilisation ; les rotations planifiées se signent mutuellement, les rotations dictées par des compromis reviennent au §15 avec le contrôle de la fourche pour limiter les dégâts. - Cécité de la feuille Private-qub — RÉSOLU. Continue à aveugler pour les qubs privés (
ref = SHA3-256(qub_id ‖ log_blind_secret)), cruqub_idpour les qubs publics (déjà §16.2.1),chashcomme la cravate autonome.log_blind_secretest un secret de corrélation/de niveau Sybil, rotation vers l'avant uniquement (§16.2.1). received_at— RÉSOLU. Gardez-le dans la feuille, engagé mais explicitement non probatoire ; jamais présenté comme preuve ou corroboration de litige sur aucune surface. Toute vérification de sanity par moniteur se compare au temps du bloc ArweaveT, pas contrôlé par l'opérateuranchored_at(§16.6).- Chronométrage à niveaux vérifiables — RÉSOLUTION DE CONCEPTION, PAS ROUTAGE ACTUEL. Le design examiné attribue le timing du bloc d'ancrage au niveau groupé et la preuve à heure exacte au T3 payant, sans SLA numérique pour le premier. Les routes actuelles n'ont pas intégré cette distinction commerciale : elles planifient une transaction individuelle pour chaque publication acceptée, et la couverture des journaux reste conditionnelle comme indiqué dans §16.1/§16.10. Le texte produit doit décrire la mise en œuvre, et non cette future répartition des niveaux.
- Arbre cumulatif sur les Travailleurs — RÉSOLU. Arbre RFC 9162 cumulatif unique + LogDO à écrivain unique avec cache de frontière (marge confortable par rapport au plafond de ~1k écritures/sec du DO ; reporter le partitionnement Merkle des racines de fragments jusqu'à ce que l'on s'en approche). La clé de coordination
(level, index)Le vecteur de test R2 node store + wiped-DO cold-leaf est implémenté (§16.9).< 300 msreste un objectif de conception/opérationnel, et non une promesse de protocole (§16.10). - Schéma de signature ANS-104 + deep-hash — RÉSOLU. RSA-PSS (type de sig 1, réutilisant le JWK du portefeuille-ancre dédié) ; Ed25519 différé vers le chemin PQ §15. Le hachage profond SHA-384 fait maison est soumis au dispositif croisé bidirectionnel, une vérification d'interopérabilité du bundle de référence uniquement statique, le partagé-
crypto.subtlealler-retour, et le moniteur d'acceptation Arweave après le regroupement (§16.8).
Contraintes de lancement obligatoires (mettre en œuvre + révision produit/juridique) :
- Plafond de réclamation (T1/T6). Aucune surface ne peut dire le journal prouve le contenu affirmé d'une feuille ou déverrouiller un tour ; la revendication autorisée pour un ancrage réussi
kind=0x02La feuille est ordonnée, de manière évidente en cas de falsification, avec un engagement de limite supérieure sans confiance. Une réponse sans tuple de reçu n'a aucune revendication de journal. Aucun exemplaire horodaté ne comporte de garantie de latence numérique. - Témoin de l'honnêteté (Q2). Équivoque du marché en tant que détectable + accusé de réception, jamais témoigné indépendamment.
- Reçu + touches d'ancrage (Q2/Q8). Avant que les revendications de non-répudiation/vérification ancrée ne soient expédiées, fournissez et compilez la clé publique du reçu, signez-la en croix avec le propriétaire de l’ancre fourni, et gardez le portefeuille de l’ancre comme son propre JWK distinct du portefeuille de téléchargement.
- Porte à hachage profond (Q8). Aucun navire anchor ou T3 tx jusqu'à ce que la fixture bidirectionnelle et le contrôle d'interopérabilité soient réussis ; le moniteur d'acceptation envoie des pages en cas d'échec.
- Prérequis de stockage (Q7). Le magasin de nœuds à clés coordonnées + le vecteur de feuilles froides à DO effacées sont des prérequis pour la garantie « la réclamation n’invalide jamais une preuve ».
17. Bundle de vérification portable (.qub)
Statut. Cette section est implémentée (W7 / UP-C2) :
qub_core::exportproduit et analyse le bundle, ettools/qub-verifyest une CLI publique et autonome qui le vérifie hors ligne. §11 et §16.9 désignent déjà « le bundle.qub» comme l’unité consommée par un vérificateur autonome ; cette section en spécifie les octets et la procédure de vérification. Elle est strictement additive — le bundle regroupe les entrées existantes du §11 et ne modifie aucun format de fil on-chain.
17.1 Objet
§11 établit que tout tiers peut vérifier l’artefact cryptographique d’un qub sans la coopération de qub. Le bundle .qub rend cette vérification portable et hors ligne : il regroupe le CBOR scellé et la signature du tour drand qui le déverrouille dans un artefact autonome, afin qu’un destinataire puisse vérifier l’intégrité du contenu, la liaison au tour et les éventuelles signatures d’auteur sans aucun appel réseau — ni récupération depuis le stockage, ni requête drand en direct, ni API qub. Un bundle seul ne prouve pas quand son texte chiffré a été créé ; une transaction de stockage vérifiée indépendamment ou une preuve de journal ancré fournit cette affirmation distincte relative à l’heure d’existence (§11, §17.5).
17.2 Format du bundle
Un QubBundle est un CBOR canonique écrit à la main sous le profil du §3.1 — longueurs définies, aucun tag, aucun flottant, forme entière la plus courte, texte NFC, champs facultatifs omis lorsqu’ils sont absents, clés triées d’abord par longueur d’octets encodés puis octet par octet. Les trois clés de 15 caractères suivent l’ordre d < i < s. Un fichier .qub brut est constitué exactement de ces octets ; pour le transport par URL ou copier-coller, les mêmes octets sont encodés en base64url sans remplissage.
| Clé | Long. enc. | Type | Présence | Signification |
|---|---|---|---|---|
version |
8 | u8 |
requise | Version du format du bundle (0x01). |
sealed_at |
10 | i64 |
facultative | Heure de scellement affirmée par le créateur (secondes Unix) ; auto-descriptive, non probante. |
drand_round |
12 | u64 |
requise | Tour auquel le qub est verrouillé. Projection du qub scellé intégré. |
arweave_tx_id |
14 | tstr |
requise | Identifiant de la transaction sous laquelle les octets scellés ont été stockés (pointeur de provenance). |
drand_chain_id |
15 | tstr |
requise | Chaîne drand (hex). Projection du qub scellé intégré. |
drand_signature |
16 | bstr |
requise | Signature de la balise drand pour drand_round — la valeur qui déverrouille le texte chiffré. |
inclusion_proof |
16 | bstr |
facultative | Preuve d’inclusion de Merkle du journal de transparence du §16, une fois le journal publié (§17.5). |
sealed_qub_cbor |
16 | bstr |
requise | Octets du SealedQubCbor interne — après déballage du §13 —, c’est-à-dire l’entrée de vérification du §11. |
drand_round et drand_chain_id sont des projections pratiques de sealed_qub_cbor, transportées afin que les outils puissent les lire sans analyser le CBOR interne. Elles sont dérivées lors de la construction et revérifiées au décodage par rapport au qub scellé analysé ; un bundle dont un champ de premier niveau diverge de sa charge utile est rejeté. La discipline de l’encodeur reflète le reste du format de fil : rejeter un drand_signature ou arweave_tx_id vide et borner chaque champ de longueur variable.
17.3 Ce que prouve la signature drand intégrée
Le bundle transporte la signature drand au lieu d’exiger que le vérificateur la récupère. Le déchiffrement timelock — tlock sur la chaîne drand, §8 — ne peut réussir qu’avec la signature authentique de la balise pour le tour lié : une valeur que la chaîne ne publie qu’une fois ce tour écoulé et qui constitue une signature BLS valide sous la clé publique de la chaîne. Une signature falsifiée ou erronée échoue à la vérification BLS ou au déchiffrement IBE/AEAD. Un bundle qui se déchiffre prouve donc : le texte chiffré est lié au tour R, et le tour R est écoulé. Le vérificateur épingle la chaîne (DrandTimelockProvider::quicknet()) et applique le contrôle de liaison au tour du §11, de sorte qu’un bundle ne peut revendiquer un tour auquel son texte chiffré n’est pas lié.
Il s’agit d’une preuve de condition de publication, et non d’un horodatage de création. Une fois le tour R écoulé, n’importe qui peut créer un nouveau texte chiffré pour R et y joindre sa signature déjà publique. Le bundle seul NE DOIT donc PAS être présenté comme la preuve que le texte chiffré ou le contenu existait avant R, avant unlock_at ou avant un événement quelconque.
17.4 Procédure de vérification hors ligne
qub-verify <file.qub> exécute entièrement depuis le bundle la procédure standard du §11, en pilotant qub_core::unlock::unlock avec un DrandTimelockProvider épinglé :
1. Parse the .qub bytes → QubBundle (canonical-CBOR guard; bound every field;
re-check drand_round / drand_chain_id against the embedded sealed qub).
2. BLS-verify bundle.drand_signature for the pinned chain and round, then
tlock_decrypt(sealed.tlock_ciphertext, bundle.drand_signature) → QubEnvelope.
3. Verify SHA3-256(body) == body_hash (§11 step 8).
4. Verify QubEnvelope.qub_id == SealedQub.qub_id (§11 step 9).
5. Verify QubEnvelope.unlock_at == SealedQub.unlock_at (§11 step 10).
6. Verify ciphertext round == unlock_round(unlock_at) and the chain binding.
7. If sig_alg != 0x00: verify author_signature (and any cosigner; §9.4).
8. Report integrity, round-elapsed/round-binding, authorship, and cosigner
verdicts separately, plus the recovered body. Do not report a commitment
timestamp unless step 9 succeeds.
9. Optional existence-time leg: verify an included §16 proof through its pinned
anchor, or independently verify the referenced storage transaction. Report
its block time as an upper bound on ciphertext existence.
La CLI quitte avec le code 0 — vérification réussie —, 1 — échec de vérification : encore verrouillé, différence de hachage du corps, liaison au tour ou à la chaîne rompue, ou signature dont la vérification échoue —, ou 2 — usage ou bundle mal formé. Un rapport --json fournit les mêmes verdicts pour l’automatisation. Le bundle étant autonome, le crate de vérification (qub-core) et la CLI (qub-verify) sont les seuls logiciels nécessaires à un tiers ; tous deux sont publics et réutilisent le chemin de vérification existant du protocole, sans cryptographie particulière.
17.5 Relation avec le journal de transparence
inclusion_proof est un emplacement facultatif pour la preuve d’inclusion de Merkle du §16. La vérification du bundle seul (§17.4) est complète pour l’intégrité, la liaison au tour/l’écoulement du tour et la paternité facultative, mais ne fournit intentionnellement aucune affirmation d’existence horodatée indépendamment. Une inclusion_proof renseignée et entièrement vérifiée jusqu’à l’ancre ajoute l’engagement propre au type de feuille et la borne supérieure temporelle du §16.11 sans changer la version du format du bundle. Une preuve absente signifie uniquement « aucune preuve incluse » — ni « invalide », ni nécessairement « non ancré ».
Dans l’implémentation de référence, cet emplacement est désormais typé : qub_core::export::QubBundle::inclusion_proof_typed() renvoie une Option<InclusionProof> qui transporte la structure complète du §16.9 — feuille, chemin d’audit, racine ancrée et AnchorRef — à travers le même champ CBOR opaque, sans changement de version du format du bundle. La CLI autonome qub-verify la consomme par sa branche --anchor et, jusqu’au provisionnement du portefeuille d’ancrage (§16, Statut), décrit une preuve renseignée mais dotée du propriétaire substitut comme limitée à l’inclusion plutôt qu’entièrement vérifiée par l’ancre.