Dies ist eine informative Übersetzung. Maßgeblich ist die englische Fassung; sie hat im Zweifel Vorrang. Zur englischen Fassung

Leitfaden für Implementierer

Wie Sie eine konforme Label-309-Implementierung aufbauen — die empfohlene Schichtenarchitektur, der sprachübergreifend byte-identische Vertrag und die Konformitäts-Testvektoren, die Interoperabilität definieren.

Label 309 ist ein Wire-Format und eine Reihe kryptografischer Konstruktionen, kein Produkt. Beliebig viele unabhängige Implementierungen, ob in TypeScript, Python, Rust, Go oder einer nativen mobilen Laufzeitumgebung, können nebeneinander bestehen, und ein von der einen erzeugter Datensatz MUSS sich unter einer anderen verifizieren lassen. Diese Seite richtet sich an das Team, das eine solche Implementierung baut. Sie beschreibt die Architektur, die die kryptografische Angriffsfläche überschaubar und prüfbar hält, den genauen Vertrag, der zwei Implementierungen interoperabel macht, und die Konformitäts-Suite, die rein mechanisch entscheidet, ob Sie diesen Vertrag erfüllt haben.

Zwei Dinge machen Label 309 sprachübergreifend interoperabel. Das erste ist der Determinismus: Die Konstruktionen sind an öffentliche Standards gebunden (RFC 8949 kanonisches CBOR, RFC 8032 Ed25519, RFC 7748 X25519, RFC 5869 HKDF, RFC 9106 Argon2id, RFC 9052 COSE), sodass dieselben Eingaben überall dieselben Bytes ergeben. Das zweite ist die Konformitäts-Suite: eine Reihe byte-genauer Testvektoren, die eine Implementierung entweder reproduziert oder nicht. Konformität ist eine Eigenschaft, die Sie überprüfen können, keine Behauptung, die Sie aufstellen.

Die Schichtenarchitektur

Eine konforme Implementierung SOLLTE kryptografische Primitive von der Anwendungslogik in eigene Schichten trennen, von denen jede nur auf die direkt darunterliegende zugreift. Die folgenden Bezeichnungen sind Rollen, keine Paketnamen; wählen Sie Ihre eigenen.

┌─────────────────────────────────────────────────────────┐
│  application                                            │
│  UI, routing, persistence, payments, background jobs    │
├─────────────────────────────────────────────────────────┤
│  SDK                                                    │
│  service client + standalone verifier + helpers         │
├─────────────────────────────────────────────────────────┤
│  wire-format library                                    │
│  schema · structural validator · canonical-CBOR codec   │
├─────────────────────────────────────────────────────────┤
│  cryptographic core                                     │
│  hashes · KDFs · signatures · KEM · AEAD · CBOR · COSE  │
│  no application or framework dependencies               │
└─────────────────────────────────────────────────────────┘

Die Schichtgrenzen sind tragend, nicht kosmetisch. Jede Schicht hat eine einzige Aufgabe und eine kurze Liste von Dingen, die sie nicht kennen darf.

Der kryptografische Kern

Die unterste Schicht enthält ausschließlich Primitive: Hash-Funktionen, KDFs, Signatur- und KEM-Operationen, die AEAD-Inhaltsschicht, kanonisches CBOR, COSE_Sign1, die Wrap-/Unwrap-Konstruktion für versiegelte PoE, Merkle-Wurzeln und -Beweise sowie die typisierten Fehlerklassen, die sie auslösen. Sie enthält keine Domänenlogik, kein HTTP, keinen Datenbankzugriff und keine Imports von UI- oder Server-Frameworks.

Diese Schicht MUSS frei von allen Abhängigkeiten bleiben, die an die Anwendung oder an einen Server gebunden sind, und sie MUSS browsertauglich sein, und das aus drei konkreten Gründen:

  • Sie läuft überall. Eine Datei zu hashen, einen Umschlag zu bauen und, das ist entscheidend, der eigenständige Verifizierer laufen in Browsern, in Serverless- Workern und auf der Kommandozeile genauso problemlos wie auf einem Server. Eine rein serverseitige Abhängigkeit (ein Datenbanktreiber, ein an eine bestimmte Laufzeitumgebung gebundenes Logging-Framework, eine UI-Bibliothek) würde diese Ziele unbrauchbar machen und jeden Consumer aufblähen, der den Kern mitbündelt.
  • Sie ist die Prüffläche. Wer den Code prüft, kann ein Paket, das nur Primitive enthält, vollständig gegen die RFCs durchlesen. Sobald Anwendungscode hineinsickert, wächst die Fläche, die eine sicherheitsprüfende Person im Kopf behalten muss, ins Unbegrenzte.
  • Sie ist das, was Dritte einbinden. Ein unabhängiger Verifizierer, jemand, der keinem Dienst vertraut, sondern allein der Blockchain, bindet diese Schicht ein und nichts darüber. Sie klein und portabel zu halten, ist genau das, was den Anspruch „prüfen Sie es selbst" praktikabel macht.

Konkret DARF der Kern NICHT ORM- oder Datenbanktreiber, UI-Frameworks, servergebundene Logging-Frameworks oder irgendein Anwendungsmodul importieren. Zufallswerte MÜSSEN vom CSPRNG der Plattform stammen (Web Crypto getRandomValues oder ein gleichwertiger Re-Export), niemals aus einer ausschließlich auf Node verfügbaren Quelle, damit dieselbe Quelle unverändert auch im Browser läuft.

Erzwingen Sie die Schichtgrenze in der CI, nicht im Code-Review

Die Regel, keine Abhängigkeiten zuzulassen, zerfällt in dem Moment, in dem sich ein bequemer Import einschleicht. Eine Implementierung SOLLTE einen Abhängigkeitsgraph-Lint laufen lassen, der jeden Import im Kern und in der Wire-Format-Bibliothek durchgeht und den Build bei jedem Specifier außerhalb einer schichtspezifischen Erlaubnisliste scheitern lässt. Prüfende vergessen; der Linter nicht.

Die Wire-Format-Bibliothek

Die nächsthöhere Schicht ist für Label 309 selbst zuständig: das Datensatz-Schema, den strukturellen Validator sowie den kanonischen CBOR-Encoder und -Decoder. Sie hängt vom kryptografischen Kern ab (für Hashing, COSE und den CBOR-Codec) und von nichts anderem, das an die Anwendung gebunden ist. Ihre Schnittstelle ist klein und rein:

  • encode — erzeugt kanonische CBOR-Bytes für einen validierten Datensatz.
  • decode — die Umkehrung davon.
  • validate — führt die strukturellen und semantischen Prüfungen des Standards über einen dekodierten Datensatz aus und liefert ein typisiertes Ergebnis (siehe Verifizierung).

In dieser Schicht leben die Regeln aus Der Datensatz als Code: der geschlossene Schlüsselsatz, die Disziplin der Chunk-Reassemblierung, die items-oder-merkle-Invariante, die Anforderungen an kanonisches CBOR. Wie der Kern bleibt sie frei von HTTP-Clients, Datenbanktreibern und Framework-Imports.

Das SDK und die Anwendung

