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:
| Primitive | Gebunden an | Ausgabe, die übereinstimmen muss |
|---|---|---|
| Seed → Ed25519- / X25519-Schlüsselpaar | HKDF-SHA-256 mit den registrierten Info-Konstanten | abgeleiteter öffentlicher und privater Schlüssel |
| HKDF-SHA-256 | RFC 5869 | Output Key Material für feste Eingabe |
| HMAC-SHA-256 Slot-Set-MAC | RFC 2104 | slots_hash- und slots_mac-Tag-Bytes für eine feste CEK und einen festen Slot-Satz |
| Argon2id (Passphrase-KDF) | RFC 9106 | abgeleiteter Schlüssel für feste (m, t, p, salt, len, password) |
| SHA-256 | FIPS 180-4 | Digest |
| BLAKE2b-256 | RFC 7693 | Digest |
| Kanonische CBOR-Kodierung | RFC 8949 §4.2.1 | kodierte Bytes für feste Eingabe |
| COSE_Sign1-Kodierung | RFC 9052 | Strukturbytes für festen Header, Payload, Signatur |
| Ed25519 signieren / verifizieren | RFC 8032 (strict) | Signatur; Urteil |
| X25519 ECDH | RFC 7748 | gemeinsames Geheimnis für feste Skalare |
| Versiegelte PoE wrap / unwrap | Versiegelte PoE | Bytes pro Slot und MAC, wenn Ephemerals und CEK injiziert werden |
| Merkle-Wurzel + Inklusionsbeweise | RFC 9162 §2.1.1 | Wurzel 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 Chunkuint88_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.EncapsulateMUSS 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.
encträgtslots(sowiekem,slots_mac). - Passphrase-Pfad: Der CEK wird direkt aus einer normalisierten Passphrase über
Argon2id abgeleitet.
encträgtpassphrase; es trägt keinkem, keinslotsund keinslots_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 stringBeide 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 BDrei 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. Dassscheme,path,aead,kemundnonceneben den Slots gepinnt sind, bedeutet: Ein Relay, das irgendein Header-Feld umstellt, selbst bei weiterhin gültigen Slot-Formen, ändertslots_hashund bricht den MAC. - Das Transkript bindet den Hash-Anspruch des Elements.
hashes_hashist ein markierter SHA-256 über diecanonicalEncode-Form der vollständigenhashes-Map des Elements. Da der Empfängerslots_macallein 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 anderenhashes-Map gespleißt wurde, scheitert am On-Chain-Abgleich, noch vor jedem Chiffretext-Abruf. Derslots-Wert ist das gemischte Array der On-Wire-Slot-Maps direkt: Jedes Slot-Feld ist ein einzelner Byte-String (epk32 B,kem_ct1120 B), sodass es keine Stückelung pro Feld zu kanonisieren gibt. slots_hashwird 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 BDie 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_keyDas 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:
- 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. - NFKC, Normalisierungsform KC nach UAX #15 unter Unicode 16.0.
- Whitespace, definiert als jedes Zeichen, das unter Unicode 16.0 die
Unicode-Eigenschaft
White_Spaceträgt; jede maximale Folge wird zu einem einzigen U+0020 SPACE zusammengezogen. - Trim, führende und nachfolgende Whitespace entfernen.
- 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. - 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_CIPHERTEXTDie 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 einencandidate_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 Siekem_ok = NOT constantTimeEqual(shared, 0^32)für denx25519-Pfad, wählen Sie den KEK in konstanter Zeit zwischen dem echten KEK und einem aus0^32unter demselben Salt undinfoabgeleiteten Dummy-KEK, und beziehen Siekem_okin 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.Decapsulatehat keinen Nullbyte-Fall, sodasskem_okauf 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 auchslots_macüberslots_hashreproduziert, 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_ctnicht 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 eincek_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_okund 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 diepub_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 reproduzierteslots_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 derif 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: Dersha2-256-Eintrag muss übereinstimmen, undblake2b-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_case—leaf_count,cose_sign1,slots_mac. Das gilt über alle Sprachen hinweg: Selbst dort, wo eine Sprache für ihre In-Memory-API idiomatischcamelCaseverwendet, nutzt der kodierte Datensatzsnake_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_cbor↔encodeCanonicalCbor), 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.
Sicherheitsmodell
Was ein Label-309-Verifizierer voraussetzt und was nicht: die Invariante der eigenständigen Überprüfbarkeit, die Datenschutzgarantien einer versiegelten PoE, die normativen Kryptografie-Regeln, die jede Implementierung einhalten muss, sowie die bekannten Grenzen des Wire-Formats.
Offene Fragen
Was im Wire-Format von Label 309 feststeht und was zurückgestellt oder zukunftsgerichtet ist: der bestätigte kryptografische Kern, die für eine künftige Revision vorgesehenen Kandidaten und das Migrationsmodell für die Änderung einer Konstante.