Zweite Ebene

Die API, das Format, die Kette

Alles, woraus ein Beleg besteht, und jede Route, die einen erzeugt oder ausliefert. Jemand hat Ihnen einen Beleg gegeben, und Sie wollen ihn ohne unsere Hilfe auseinandernehmen — dafür ist diese Seite da, und dass man ihn ohne unsere Hilfe auseinandernehmen kann, ist der ganze Entwurf.

Zugang, ehrlich gesagt

Es gibt keinen API-Schlüssel, kein Konto und keinen Tarif, der ein höheres Kontingent freischaltet. Jede Route unten ist entweder öffentlich oder durch ein Token abgesichert, das zu einer Sitzung oder einem Beleg gehört und an die Person übergeben wird, die sie gestartet hat.

Die Routen, die ein Dokument oder das Geld führen, weisen eine Anfrage ab, deren Origin-Kopfzeile einen fremden Host nennt. Eine Anfrage, die überhaupt keine Origin-Kopfzeile sendet — ein Server, der einen Server aufruft —, wird nicht abgewiesen, und wir sagen das lieber hier, als dass es jemand entdeckt und annimmt, es sei verborgen worden. Es ist keine unterstützte Integrationsfläche: Sie hat kein Versionsversprechen, keinen Schlüssel zum Rotieren und kein auf sie abgestimmtes Limit, und sie kann sich mit jedem Deploy ändern.

Wenn Sie darauf ordentlich aufbauen wollen, schreiben Sie uns. Eine unterstützte, versionierte Fläche mit Schlüsseln ist etwas, das wir für einen echten Aufrufer bauen würden und für einen hypothetischen nicht gebaut haben.

Routen

Jede Antwort ist JSON. Sitzungsrouten sind no-store und noindex; ein Token, das wohlgeformt ist, dessen Sitzung aber endete, antwortet 404 mit expired — etwas anderes als ein Token, das es nie gab.

Die Tabelle unten ist das Protokoll selbst und wird nur auf Englisch veröffentlicht.

MethodeRouteWas sie tutAntwortet
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. — wer: 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. — wer: 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. — wer: 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. — wer: 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. — wer: 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. — wer: the token202
DELETE/api/check/:tokenEnd the session now and drop the preview. The session expires on its own either way. — wer: the token{deleted: true}
POST/api/checkoutTurn a finished check into an invoice. {token, email, tier}. — wer: 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. — wer: 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. — wer: 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}

Eine Belegroute bestätigt nie eine Kennung, die sie nicht hält, und eine Adminroute ohne ihr Token antwortet Byte für Byte wie ein Pfad, hinter dem gar keine Route steht — gleicher Status, gleiche Kopfzeilen, gleicher Rumpf. Das ist Absicht: Der Unterschied zwischen beiden Antworten wäre sonst eine Landkarte dessen, was existiert.

Ein Server für KI-Agenten

Es gibt noch keinen, und dies ist der einzige Ort, an dem wir ihn ankündigen würden. Der Patch, der dieses Produkt neu geordnet hat, stellte einen modellseitigen Server in dieselbe zweite Ebene wie die Routen oben, und die ehrliche Lage heute ist, dass die Routen oben die ganze Maschinenfläche sind: Eine Prüfung ist ein POST, ihr Ergebnis ein GET, und ein Beleg ist eine öffentliche URL. Wird ein agentenseitiger Server gebaut, umschließt er eben diese und nichts weiter, und dieser Abschnitt sagt es dann mit Namen und Transport. Bis dahin gibt es nichts zu installieren und nichts einzurichten.

Die Signatur

Ein Beleg ist kanonisches JSON. Sein self_hash ist der SHA-256 der kanonischen Bytes des Belegs, aus dem zwei Mitglieder der obersten Ebene entfernt wurden — self_hash selbst und signatures —, sodass der Hash außerhalb seiner selbst liegt und die Signaturen außerhalb des gehashten Rumpfs. Eben das erlaubt es, einen Beleg später gegenzuzeichnen, ohne dass sein Hash sich bewegt.

Die Signatur ist Ed25519 über die 32 rohen Bytes, die das Hex-Dekodieren von self_hash ergibt. Nicht über die 64 Hex-Zeichen, nicht über das kanonische JSON, nicht über einen Hash des Hashes: Ed25519 hasht seine Eingabe selbst, also ist das unmittelbare Signieren des Digests das ganze Verfahren.

Jeder Base64-Wert im Format ist kanonisch — das Standardalphabet, die vorgeschriebene Auffüllung, kein Leerraum, und die ungenutzten unteren Bits des letzten Symbols auf null —, sodass ein Schlüssel eine einzige Schreibweise hat und zwei Schlüssel als Bytes oder als Text mit demselben Ergebnis verglichen werden können. Ein Dekodierer, der eine fehlende Auffüllung oder das URL-sichere Alphabet duldet, läse dieselben Bytes aus mehreren Schreibweisen; dieses Format lässt eine zu.

Ein Beleg trägt eine einzige Signatur, deren Rolle issuer ist. Null heißt, er wurde nie ausgestellt; zwei oder mehr heißt, er kann nicht angeben, von wem; beides sind Fehlschläge. Die Signatur wird gegen den Schlüssel geprüft, der für ihre Schlüsselkennung veröffentlicht ist, nie gegen den Schlüssel, den der Beleg über sich selbst führt — ein Beleg darf nicht für sich selbst einstehen dürfen.

Die Kette und die Wurzeln