Das SDK fasst die unteren Schichten zu ergonomischen Hilfsfunktionen zusammen: einen Service-Client, Hilfen zum Aufbauen und Entsperren von Umschlägen sowie den eigenständigen Verifizierer, also die Funktion, die einen Datensatz dekodiert, seine Struktur prüft, etwaige Datensatz-Signaturen gegen den On-Chain-Schlüssel verifiziert und allein aus öffentlichen Daten ein Urteil bildet. Der eigenständige Verifizierer MUSS ohne Netzwerkzugriff auf irgendeinen vom Implementierer betriebenen Dienst funktionieren; seine einzige externe Eingabe ist ein öffentlicher Blockchain-Explorer, den die prüfende Stelle selbst wählt. Auch das SDK SOLLTE browsertauglich bleiben.

Die Anwendungsschicht, also UI, Routing, Persistenz, Abrechnung und Hintergrundjobs, ist Greenfield und trägt keinerlei Interoperabilitätspflichten. Nichts im Standard schreibt vor, wie Sie sie bauen, nur dass sie oberhalb der verifizierten Krypto-Fläche sitzt und nicht in sie hineingreift.

Der byte-identische Vertrag

Interoperabilität ist eine Eigenschaft von Bytes, nicht von Absichten. Zwei Implementierungen sind genau dann interoperabel, wenn die Primitive, deren Ausgabe keinerlei Spielraum hat, aus denselben Eingaben dieselben Bytes erzeugen. Das ist der Paritätsvertrag, und er ist das Herzstück der Konformität.

Der Vertrag teilt sich sauber in zwei Hälften. Operationen, deren Ausgabe vollständig durch ihre Eingaben festgelegt ist, MÜSSEN über alle Implementierungen hinweg byte-identisch sein. Operationen, die Zufallswerte verbrauchen, können von Aufruf zu Aufruf nicht byte-gleich sein; für sie lautet der Vertrag wechselseitige Verarbeitbarkeit: Ein von einer Implementierung erzeugter Wert MUSS von jeder anderen verarbeitet werden können (ein in einer Sprache versiegelter Chiffretext lässt sich in einer anderen entschlüsseln).

Byte-identische Primitive

Jede der folgenden Operationen ist eine reine Funktion ihrer Eingaben und MUSS in jeder konformen Implementierung byte-identische Ausgabe liefern:

PrimitiveGebunden anAusgabe, die übereinstimmen muss
Seed → Ed25519- / X25519-SchlüsselpaarHKDF-SHA-256 mit den registrierten Info-Konstantenabgeleiteter öffentlicher und privater Schlüssel
HKDF-SHA-256RFC 5869Output Key Material für feste Eingabe
HMAC-SHA-256 Slot-Set-MACRFC 2104slots_hash- und slots_mac-Tag-Bytes für eine feste CEK und einen festen Slot-Satz
Argon2id (Passphrase-KDF)RFC 9106abgeleiteter Schlüssel für feste (m, t, p, salt, len, password)
SHA-256FIPS 180-4Digest
BLAKE2b-256RFC 7693Digest
Kanonische CBOR-KodierungRFC 8949 §4.2.1kodierte Bytes für feste Eingabe
COSE_Sign1-KodierungRFC 9052Strukturbytes für festen Header, Payload, Signatur
Ed25519 signieren / verifizierenRFC 8032 (strict)Signatur; Urteil
X25519 ECDHRFC 7748gemeinsames Geheimnis für feste Skalare
Versiegelte PoE wrap / unwrapVersiegelte PoEBytes pro Slot und MAC, wenn Ephemerals und CEK injiziert werden
Merkle-Wurzel + InklusionsbeweiseRFC 9162 §2.1.1Wurzel und Beweise pro Blatt über eine geordnete Blattliste

Zwei Punkte verdienen besondere Beachtung. Ed25519 ist strict: Ein konformer Verifizierer MUSS die Regeln zum kanonischen S und zur Ablehnung von Punkten niedriger Ordnung aus RFC 8032 §5.1.7 anwenden, damit zwei Implementierungen nicht nur darin übereinstimmen, welche Signaturen sie akzeptieren, sondern auch darin, welche sie ablehnen. Argon2id überspannt Ökosystemgrenzen: Verschiedene Sprachen greifen zu verschiedenen Argon2-Bibliotheken, aber jede konforme Bibliothek implementiert RFC 9106 und MUSS für identische Parameter identische Ausgabe liefern. Der Vertrag ist der Parametersatz, nicht die Bibliothek.

Operationen, die Zufallswerte verbrauchen

Die Schlüsselerzeugung, das Wrapping versiegelter PoE unter frischen Ephemerals pro Slot und die Umschlag-Verschlüsselung ziehen allesamt frische Zufallswerte, sodass ihre Ausgabe bei jedem Aufruf anders ausfällt und sich nicht byte-genau festlegen lässt. Der Vertrag für diese Operationen lautet wechselseitige Verarbeitbarkeit: Die von einer Implementierung erzeugte Ausgabe MUSS von jeder anderen verarbeitet werden können. Ein in einer Sprache versiegelter Datensatz MUSS sich in einer anderen entschlüsseln lassen; ein in einer Sprache erzeugtes Schlüsselpaar MUSS sich in einer anderen verifizieren und als Verschlüsselungsziel verwenden lassen. Konformitäts-Suiten legen diese Operationen über deterministische Test-Hooks fest, die die Ephemerals injizieren, sodass das Wrapping reproduzierbar wird, sowie über Round-Trip-Fixtures, die in der einen Sprache verschlüsseln und in der anderen entschlüsseln.

Die versiegelte PoE-Konstruktion bauen

Die versiegelte PoE ist der dichteste Teil des Wire-Formats und der Teil, an dem ein einziges falsches Byte, ein vertauschter Map-Schlüssel, ein um ein Zeichen verfehltes Label, eine nicht kanonische Stückelung, einen Umschlag erzeugt, der sich in Ihrer eigenen Implementierung öffnet, aber in keiner anderen. Dieser Abschnitt ist die Bau-Checkliste: die exakten Rezepte, das Schlüssel-Commitment auf jedem Pfad, die Probeentschlüsselungsschleife und die Schutzmaßnahmen, die jeder Erzeuger und Verifizierer durchsetzen muss. Die Konstruktionsreferenz unter Versiegelte PoE ist die Prosa; dies ist die Verdrahtung, damit das Paritäts-Gate grün wird. Pinnen Sie diese externen Spezifikationen exakt, da ihre Interna Bytes festlegen, die Sie reproduzieren müssen:

  • chacha20-poly1305-stream64k, das Inhaltsformat, ist ChaCha20-Poly1305 (RFC 8439) im 64-KiB-segmentierten STREAM-Layout der age-v1-Spezifikation. Pinnen Sie die Chunk-Größe (65536), die 12-Byte-Nonce pro Chunk uint88_be(counter) ‖ final_flag, die leere AAD pro Chunk und die Final-Flag-Regel exakt – sie legen Bytes fest, die Sie reproduzieren müssen.
  • X-Wing (der mlkem768x25519-KEM) ist draft-connolly-cfrg-xwing-kem-10. Behandeln Sie es als Black-Box-KEM: Die Konstruktion bindet den öffentlichen Empfängerschlüssel und den Geheimtext in den Schlüsselableitungsschritt selbst ein, sodass sie sich auf keine Eigenschaft des internen Hashens im Kombinierer stützt. XWing.Encapsulate MUSS die Gültigkeitsprüfung des öffentlichen Schlüssels aus der festgelegten Revision anwenden und sich weigern, an einen Schlüssel zu kapseln, der sie nicht besteht; die Untergrenze „nie unter die klassische X25519-Sicherheit" ist auf gültig erzeugte Schlüssel beschränkt, und wer die Prüfung auslässt, verwirkt die Untergrenze für diesen Empfänger. Die KEM-Konformitätsvektoren pinnen die Kapselung gegen Draft-10, sodass eine Abweichung bei der Draft-Revision sofort auffällt.

