Deuxième couche

L’API, le format, la chaîne

Tout ce dont un reçu est fait, et chaque route qui en produit ou en sert un. Quelqu’un vous a remis un reçu et vous voulez le démonter sans notre aide — c’est à cela que sert cette page, et pouvoir le démonter sans notre aide est toute la conception.

L’accès, honnêtement

Il n’y a pas de clé d’API, pas de compte et pas de formule qui débloque un débit supérieur. Chaque route ci-dessous est soit publique, soit protégée par un jeton qui appartient à une session ou à un reçu et qui est remis à qui l’a lancée.

Les routes qui portent un document ou l’argent refusent une requête dont l’en-tête Origin nomme un hôte qui n’est pas le nôtre. Une requête qui n’envoie aucun en-tête Origin — un serveur qui appelle un serveur — n’est pas refusée, et nous préférons le dire ici plutôt que de laisser quelqu’un le découvrir et supposer que c’était caché. Ce n’est pas une surface d’intégration prise en charge : elle n’a pas de promesse de version, pas de clé à faire tourner et pas de limite de débit réglée pour elle, et elle peut changer à n’importe quel déploiement.

Si vous voulez bâtir là-dessus sérieusement, écrivez-nous. Une surface prise en charge, versionnée et à clés est une chose que nous construirions pour un appelant réel et que nous n’avons pas construite pour un appelant hypothétique.

Routes

Chaque réponse est du JSON. Les routes de session sont en no-store et noindex ; un jeton bien formé dont la session s’est terminée répond 404 avec expired, ce qui n’est pas la même chose qu’un jeton qui n’a jamais existé.

La table ci-dessous est le protocole lui-même et n’est publiée qu’en anglais.

MéthodeRouteCe qu’elle faitRépond
POST/api/checkStart a check. Multipart with a file, or JSON {text, ref}. A ref of word-win, word-mac or word-web over a posted string records that the caller assembled the string out of an open document, and the receipt then names the add-in's extractor rather than the file reader's. — qui: same-origin202 {token, expires_at, arm}
GET/api/check/:tokenThe session as it stands: every claim found, and each verdict as it lands. This is what the page polls while a check runs. — qui: the tokenthe session view
GET/api/check/:token/statusThe completion signal, read off the database row rather than out of memory — so a deploy in the middle of a payment does not tell somebody who has paid that their session is gone. — qui: the token{receipt_id, sealed, checked_out, paid, closed, session}
GET/api/check/:token/preview.jsonThe unsigned, unchained receipt-shaped preview of a check that has finished but has not been sealed. — qui: the tokena receipt-shaped object with no signature
GET/api/check/:token/binding.jsonThe binding bundle: what turns the per-claim fingerprints in a receipt back into the claims they were computed from. — qui: the tokena downloaded file
POST/api/check/:token/holdsRun the adversarial third level over one claim. Off by default, never sealed into a receipt, and it sends the sentence you wrote. — qui: the token202
DELETE/api/check/:tokenEnd the session now and drop the preview. The session expires on its own either way. — qui: the token{deleted: true}
POST/api/checkoutTurn a finished check into an invoice. {token, email, tier}. — qui: same-origina payment URL
GET/api/receipt/:idThe public projection of a sealed receipt: every cited URL removed, every verdict, count and hash kept, signed in its own right.the stored canonical bytes
GET/api/receipt/:id/proofThe inclusion proof pointing at the day's Merkle root. 202 while that day has not been rooted yet.an audit path
GET/r/:id/receipt.jsonThe full sealed receipt, cited URLs and all. This is the creator's file, and the reason the projection exists. — qui: the creator's linkthe sealed bytes
GET/r/:id/receipt.pdfThe printable receipt: the page somebody staples to a report and is asked about under oath. — qui: the creator's linka PDF
GET/r/:id/qr.pngThe receipt's QR as a raster, for a printed page.a PNG
GET/chain/headWhere the chain is now.{seq, self_hash, issued_at, last_root}
GET/chain/root/:dateOne UTC day's signed root file, exactly as it was stored and published.the stored canonical bytes
GET/chain/verifyOur own replay of the chain, and the first break in it if there is one. Rooted days are re-derived on every call; the answer is memoised for sixty seconds.{ok, checked, head_seq, first_broken, …}
GET/keys.jsonEvery public key this install has ever signed with, so a verifier can pin them rather than trust the key a receipt carries about itself.the key set
GET/healthzWhether the service is up.{ok}

Une route de reçu ne confirme jamais un identifiant qu’elle ne détient pas, et une route d’administration sans son jeton répond octet pour octet comme un chemin derrière lequel il n’y a aucune route — même statut, mêmes en-têtes, même corps. C’est voulu : la différence entre les deux réponses serait sinon une carte de ce qui existe.

Un serveur pour les agents IA