Es gibt eine Kette. seq ist dicht und beginnt bei 1, und prev_hash bei seq n ist der self_hash von seq n−1. Beide liegen im signierten Rumpf, sodass die Position eines Belegs mit ihm signiert wird: Einen Beleg zu verschieben oder irgendetwas davor umzuschreiben, ändert seinen Hash.

Die verketteten Belege jedes UTC-Tages bilden einen Merkle-Baum, gehasht so, wie RFC 6962 es festlegt — Blättern ist ein Nullbyte vorangestellt, inneren Knoten eine Eins, sodass ein Blatt nie als Knoten ausgegeben werden kann. Die Tageswurzel wird unter einem eigenen Wurzelschlüssel signiert und in ein öffentliches Verzeichnis veröffentlicht, das wir nicht still ändern können. Auch ein leerer Tag bekommt eine Datei, sodass eine Lücke in der Reihe von außen sichtbar wäre.

Der Inklusionsnachweis eines Belegs ist ein Prüfpfad in eine dieser Wurzeln. Ein heute versiegelter Beleg hat keinen Nachweis, bis die Wurzel dieser Nacht gezogen ist, und die Nachweisroute sagt pending, statt einen zu erfinden.

Was ein öffentlicher Beleg nicht trägt

Was unter dem Link eines Belegs zu sehen ist, ist eine Projektion: jede zitierte URL entfernt, jeder Fundort auf {type, domain} reduziert, die Kennung des Fallregisters für einen Datensatz mit ihm gestrichen — die Menge dieser Kennungen wäre das Fundstellenverzeichnis des Dokuments. Alles Übrige bleibt Byte für Byte erhalten, Hashes und Signaturen eingeschlossen, sodass die Person mit dem vollständigen Beleg beides aneinander binden kann. Eine Projektion wird eigenständig signiert, mit einem zusätzlichen Mitglied, das der versiegelte Beleg nie hat, sodass ihre gesamte sichtbare Fläche — Befunde, Anzahlen, Status, Domains — von einer Signatur gedeckt ist und nicht nur vom Hash des Belegs.

Das Prüfprogramm

Das Prüfprogramm steht unter MIT-Lizenz und wird auf einem öffentlichen Code-Hoster gespiegelt. Es stellt keine eigene Anfrage: Nachprüfen heißt Hashen und Signaturprüfungen über Bytes, die Sie bereits haben, weshalb der Zähler daneben auf der Prüfseite ehrlich null anzeigen kann.

Es läuft hier, in Ihrem Browser, als kompiliertes Modul, das von dieser Website ausgeliefert wird. Laden Sie die Prüfseite einmal, trennen Sie die Verbindung, und sie prüft weiterhin — das ist die Probe, die man macht, bevor man irgendetwas hiervon glaubt, und die man wiederholt, falls wir je verschwinden.

Aus einem Terminal

Dieselben Prüfungen laufen von der Kommandozeile gegen einen Beleg auf Ihrer Festplatte. Das Paket liegt noch nicht in der öffentlichen Registry, npx exhibitb löst heute also nicht auf, und wir drucken es nicht ab, als täte es das. Was es heute gibt, ist der Browser-Build oben — dasselbe Prüfprogramm — und die Quellen auf dem öffentlichen Code-Hoster, die zu beidem bauen. Wird das Paket veröffentlicht, steht sein Name am selben Tag im Änderungsprotokoll, nicht vorher.

Hier ausführen, an einem zugesandten Beleg

Was das Add-in sendet

Ein Beleg hält fest, welcher Extraktor den Text erzeugt hat, den er beschreibt, und dieses Feld ist nichts wert, solange nicht eine fremde Person mit demselben Dokument dieselbe Zeichenkette nachbauen und denselben Hash erhalten kann. Dies ist diese Spezifikation. Sie ist der veröffentlichte Vertrag hinter der Bezeichnung exhibitb-word@1, und sie ist der Grund, weshalb ein in einem Dokument erzeugter Beleg überhaupt an das Dokument gebunden werden kann.

Die Spezifikation unten ist der veröffentlichte Vertrag selbst und wird nur auf Englisch abgedruckt, damit ein Nachbau danach überall Byte für Byte gleich ausfällt.

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.

Die Reihenfolge, in der die Zeichenkette entsteht

  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.

Was sie verbindet

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

Was nie darin steht

  • 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

Die Bereinigung, zuletzt angewandt und auf nichts sonst

  • 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

Den eigenen Nachbau prüfen

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.

Warum die beiden Extraktoren unterschiedlich zählen

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

Die andere Bezeichnung. Eine hochgeladene .docx wird von einer Bibliothek gelesen, die die eigenen Kinder des Dokuments durchläuft — den Haupttext und die Tabellen darin; Fußnoten und Endnoten liegen anderswo im Container und werden nicht gelesen. Alles Übrige an der Zeichenkette ist gleich, einschließlich der Bereinigung unten und des Hashes darüber.

Das Format vollständig

Die kanonische Form, das Belegschema Mitglied für Mitglied, die Kettenregeln, die Merkle-Konstruktion, die Wurzeldatei, der Inklusionsnachweis und die Projektionsregel sind in einem Dokument festgelegt, mit Testvektoren daneben — dieselbe Datei, Byte für Byte, die im Checkout des Prüfprogramms mitgeliefert wird. Jede Zahl darin lässt sich von Dritten reproduzieren.

Testvektoren

  • 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

Wenn auf dieser Seite etwas falsch ist, oder eine Prüfung, die Sie dagegen geschrieben haben, fehlschlägt und Sie meinen, sie sollte es nicht, erreicht die Support-Adresse im Fußbereich einen Menschen, der sich die Bytes mit Ihnen ansieht.