Ein CEK, zwei Pfade zur Schlüsselzustellung

Ein versiegelter Datensatz verschlüsselt den Klartext einmal unter einem einzigen Inhaltsverschlüsselungsschlüssel (CEK) und stellt diesen CEK dann über einen von zwei einander ausschließenden Pfaden zu, unterschieden durch die Anwesenheit von Feldern, es gibt kein Modus-Tag:

  • Slot-Pfad: Der CEK wird unabhängig für jeden Empfänger unter einem Slot-Schlüsselverschlüsselungsschlüssel umhüllt. enc trägt slots (sowie kem, slots_mac).
  • Passphrase-Pfad: Der CEK wird direkt aus einer normalisierten Passphrase über Argon2id abgeleitet. enc trägt passphrase; es trägt kein kem, kein slots und kein slots_mac.

Beide Pfade teilen enc.scheme (immer 1; alles andere ablehnen), enc.aead (chacha20-poly1305-stream64k) und enc.nonce (24 Bytes). Sie unterscheiden sich darin, wo das Schlüssel-Commitment liegt: on-chain in slots_mac auf dem Slot-Pfad, in einem 32-Byte-Header innerhalb des Chiffretext-Blobs auf dem Passphrase-Pfad. Beide binden den Hash-Anspruch des Elements in ihr Transkript ein, und beide versiegeln den Inhalt im selben segmentierten STREAM; der Unterschied liegt in der Schlüsselzustellung und im Commitment, nicht in der Inhaltsschicht.

Wrap pro Slot (Slot-Pfad)

Wählen Sie einen KEM für den ganzen Datensatz, mischen Sie nie KEMs innerhalb eines einzelnen slots[]. Für jeden der N Empfänger leiten Sie einen frischen Slot-Schlüsselverschlüsselungsschlüssel ab und umhüllen den gleichen CEK darunter mit ChaCha20-Poly1305 bei einem 12-Byte-Null-Nonce, AAD auf das Info-Label dieses KEM gesetzt (nie leere AAD), was genau 48 Bytes erzeugt (32 Byte CEK-Geheimtext + 16 Byte Tag). Der Null-Nonce ist nur sicher, weil der Schlüsselverschlüsselungsschlüssel pro Slot gilt; siehe die Eindeutigkeits-Schutzmaßnahme weiter unten.

x25519 (klassisch). Frisches ephemeres X25519-Schlüsselpaar pro Slot:

priv_epk : randomBytes(32)                        ; fresh per slot
pub_epk  : x25519_publicKey(priv_epk)
shared   : x25519_sharedSecret(priv_epk, pub_R)   ; reject all-zero result
kek_salt : SHA-256("cardano-poe-x25519-kek-salt-v1" || enc.nonce || pub_epk || pub_R)   ; 32 B
KEK      : HKDF-SHA-256(ikm  = shared, salt = kek_salt,
                        info = "cardano-poe-kek-v1", L = 32)
wrap     : ChaCha20-Poly1305(key = KEK, nonce = zeros(12),
                             ad = "cardano-poe-kek-v1", plaintext = CEK)   ; 48 B
slot     : { "epk": pub_epk, "wrap": wrap }

mlkem768x25519 (hybrid; X-Wing). Frische X-Wing-Kapselung pro Slot:

enc    = XWing.Encapsulate(pub_R)       ; named fields — MUST NOT consume positional order
kem_ct = enc.ct                         ; 1120 B
shared = enc.ss                         ; 32 B
kek_salt : SHA-256("cardano-poe-xwing-kek-salt-v1" || enc.nonce || kem_ct || pub_R)   ; 32 B
KEK      : HKDF-SHA-256(ikm  = shared, salt = kek_salt,
                        info = "cardano-poe-kek-mlkem768x25519-v1", L = 32)
wrap     : ChaCha20-Poly1305(key = KEK, nonce = zeros(12),
                             ad = "cardano-poe-kek-mlkem768x25519-v1", plaintext = CEK)
slot     : { "kem_ct": kem_ct, "wrap": wrap }   ; kem_ct = single 1120-byte byte string

Beide Salts haben eine Form, SHA-256(label || enc.nonce || <Slot-KEM-Material> || pub_R), die das 32-Byte-Ephemere pub_epk auf dem klassischen Pfad und den 1120-Byte-X-Wing-Geheimtext kem_ct auf dem hybriden Pfad trägt; || ist Byte-Verkettung, und jedes Salt-Präfix-Literal ist exaktes ASCII ohne Terminator und ohne Längenpräfix. pub_R ist der kanonische Wire-Schlüssel des Empfängers (32 B bei x25519, die festgelegten 1216 B bei mlkem768x25519). Der hybride Slot trägt kein separates epk – das ephemere X25519 sind die letzten 32 Bytes von kem_ct –, und kem_ct ist ein einzelner CBOR-Byte-String von exakt 1120 Bytes: Nur der gesamte Datensatzkörper wird für den Transport gestückelt, niemals ein einzelnes Feld.

Das Salt bindet drei Werte: das KEM-Material des Slots (KEK slot-eindeutig), pub_R (vereitelt ein Confused-Deputy-Weiterreichen gegen einen anderen Empfänger) und enc.nonce (verankert den KEK an einen Umschlag, sodass wiederholte KEM-Zufälligkeit nur zu datensatzübergreifender Verknüpfbarkeit degradiert). Die distinkten Info-Labels liefern KEM-übergreifende Domänentrennung, sodass kein unter einem KEM abgeleiteter KEK einem unter dem anderen abgeleiteten bei identischem gemeinsamem Geheimnis gleichen kann. Verwenden Sie jedes der elf internen Labels Byte für Byte: cardano-poe-kek-v1, cardano-poe-kek-mlkem768x25519-v1, cardano-poe-x25519-kek-salt-v1, cardano-poe-xwing-kek-salt-v1, cardano-poe-item-hashes-v1, cardano-poe-slots-transcript-v1, cardano-poe-slots-mac-v1, cardano-poe-passphrase-transcript-v1, cardano-poe-passphrase-mac-v1, cardano-poe-payload-v1, cardano-poe-payload-passphrase-v1. Keines davon wird je auf der Übertragungsstrecke serialisiert; sie sind feste Konstanten, nicht über ein Register wählbar. Ein einziges abweichendes Byte ergibt einen slots_mac, ein Commitment oder einen AEAD-Tag, den der ehrliche Erzeuger nicht reproduzieren kann.

Mischen, bevor Sie den MAC bilden. Die Eingabereihenfolge („primärer Empfänger zuerst“) ist privilegierte Metainformation; Slots in Eingabereihenfolge zu veröffentlichen, gibt sie preis. Mischen Sie slots[] mit einem CSPRNG über eine unverzerrte Fisher-Yates-Permutation, eine bloße Indexziehung u32 % m neigt zu niedrigen Resten und muss per Rejection-Sampling auf einen gleichverteilten Index gebracht werden, bevor Sie den Slot-Satz-MAC berechnen, der die gemischte On-Wire-Reihenfolge bindet.

Slot-Satz-MAC: das Transkript hashen, dann unter dem CEK HMAC

Der Slot-Satz-MAC bindet die gesamte Slot-Menge, samt der Header-Felder, die festlegen, wie die Slots gelesen werden, an den CEK. Bauen Sie ihn in zwei Schritten: ein geschlossenes Transkript einmal hashen, dann diesen Hash per HMAC absichern:

hashes_hash : SHA-256("cardano-poe-item-hashes-v1" || canonicalEncode(item.hashes))   ; 32 B

SLOTS_TRANSCRIPT = {                          ; closed 7-key map; keys are a set, not an order
    "scheme":      1,
    "path":        "slots",
    "aead":        <enc.aead>,                ; the content-format identifier
    "kem":         <enc.kem>,                 ; "x25519" | "mlkem768x25519"
    "nonce":       <enc.nonce>,               ; bytes(24)
    "slots":       <slots>,                   ; the shuffled on-wire slot array
    "hashes_hash": hashes_hash                ; bytes(32), over this item's hashes
}
slots_hash : SHA-256("cardano-poe-slots-transcript-v1" || canonicalEncode(SLOTS_TRANSCRIPT))
HMAC_KEY   : HKDF-SHA-256(ikm = CEK, salt = "", info = "cardano-poe-slots-mac-v1", L = 32)
slots_mac  : HMAC-SHA-256(key = HMAC_KEY, msg = slots_hash)   ; 32 B

Drei Dinge entscheiden hier über die Parität:

  • Das Transkript ist eine geschlossene Map, serialisiert von canonicalEncode. Seine Schlüsselreihenfolge ist die Sortierung nach RFC 8949 §4.2.1, niemals von Hand arrangiert. Dass scheme, path, aead, kem und nonce neben den Slots gepinnt sind, bedeutet: Ein Relay, das irgendein Header-Feld umstellt, selbst bei weiterhin gültigen Slot-Formen, ändert slots_hash und bricht den MAC.
  • Das Transkript bindet den Hash-Anspruch des Elements. hashes_hash ist ein markierter SHA-256 über die canonicalEncode-Form der vollständigen hashes-Map des Elements. Da der Empfänger slots_mac allein aus On-Chain-Bytes neu berechnet, bestätigt ein MAC-Treffer, dass der Umschlag für genau diesen Hash-Anspruch versiegelt wurde – ein Umschlag, der auf ein Element mit einer anderen hashes-Map gespleißt wurde, scheitert am On-Chain-Abgleich, noch vor jedem Chiffretext-Abruf. Der slots-Wert ist das gemischte Array der On-Wire-Slot-Maps direkt: Jedes Slot-Feld ist ein einzelner Byte-String (epk 32 B, kem_ct 1120 B), sodass es keine Stückelung pro Feld zu kanonisieren gibt.
  • slots_hash wird einmal berechnet und über die Probeentschlüsselungsschleife konstant gehalten. Die MAC-Prüfung pro Slot rekeyt HMAC aus jedem Kandidaten-CEK, aber stets über denselben 32-Byte-slots_hash. Das Vorhashen lässt das CEK-geschlüsselte Commitment intakt: Es ändert die HMAC-Nachricht vom vollständigen Transkript zu dessen SHA-256, nichts weiter.

Der MAC-Algorithmus, seine Schlüsselableitung und das Transkriptschema sind allesamt durch enc.scheme = 1 festgelegt und für beide KEMs identisch; es gibt keinen On-Wire-MAC-Bezeichner. slots_mac ist genau 32 Bytes lang und wird in konstanter Zeit verifiziert.

Inhaltsverschlüsselung: der segmentierte STREAM

Verschlüsseln Sie den Klartext einmal im segmentierten STREAM unter einem Inhaltsschlüssel, der aus dem CEK abgeleitet ist. Der Inhaltsschlüssel ist ein eigenes HKDF-Blatt des CEK – gesalzen mit enc.nonce, unter einem pfadspezifischen info –, sodass die Wrap-Schicht und die Inhaltsschicht nie dasselbe Primitive auf denselben Bytes schlüsseln:

content_key : HKDF-SHA-256(ikm = CEK, salt = enc.nonce, info = "cardano-poe-payload-v1", L = 32)

; STREAM (chacha20-poly1305-stream64k):
CHUNK_SIZE  : 65536 plaintext bytes per non-final chunk
chunk nonce : uint88_be(counter) || final_flag   ; 12 B; counter from 0, +1 per chunk;
                                                 ; final_flag = 0x01 on the last chunk, else 0x00
per-chunk AAD : empty
ciphertext  : seal(chunk_0) || seal(chunk_1) || ... || seal(chunk_final)
              ; each chunk sealed with ChaCha20-Poly1305 under content_key; sealed = plaintext + 16 B

Die AAD pro Chunk ist leer – und das ist korrekt, keine Auslassung: Der Inhaltsschlüssel leitet sich aus dem CEK ab, und der CEK ist bereits an den vollständigen Header gebunden (einschließlich hashes_hash) durch slots_mac. Verändern Sie ein Header-Feld, und der Empfänger leitet einen anderen Inhaltsschlüssel ab, sodass der Stream sich nicht öffnen lässt; eine AAD pro Chunk würde denselben Kontext bei jedem Chunk erneut binden, ohne Sicherheit hinzuzufügen. Die Zähler-Nonces sind sicher, weil der Inhaltsschlüssel einmalig ist (ein frischer CEK, gesalzen mit der umschlageindeutigen enc.nonce), sodass nie zwei Streams ein (key, nonce)-Paar teilen.

Bauen Sie den STREAM so, dass eine Abschneidung erkennbar ist: Jeder nicht-finale Chunk ist exakt 65536 Klartext-Bytes, der finale Chunk trägt final_flag = 0x01 und 0–65536 Bytes (ein leerer Klartext ist ein einzelner finaler Chunk der Länge null – ein einzelner 16-Byte-Tag), und ein Verifizierer MUSS scheitern (TAMPERED_CIPHERTEXT) bei einem fehlenden Final-Flag, einem Final-Flag auf einem nicht-finalen Chunk, Daten nach dem finalen Chunk oder einem zu kurzen nicht-finalen Chunk. Verifizieren Sie den Tag jedes Chunks, bevor Sie dessen Klartext freigeben, und behandeln Sie freigegebene Bytes als vorläufig, bis die erneute Hash-Prüfung nach der Entschlüsselung besteht.

Der Klartext sind die exakten ursprünglichen Inhaltsbytes; die Konstruktion stellt keinen Dateinamen, keinen MIME-Typ, kein Größenfeld und keinen Metadaten-Wrapper voran, hängt nichts an und verschlüsselt nichts davon. Der veröffentlichte Chiffretext-Blob sind die STREAM-Chunks (auf dem Passphrase-Pfad mit dem unten beschriebenen 32-Byte-Commitment-Header davor). Die zusammengesetzte enc-Map und die resultierende URI gehen auf die Chain; die Chiffretext-Bytes nicht – veröffentlichen Sie sie in einem inhaltsadressierten Speicher und legen Sie die ar://- oder ipfs://-URI in das uris[] des Elements.

Passphrase-Pfad

Wenn es keine Empfänger gibt, leiten Sie den CEK aus einer normalisierten Passphrase mit Argon2id ab. Es gibt kein epk, keinen Wrap pro Slot, keinen Slot-Satz-MAC und keine Probeentschlüsselungsschleife. Das Schlüssel-Commitment, das auf dem Slot-Pfad slots_mac liefert, liegt hier stattdessen in einem 32-Byte-Header innerhalb des Chiffretext-Blobs, den STREAM-Chunks vorangestellt:

passphrase_bytes = utf8(normalize(passphrase))   ; cardano-poe-pw-norm-v1
CEK = argon2id(passphrase_bytes, salt = enc.passphrase.salt,
               params = enc.passphrase.params, L = 32)

hashes_hash = SHA-256("cardano-poe-item-hashes-v1" || canonicalEncode(item.hashes))

PASSPHRASE_TRANSCRIPT = {                     ; closed 6-key map; keys are a set, not an order
    "scheme":      1,
    "path":        "passphrase",
    "aead":        <enc.aead>,
    "nonce":       <enc.nonce>,               ; bytes(24)
    "hashes_hash": hashes_hash,               ; bytes(32), over this item's hashes
    "passphrase": {                           ; closed sub-map
        "alg":           "argon2id",
        "salt":          enc.passphrase.salt,
        "params":        { "m": m, "t": t, "p": p },
        "normalization": "cardano-poe-pw-norm-v1"   ; scheme-fixed constant, NOT on the wire
    }
}
pw_hash     = SHA-256("cardano-poe-passphrase-transcript-v1" || canonicalEncode(PASSPHRASE_TRANSCRIPT))
PW_MAC_KEY  = HKDF-SHA-256(ikm = CEK, salt = "", info = "cardano-poe-passphrase-mac-v1", L = 32)
commitment  = HMAC-SHA-256(key = PW_MAC_KEY, msg = pw_hash)   ; 32 B

content_key = HKDF-SHA-256(ikm = CEK, salt = enc.nonce, info = "cardano-poe-payload-passphrase-v1", L = 32)
ciphertext blob = commitment || STREAM chunks                 ; STREAM under content_key

Das PASSPHRASE_TRANSCRIPT bindet die KDF-Parameter, die Header-Felder und den Hash-Anspruch des Elements in das Commitment: Eine Manipulation an salt, einem params-Wert, nonce, aead – oder das Einspleißen des Umschlags auf einen anderen Hash-Anspruch – ergibt einen anderen pw_hash, und die Commitment-Prüfung scheitert. Der "normalization"-Wert ist eine schemafeste Konstante, die in das Transkript eingespeist wird, um exakt das Profil festzuhalten, unter dem der CEK abgeleitet wurde; er wird nie auf der Übertragungsstrecke serialisiert (der Erzeuger gibt nur { alg, salt, params } aus).

Auf der Verifizierungsseite leiten Sie den Kandidaten-CEK ab, lesen die führenden 32 Bytes des Chiffretext-Blobs, berechnen das Commitment neu und vergleichen in konstanter Zeit, bevor irgendein STREAM-Chunk geöffnet wird. Ein Blob, der kürzer als 48 Bytes ist (32-Byte-Commitment + 16-Byte-Minimum des STREAM), ist fehlgeformt (TAMPERED_CIPHERTEXT). Bei einem Nichttreffer – falsche Passphrase, manipulierter salt / params / Header oder ein eingespleißter Umschlag – fördern Sie denselben einzelnen generischen Fehlschlag zutage und beginnen das Streaming nicht; eine falsche Passphrase ist von einem manipulierten Datensatz nicht zu unterscheiden. Das Commitment liegt bewusst off-chain: Ein On-Chain-Commitment wäre ein kostenloses Offline-Rate-Orakel für jeden Passphrase-Datensatz, einschließlich solcher, deren Chiffretext zurückgehalten wird.

Setzen Sie die Parameter-Untergrenzen durch: salt-Länge 16–64 Bytes; m ≥ 65536 KiB (≈ 64 MiB), t ≥ 3, p ≥ 1. Pinnen Sie die Argon2-Version auf 0x13 (19); keine andere Version ist unter enc.scheme: 1 zulässig, und es gibt kein Versionsfeld auf der Übertragungsstrecke. Wo die Plattform es unterstützt, SOLLTEN Erzeuger p = 4 ausgeben (das zweite empfohlene Profil aus RFC 9106 §4); Verifizierer DÜRFEN jedes p ≥ 1 akzeptieren, vorbehaltlich der Deployment-Obergrenzen. Argon2id überspannt Ökosystemgrenzen sauber, der Parametersatz, nicht die Bibliothek, ist der Vertrag, sodass ein festes (m, t, p, salt, len, password) in jeder Implementierung byte-identische Ausgabe liefern muss. Die Bindung zwischen einer Passphrase und ihrem Umschlag ist das oben beschriebene Commitment im Chiffretext; eine falsche Passphrase und ein manipulierter Chiffretext zeigen sich beide als ein einziger generischer Fehlschlag.

Begrenzen Sie die rohe Passphrase vor der Normalisierung und Argon2id: Lehnen Sie jede Eingabe ab, die länger als die Referenzgrenze MAX_PASSPHRASE_INPUT_BYTES = 4096 UTF-8-Bytes ist, damit eine pathologische Passphrase keine Dienstverweigerung vor dem KDF antreiben kann. Wie die MAX_SLOTS- und die Umschlaggrenze auf dem Slots-Pfad ist dies eine deployment-festgelegte Konstante, die Sie verschärfen DÜRFEN, kein Wire-Feld.

Das Normalisierungsprofil ist normativ

Zwei Implementierungen MÜSSEN aus derselben Passphrase einen byte-identischen CEK ableiten, und der einzige Weg, das zu garantieren, ist eine gepinnte Normalisierung. Das Profil cardano-poe-pw-norm-v1, in dieser Reihenfolge angewendet:

  1. Nicht zugewiesene Codepoints ablehnen, eine Passphrase, die einen unter Unicode 16.0 nicht zugewiesenen Codepoint enthält, wird abgelehnt (ENC_PASSPHRASE_UNNORMALIZABLE), bevor irgendeine Normalisierung läuft. Unicode garantiert Normalisierungsstabilität nur über zugewiesene Codepoints, sodass dies ein Loch für künftiges Driften schließt und für ehrliche Nutzer unsichtbar ist.
  2. NFKC, Normalisierungsform KC nach UAX #15 unter Unicode 16.0.
  3. Whitespace, definiert als jedes Zeichen, das unter Unicode 16.0 die Unicode-Eigenschaft White_Space trägt; jede maximale Folge wird zu einem einzigen U+0020 SPACE zusammengezogen.
  4. Trim, führende und nachfolgende Whitespace entfernen.
  5. Leere ablehnen, ist das Ergebnis der leere String, ablehnen (ENC_PASSPHRASE_EMPTY); eine Passphrase aus reinem Whitespace würde den Datensatz sonst an einen CEK schlüsseln, den jede Partei ableiten kann.
  6. Encode, UTF-8; diese Bytes sind die Argon2id-Passworteingabe.

Pinnen Sie Unicode wörtlich auf 16.0 und lassen Sie es nicht treiben: Die Menge der White_Space-Eigenschaft, die Menge der zugewiesenen Codepoints und die NFKC-Mapping-Tabellen sind allesamt versionsabhängig, sodass das Auflösen des Profils gegen eine andere Unicode-Version aus derselben Passphrase einen anderen CEK ableiten und einen ehrlichen Datensatz nicht öffnen kann. Eine künftige Revision, die eine neuere Unicode-Version übernimmt, tut dies unter einer neuen Profilkennung, niemals durch Neuinterpretation von cardano-poe-pw-norm-v1.

Probeentschlüsselung: jeden Slot öffnen, den MAC einbeziehen, generisch scheitern