Il n’y en a pas encore, et c’est le seul endroit où nous l’annoncerions. Le correctif qui a réordonné ce produit a placé un serveur destiné aux modèles dans la même deuxième couche que les routes ci-dessus, et la position honnête aujourd’hui est que ces routes sont toute la surface machine : un contrôle est un POST, son résultat un GET, et un reçu une URL publique. Quand un serveur destiné aux agents sera construit, il enveloppera celles-là et rien de plus, et cette section le dira avec le nom et le transport. D’ici là, il n’y a rien à installer et rien à configurer.

La signature

Un reçu est du JSON canonique. Son self_hash est le SHA-256 des octets canoniques du reçu privé de deux membres de premier niveau — self_hash lui-même et signatures — de sorte que l’empreinte vit hors d’elle-même et que les signatures vivent hors du corps haché. C’est cela qui permet de contresigner un reçu plus tard sans que son empreinte bouge.

La signature est Ed25519 sur les 32 octets bruts obtenus en décodant self_hash depuis l’hexadécimal. Pas sur les 64 caractères hexadécimaux, pas sur le JSON canonique, pas sur une empreinte de l’empreinte : Ed25519 hache lui-même son entrée, donc signer le condensé directement est tout le schéma.

Chaque valeur base64 du format est canonique — l’alphabet standard, le remplissage complet, aucun espace, et les bits de poids faible inutilisés du dernier symbole à zéro — de sorte qu’une clé a une seule écriture et que deux clés se comparent comme octets ou comme texte avec la même réponse. Un décodeur qui tolère un remplissage manquant ou l’alphabet compatible URL lirait les mêmes octets depuis plusieurs écritures ; ce format n’en autorise qu’une.

Un reçu porte une seule signature, dont le rôle est issuer. Zéro dit qu’il n’a jamais été émis ; deux ou plus disent qu’il ne peut pas indiquer par qui ; les deux sont des échecs. La signature est vérifiée contre la clé publiée pour son identifiant de clé, jamais contre la clé que le reçu porte à son propre sujet — un reçu ne doit pas pouvoir répondre de lui-même.

La chaîne et les racines

Il y a une seule chaîne. seq est dense et commence à 1, et prev_hash au seq n est le self_hash du seq n−1. Les deux se trouvent dans le corps signé, de sorte que la position d’un reçu est signée avec lui : déplacer un reçu, ou réécrire quoi que ce soit avant lui, change son empreinte.

Les reçus chaînés de chaque jour UTC forment un arbre de Merkle, haché tel que le définit la RFC 6962 — les feuilles préfixées d’un octet nul, les nœuds internes d’un un, pour qu’une feuille ne puisse jamais passer pour un nœud. La racine du jour est signée sous une clé de racine distincte et publiée dans un dépôt public que nous ne pouvons pas modifier en silence. Un jour vide reçoit tout de même un fichier, pour qu’un trou dans la série se voie de l’extérieur.

La preuve d’inclusion d’un reçu est un chemin d’audit vers l’une de ces racines. Un reçu scellé aujourd’hui n’a pas de preuve avant que la racine de cette nuit soit tirée, et la route de preuve dit pending plutôt que d’en inventer une.

Ce qu’un reçu public ne porte pas

Ce qu’un lecteur voit au lien d’un reçu est une projection : chaque URL citée retirée, chaque localisateur réduit à {type, domain}, l’identifiant propre du registre judiciaire pour un dossier retiré avec elle — l’ensemble de ces identifiants serait la table des sources du document. Tout le reste demeure octet pour octet, empreintes et signatures comprises, pour que celui qui détient le reçu complet puisse lier les deux. Une projection est signée à part entière, avec un membre supplémentaire que le reçu scellé n’a jamais, de sorte que toute sa surface visible — verdicts, comptages, états, domaines — est couverte par une signature et pas seulement par l’empreinte du reçu.

Le vérificateur

Le vérificateur est sous licence MIT et répliqué sur un hébergeur de code public. Il ne fait aucune requête de son côté : vérifier, c’est hacher et contrôler des signatures sur des octets que vous détenez déjà, ce pourquoi le compteur à côté de lui sur la page de vérification peut honnêtement afficher zéro.

Il s’exécute ici, dans votre navigateur, comme un module compilé servi depuis ce site. Chargez la page de vérification une fois, déconnectez-vous, et elle vérifie encore — c’est le contrôle à faire avant d’accorder du crédit à tout ceci, et celui à refaire si nous venions à disparaître.

Depuis un terminal

Les mêmes contrôles s’exécutent en ligne de commande contre un reçu sur votre disque. Le paquet n’est pas encore sur le registre public, donc npx exhibitb ne se résout pas aujourd’hui et nous n’allons pas l’imprimer comme s’il le faisait. Ce qui existe aujourd’hui, c’est la version navigateur ci-dessus, qui est le même vérificateur, et les sources sur l’hébergeur de code public, qui compilent vers les deux. Quand le paquet sera publié, il sera nommé dans le journal des modifications le jour même, pas avant.