Ein Empfänger besitzt einen KEM-Privatschlüssel und entdeckt seinen Slot, indem er versucht, jeden zu öffnen, öffentliche Empfängerschlüssel stehen nicht auf der Leitung. Bevor Sie irgendein KEM- oder AEAD-Primitive aufrufen, führen Sie die Ressourcengrenzen aus, dann die strukturellen Schutzprüfungen. Begrenzen Sie zuerst den Ressourcenverbrauch des Parsers: Lehnen Sie einen Umschlag ab, dessen dekodierte Größe 65536 Bytes überschreitet (ENC_ENVELOPE_TOO_LARGE) oder dessen slots[] MAX_SLOTS = 1024 überschreitet (ENC_SLOTS_TOO_MANY). Beide Referenzgrenzen liegen weit über der ~16-KiB-Transaktionsmetadatengrenze von Cardano, die einen ehrlichen Datensatz beschränkt; sie sind deployment-festgelegte Konstanten, die Sie verschärfen DÜRFEN, niemals Wire-Felder. Dann die strukturellen Schutzprüfungen: scheme == 1; aead, kem registriert; nonce 24 Bytes; slots_mac 32 Bytes; slots nicht leer; Empfänger-Secret 32 Bytes; jeder wrap 48 Bytes; je nach KEM jedes epk exakt 32 Bytes ohne kem_ct (x25519) oder jedes kem_ct exakt 1120 Bytes ohne epk (mlkem768x25519).

Lehnen Sie Dubletten der Kapselung innerhalb des Datensatzes hier ab, vor jedem Primitive. Alle epk-Werte müssen auf dem klassischen Pfad eindeutig sein, alle kem_ct-Werte auf dem hybriden Pfad; eine Dublette löst ENC_SLOTS_DUPLICATE_KEM_MATERIAL aus. Das ist der verifizierer-prüfbare Anteil der Invariante zur Eindeutigkeit des Slot-KEK, auf die sich der Null-Nonce-Wrap stützt; Wiederverwendung über Datensätze oder Schlüssel hinweg ist eine Erzeugerpflicht, die kein Verifizierer erkennen kann. Diese Ablehnung greift allein bei einem wiederholten epk / kem_ct; zweimal an denselben Empfänger zu versiegeln, mit frischen Ephemerals pro Slot, ist rechtmäßig und löst sie nicht aus (siehe die Regel zu mehreren Treffern weiter unten). unwrap-negative trägt den Fall der epk-Dublette mit KEK-Wiederverwendung.

Dann läuft die Schleife, wobei Sie slots_hash einmal davor neu berechnen und konstant halten:

found        = false
cek_conflict = false
selected_CEK = 0^32
for slot in slots:                            ; iterate ALL slots — no early break
    ; derive KEK per-KEM, as in the wrap recipe. For x25519 the all-zero shared
    ; secret is rejected via a secret-independent bit, not an early branch:
    ;   kem_ok = NOT constantTimeEqual(shared, 0^32)
    ;   KEK    = ct_select(kem_ok, real_KEK, dummy_KEK)   ; dummy_KEK from ikm=0^32, same salt/info
    ; (XWing.Decapsulate has no all-zero case; kem_ok stays true on the hybrid path.)
    open_ok, candidate_CEK = ChaCha20-Poly1305_open_or_dummy(KEK, zeros(12), kem_info_label, slot.wrap)
    HMAC_KEY = HKDF-SHA-256(candidate_CEK, salt = "", info = "cardano-poe-slots-mac-v1", L = 32)
    mac_ok   = constantTimeEqual(HMAC-SHA-256(HMAC_KEY, slots_hash), slots_mac)
    ok       = kem_ok AND open_ok AND mac_ok                     ; kem_ok folded into acceptance
    first        = ok AND NOT found                              ; first matching slot
    cek_conflict = cek_conflict OR (ok AND found AND NOT constantTimeEqual(candidate_CEK, selected_CEK))
    selected_CEK = ct_select(first, candidate_CEK, selected_CEK)   ; constant-time
    found        = found OR ok
if NOT found:    reject (single generic failure)
if cek_conflict: reject (single generic failure)
content_key = HKDF-SHA-256(selected_CEK, salt = enc.nonce, info = "cardano-poe-payload-v1", L = 32)
plaintext   = STREAM_open(content_key, ciphertext)   ; per-chunk authenticated release
if STREAM_open fails at any chunk: reject (single generic failure)   ; TAMPERED_CIPHERTEXT

Die nicht verhandelbaren Punkte in dieser Schleife:

  • Öffnen Sie atomar; geben Sie nie unverifizierten Klartext frei. Beide *_open_or_dummy-Primitive sind atomar: Bei einem Fehlschlag des AEAD-Tags geben sie keinen Klartext zurück, und der zurückgegebene Kandidat (der umhüllte CEK oder der Inhalts-Klartext) ist ein fester oder pseudozufälliger Dummy, der vom fehlgeschlagenen Chiffretext unabhängig ist. Das ist es, was die Schleife einen candidate_CEK über ein fehlgeschlagenes Öffnen des Wrap hinweg tragen lässt, ohne je unauthentifizierte Bytes offenzulegen.
  • Beziehen Sie die Nullbyte-Prüfung in ein geheimnisunabhängiges kem_ok-Bit ein. Berechnen Sie kem_ok = NOT constantTimeEqual(shared, 0^32) für den x25519-Pfad, wählen Sie den KEK in konstanter Zeit zwischen dem echten KEK und einem aus 0^32 unter demselben Salt und info abgeleiteten Dummy-KEK, und beziehen Sie kem_ok in die Annahme ein (ok = kem_ok AND open_ok AND mac_ok). Verzweigen Sie bei einem ungültigen Anteil nicht vorzeitig hinaus, ein Slot mit ungültigem ECDH kann nie angenommen werden, und die Schleife leistet dennoch identische Arbeit. (XWing.Decapsulate hat keinen Nullbyte-Fall, sodass kem_ok auf dem hybriden Pfad fest wahr ist.)
  • Beziehen Sie die slots_mac-Prüfung in die Schleife ein. Ein böswilliger Absender kann einen Slot bauen, der sich unter dem Schlüssel des Empfängers mit einem vom Angreifer gewählten CEK öffnet (ohne Kenntnis des Privatschlüssels). Den ersten AEAD-Erfolg als „unseren“ zu akzeptieren, ließe diesen gefälschten Slot einen ehrlichen überschatten. Zu verlangen, dass der Kandidaten-CEK auch slots_mac über slots_hash reproduziert, vereitelt Slot-Austausch, Slot-Entfernung und Slot-Umordnung. Lassen Sie das nie aus.
  • Erlauben Sie mehrere Treffer; lehnen Sie nur einen CEK-Konflikt ab. Ein Empfängerschlüssel DARF rechtmäßig mehr als einen Slot treffen, denselben CEK an denselben Empfänger in mehreren Slots zu versiegeln, jeden mit frischen Ephemerals, ist gültiges Auffüllen der Empfängeranzahl und löst die Ablehnung doppelter epk/kem_ct nicht aus. Wählen Sie den CEK des ersten Treffers und lehnen Sie nicht allein deshalb ab, weil mehr als ein Slot getroffen hat. Die einzige abzulehnende Anomalie sind zwei treffende Slots, die unterschiedliche CEKs wiederherstellen (Konstantzeitvergleich): Führen Sie ein cek_conflict-Bit mit und fördern Sie den einzelnen generischen Fehlschlag zutage, falls es gesetzt ist. Das ist Verteidigung in der Tiefe: Unter dem Commitment auf die Slot-Menge ist ein Treffer mit abweichendem CEK bereits unmöglich, sodass es gegen eine fehlerhafte Implementierung „fail closed“ fällt.
  • Iterieren Sie alle Slots innerhalb des Durchlaufs eines einzelnen Privatschlüssels, eine konstante Anzahl von Slot-Operationen pro Schlüssel, ohne vorzeitigen Abbruch, sodass ein Zeitbeobachter nicht herleiten kann, welcher Slot passte. Treiben Sie die Nullbyte-Ablehnung über kem_ok und Pseudoarbeit voran, statt vorzeitig auszusteigen. Ein Empfänger mit mehreren Schlüsseln iteriert Schlüssel × Slot und DARF über die Schlüssel hinweg vorzeitig abbrechen (was nur das schwache Signal „welcher Schlüssel passte“ preisgibt), muss aber über die Slots eines einzelnen Schlüssels konstantzeitig bleiben, und muss die pub_R-Hälfte des Salts pro Schlüssel neu ableiten, da beide KEMs den eigenen öffentlichen Schlüssel des Empfängers in das KEK-Salt einbinden. Binden Sie dieses Salt an die kanonische Wire-Kodierung des Schlüssels, genau der 32-Byte-X25519-Public-Key oder genau die festgelegten 1216-Byte-X-Wing-Public-Key-Bytes, niemals eine nicht-kanonische Neukodierung, sonst leiten beide Seiten unterschiedliche KEKs ab.
  • Reichen Sie nicht vertrauenswürdigen Aufrufern eine generische Fehlschlag-Form. Intern dürfen Sie für die lokale Diagnose typisierte Ergebnisse führen, WRONG_RECIPIENT_KEY (kein Slot geöffnet), TAMPERED_HEADER (ein Slot wurde geöffnet, aber kein Kandidaten-CEK reproduzierte slots_mac), TAMPERED_CIPHERTEXT (die Inhalts-AEAD scheiterte, nachdem ein CEK zurückgewonnen und der MAC verifiziert war), aber ein externer Beobachter DARF sie nicht nach der Form der Antwort unterscheiden können. Zum Timing ist das Modell bewusst eingegrenzt: Ein Verifizierer DARF an der if NOT found-Prüfung vor der Inhaltsentschlüsselung zurückkehren, was einen Nicht-Empfänger von einem Empfänger trennt, dessen Chiffretext nicht aufgeht. Das verrät allein Empfänger-versus-Nicht-Empfänger, nie welcher Slot oder welches Schlüsselmaterial; ein einheitliches Timing zwischen diesen beiden Fällen ist nicht erforderlich, und ein Dummy-Inhaltsöffnen DARF NICHT vorgeschrieben werden. Die Konstantzeit-Garantie, die gilt, ist die Über-alle-Slots-Invariante oben.
  • Berechnen Sie den Klartext-Hash nach der Entschlüsselung neu und vergleichen Sie ihn. Die On-Chain-hashes-Map legt sich auf den Klartext fest, nicht auf den Geheimtext, sodass der Empfänger (auf der Anwendungsebene) den Digest neu berechnen und vergleichen muss: Der sha2-256-Eintrag muss übereinstimmen, und blake2b-256, falls vorhanden. Eine Abweichung bedeutet, dass der Hash-Anspruch des Datensatzes nicht zu den entschlüsselten Bytes passt, handeln Sie dann nicht auf Grundlage des Klartexts. Der strukturelle Validator entschlüsselt nie.

Die Nutzlast auf beiden Seiten begrenzen

Der segmentierte STREAM erlegt keine kryptografische Nutzlast-Obergrenze auf: Der 88-Bit-Zähler pro Chunk lässt 2^88 Chunks zu, und jeder Chunk wird unter einem distinkten (content_key, nonce)-Paar klar innerhalb der Einzelaufruf-Grenze von RFC 8439 versiegelt, sodass es kein Risiko eines Zählerüberlaufs gibt, gegen das man sich schützen müsste. Das Maximum, das ein Erzeuger oder Verifizierer durchsetzt, ist daher eine Denial-of-Service-Richtlinie des Deployments, keine Wire-Konstante – setzen Sie sie inkrementell durch, während der Stream geschrieben oder gelesen wird, und brechen Sie ab, bevor eine übergroße Nutzlast gepuffert wird. Eine Abschneidung wird strukturell durch das Final-Flag erkannt, nicht durch eine Größengrenze. Dieselbe Haltung gilt auf dem Slot-Pfad und dem Passphrase-Pfad.

Konformitäts-Fixtures der versiegelten PoE

Die versiegelte PoE-Ecke des Korpus ist der Ort, an dem die meisten sprachübergreifenden Bugs auftauchen. Treiben Sie Ihre Implementierung durch alles davon. Die positiven Fixtures pinnen das deterministische Wrap und die Probeentschlüsselungsschleife für beide KEMs – ein- und mehrere Empfänger, gemischtes N und den Worst-Case mit mehreren Privatschlüsseln – sowie den rechtmäßigen Fall eines Empfängers, der zwei Slots trifft (frische Ephemerals, derselbe CEK, MUSS entschlüsseln, sodass eine Implementierung, die mehrere Treffer ablehnt, hier scheitert) und den Passphrase-Pfad (Commitment-Header plus STREAM-Chunks in einem Blob). Ein eigener STREAM-Layout-Satz pinnt einen leeren Klartext (ein finaler Chunk der Länge null), eine Nutzlast aus einem Chunk und eine Nutzlast aus mehreren Chunks, die die 65536-Byte-Grenze überschreitet. Gezielte KATs pinnen beide KEK-Salts (SHA-256(label ‖ enc.nonce ‖ <KEM material> ‖ pub_R)), hashes_hash und seinen Platz in beiden Transkripten, die X-Wing-Kapselung gegen Draft-10, das HKDF-Extract mit Null-Längen-Salt (die Konvention für ein fehlendes Salt aus RFC 5869 §2.2, das die slots_mac-Schlüsselableitung spiegelt), die Bech32-Empfänger-/Geheimnis-Kodierungen und die Identitäts-Seed-Kodierung mit Prüfsumme.

Die negativen Fixtures pinnen die Ablehnungscodes: einen gefälschten Schatten-Slot vor einem ehrlichen Slot (der Datensatz MUSS dennoch unter dem ehrlichen CEK entschlüsseln); ein Header-Umkippen (kem/aead/scheme), das die Slot-Formen gültig lässt; ein hashes-Spleißen auf ein Element mit einem anderen Hash-Anspruch; die Passphrase-Commitment-Fehlschläge (falsche Passphrase, manipulierter salt/params, manipulierter Header – alle scheitern, bevor irgendein Chunk öffnet); die Passphrase-Normalisierungs-Ablehnungen (eine Eingabe mit nicht zugewiesenem Codepoint und eine Eingabe aus reinem Whitespace); das X25519-Geheimnis aus lauter Nullen; die Dublette eines Slots innerhalb des Datensatzes; und die STREAM-Manipulationsfälle (gekippter Chunk-Tag, abgeschnittener Stream, nachfolgende Daten, zu kurzer nicht-finaler Chunk). Zwei Eigenschaften haben keinen Byte-Vektor und werden stattdessen per Verhalten bestätigt: die Ablehnung des CEK-Konflikts (eine zu konstruieren ist genau die Multi-Key-Commitment-Kollision, die der Standard als unmöglich annimmt) und die Konstantzeit-über-alle-Slots-Garantie. Reproduzieren Sie jede gepinnte Byte-Zeichenkette und geben Sie für jeden negativen Fall den exakten Code aus.