L’exécuter ici, sur un reçu qu’on vous a envoyé

Ce qu’envoie le complément

Un reçu enregistre quel extracteur a produit le texte qu’il décrit, et ce champ ne vaut rien tant qu’un inconnu détenant le même document ne peut pas reconstituer la même chaîne de caractères et obtenir la même empreinte. Voici cette spécification. C’est le contrat publié derrière l’étiquette exhibitb-word@1, et c’est pourquoi un reçu produit dans un document peut être lié au document.

La spécification ci-dessous est le contrat publié lui-même et n’est imprimée qu’en anglais, pour qu’une reconstitution faite d’après elle soit identique à l’octet près partout.

exhibitb-word@1

exhibitb-word@1 — the string a receipt from the add-in describes

A receipt names the extractor that produced the text it was made from. When the check ran from inside Word, the text was assembled in the open document rather than parsed out of an uploaded file, and this is how. Rebuild the string from your own copy of the document, hash it, and it matches the text_sha256 in the receipt — or it does not, and you have learned something.

L’ordre dans lequel la chaîne est construite

  1. Take the paragraphs of the document's main story, in the order Word lays them out. Paragraphs inside a table are part of that order: a table is read row by row, and each row cell by cell in reading order, at the point where the table sits.
  2. Then every footnote body, in footnote-number order.
  3. Then every endnote body, in endnote-number order.
  4. A paragraph's text is its own characters. A hyperlink contributes the words the reader sees; the address it points to is not inserted.
  5. The three groups are concatenated in that order — body, then footnotes, then endnotes — with nothing between them but the same separator that joins paragraphs.

Ce qui les joint

Every paragraph, footnote body and endnote body is joined to the next by two line feeds (U+000A twice), which is one blank line.

Ce qui n’y figure jamais

  • the .docx file itself — no part of the container is uploaded
  • headers and footers
  • comments, and the identity of whoever wrote them
  • tracked changes: deleted-but-unaccepted text is not sent, insertions are sent as the paragraph reads with them applied, and no revision author is sent
  • styles, fonts, numbering, colour and every other piece of formatting
  • images, charts, embedded objects and their captions' pictures (a caption's own words are a paragraph like any other)
  • the file name, the folder it sits in, and every piece of document metadata
  • fields, in favour of the result they display

Le nettoyage, appliqué en dernier et à rien d’autre

  • carriage returns become line feeds: and a lone both become
  • control characters are removed (U+0000–U+0008, U+000B, U+000C, U+000E–U+001F, U+007F)
  • soft hyphens (U+00AD) are removed
  • spaces and tabs immediately before a line feed are removed
  • three or more consecutive line feeds collapse to two
  • leading and trailing whitespace is removed from the whole string

Contrôler votre propre reconstitution

The cleaning is the same eight lines the uploaded-file path runs, and it is idempotent: running it twice changes nothing, so a rebuild that applies it once is comparable byte for byte. SHA-256 of the resulting UTF-8 bytes is the receipt's document.text_sha256; document.chars is that string's length in code units.

Pourquoi les deux extracteurs ne comptent pas pareil

The two paths do not read the same amount of a document, and the label is how a reader tells them apart. exhibitb-word@1 reads footnotes and endnotes. exhibitb-text@1 over an uploaded .docx reads the main story only, so a footnote-heavy report checked as a file will show fewer citations than the same report checked from inside Word. Neither number is wrong; they are counts of different strings, and the receipt says which one it made.

exhibitb-text@1

L’autre étiquette. Un .docx envoyé est analysé par une bibliothèque qui parcourt les enfants propres du document, c’est-à-dire le récit principal et les tableaux qu’il contient — les notes de bas de page et de fin sont ailleurs dans le conteneur et ne sont pas lues. Tout le reste de la chaîne de caractères est identique, y compris le nettoyage ci-dessous et l’empreinte prise dessus.

Le format en entier

La forme canonique, le schéma du reçu membre par membre, les règles de chaîne, la construction de Merkle, le fichier de racine, la preuve d’inclusion et la règle de projection sont spécifiés dans un seul document, avec des vecteurs de test à côté — le même fichier, octet pour octet, que celui livré dans la copie de travail du vérificateur. Chaque nombre qu’il contient peut être reproduit par un tiers.

Vecteurs de test

  • the canonical form of an object, with the worked examples that pin the whitespace and the number formatting
  • self-hash vectors, including an object carrying a wrong self-hash and a nonsense signature array hashing to what it hashes to without them
  • a signed receipt, its key set and its verification result
  • Merkle roots for trees of 0, 1, 2, 3 and 7 leaves, with every audit path

Si quelque chose sur cette page est faux, ou si un contrôle que vous avez écrit contre elle échoue et que vous pensez qu’il ne devrait pas, l’adresse de support en pied de page atteint une personne qui regardera les octets avec vous.