Eine Eigenschaft der versiegelten PoE hat keinen Byte-Vektor: die Ablehnung des CEK-Konflikts, also zwei treffende Slots, die unterschiedliche CEKs wiederherstellen, lässt sich nicht als Fixture konstruieren, denn eines zu konstruieren ist genau die Multi-Key-Commitment-Kollision, die der Standard als unmöglich annimmt. Pinnen Sie sie stattdessen mit einem implementierungsnahen Verhaltenstest, der bestätigt, dass Ihre Probeentschlüsselungsschleife bei einem erzwungenen Konflikt „fail closed“ fällt, ebenso wie die Konstantzeit-über-alle-Slots-Eigenschaft per Verhalten statt als Byte-Zeichenkette bestätigt wird.

Konformität und Testvektoren

Die normativen Testvektoren sind der Interoperabilitätsvertrag. Eine Implementierung ist genau dann konform, wenn sie jeden festgelegten Byte-String der Konformitäts-Suite aus denselben Eingaben reproduziert und für jedes negative Fixture den korrekten typisierten Fehlercode ausgibt. Es gibt keine Teilpunkte und keine Berufung: Schlägt ein Vergleich fehl, ist die Implementierung falsch, niemals der Vektor.

Die Vektoren liegen in der Konformitäts-Suite des Standards, gegliedert nach Primitivklasse: Datensatz-Fixtures, Wrap/Unwrap für versiegelte PoE, COSE_Sign1-Signaturen, HKDF, Seed-Ableitung, Argon2id und kanonisches CBOR. Jeder legt die Eingaben in Hex in Kleinbuchstaben und die erwarteten Ausgaben fest. So verwenden Sie sie: Geben Sie die Eingaben in Ihre Implementierung ein, vergleichen Sie jede benannte Ausgabe byteweise und korrigieren Sie Ihren Code bei jeder Abweichung.

Drei Pflichten, die jede Implementierung erfüllen muss

Reproduzieren Sie die positiven Vektoren. Für jedes Datensatz-Fixture MÜSSEN beide Hälften gelten: encode(record) == expected_cbor UND der Round-Trip encode(decode(expected_cbor)) == expected_cbor. Der Round-Trip verallgemeinert über die Fixtures hinaus: Für beliebige wohlgeformte Eingaben gilt encode(decode(x)) == x. Ein Decoder, der Informationen verliert oder umordnet, oder ein Encoder, der nicht kanonisch ist, verletzt dies und scheitert an der Konformität.

Geben Sie die richtigen Ablehnungscodes aus. Die negativen Fixtures koppeln einen absichtlich fehlerhaften Datensatz an den genauen typisierten Fehlercode, den ein struktureller Validator auslösen MUSS. Die Bytes gültiger Datensätze zu reproduzieren ist die eine Hälfte des Vertrags; ungültige mit dem korrekten Code abzulehnen ist die andere. Ein Validator, der einen fehlerhaften Datensatz aus dem falschen Grund ablehnt oder ihn akzeptiert, ist nicht konform. Die negativen Fixtures sind die einzige maßgebliche Quelle für die sprachübergreifende Ablehnungsparität: Dieselbe fehlerhafte Eingabe MUSS in jeder Implementierung denselben Code auslösen. Den vollständigen Katalog der Codes und ihrer Bedeutungen finden Sie unter Verifizierung.

Stimmen Sie mit den Registries überein. Algorithmen-Kennungen sind benannte Zeichenketten aus den Registries unter Algorithmen-Registries. Eine unbekannte Kennung MUSS genau den Code für nicht unterstützte Algorithmen zutage fördern, niemals eine stille Akzeptanz oder einen Panic.

Korrigieren Sie die Implementierung, niemals den Vektor

Die Vektoren sind an die zugrunde liegenden RFCs und an die deterministischen Konstruktionen dieses Standards gebunden. Schlägt ein Vergleich fehl, liegt der Fehler in der geprüften Implementierung. Einen Vektor zu bearbeiten, damit eine Suite durchläuft, verwandelt ein reales Interoperabilitätsproblem in ein latentes, das erst dann auftaucht, wenn ein Datensatz on-chain die Grenze zwischen Implementierungen überschreitet, also zum denkbar ungünstigsten Zeitpunkt.

Lassen Sie die Parität bei jeder Änderung laufen

Eine Implementierung, die mehr als eine Sprache ausliefert oder die Interoperabilität mit einer anderen nachweisen will, SOLLTE einen einzigen Continuous-Integration- Job laufen lassen, der jedes Paket baut, die Testsuite jeder Sprache gegen die gemeinsamen Fixtures ausführt, den Abhängigkeitsgraph-Lint erzwingt und prüft, dass der Fixture-Satz auf beiden Seiten identisch ist. Ein Fixture, das auf einer Seite hinzugefügt wurde, auf der anderen aber nicht, lässt das Gate scheitern: Die beiden Implementierungen sind stillschweigend auseinandergelaufen, und der Build fängt das ab, bevor es ein echter Datensatz tut. Die Fixtures sind die kanonische Quelle; jede Sprache hält ein byte-identisches Abbild davon, und das Gate stellt sicher, dass das Abbild vollständig und exakt ist.

Namens- und Wire-Konventionen

Ein paar Konventionen halten eine Implementierung lesbar und das Wire-Format stabil:

  • Wire-Feldnamen sind snake_caseleaf_count, cose_sign1, slots_mac. Das gilt über alle Sprachen hinweg: Selbst dort, wo eine Sprache für ihre In-Memory-API idiomatisch camelCase verwendet, nutzt der kodierte Datensatz snake_case-Schlüssel, denn die Schlüssel sind Teil der kanonischen Bytes, die eine Signatur abdeckt.
  • Kennungen sind Registry-Strings, keine fest im Code verdrahteten Enums. Hashes, AEADs, KEMs, KDFs und Signaturen verweisen alle auf benannte Kennungen; einen Algorithmus hinzuzufügen (etwa eine Post-Quanten-KEM) ist ein additiver Registry-Eintrag, niemals ein Bruch des Wire-Formats.
  • Sprachübergreifende Methodennamen entsprechen einander semantisch. Eine Funktion in der einen Sprache hat in der anderen ein gleichnamiges Gegenstück (encode_canonical_cborencodeCanonicalCbor), sodass jemand, der in einer der beiden Sprachen zu Hause ist, die eine Schnittstelle auf die andere abbilden und die Parität durch bloßes Lesen nachvollziehen kann.
  • Bringen Sie zuerst die Krypto-Schichten zum Laufen. Stellen Sie den kryptografischen Kern und die Wire-Format-Bibliothek gegen die Vektoren auf und bringen Sie das Paritäts-Gate auf Grün, bevor Sie eine einzige Zeile Anwendungscode schreiben. Der eigenständige Verifizierer ist die kleinste anwendungsnahe Fläche und das Nächste, was Sie bauen sollten; alles Weitere sitzt auf einer Krypto-Schicht, die Sie bereits als korrekt nachgewiesen haben.

Verwandte Seiten

  • Der Datensatz — das Wire-Format, das der Validator und der Encoder umsetzen.
  • Versiegelte PoE — die Konstruktionsreferenz hinter den Bau-Rezepten hier.
  • Algorithmen-Registries — die benannten Kennungen, die eine Implementierung auflöst.
  • Verifizierung — die Validierungspipeline, der eigenständige Verifizierer und der Fehlercode-Katalog.