Signaturen
Das optionale `sigs`-Array auf Datensatzebene, ein abgetrenntes COSE_Sign1 über den gesamten Datensatzkörper, sein domänengetrennter signierter Inhalt, die beiden Wege zur Übermittlung des Signaturschlüssels und die strenge Ed25519-Verifizierung.
Ein Label 309-Datensatz KANN eine oder mehrere Signaturen zur Urheberschaft in einem optionalen
sigs-Array auf oberster Ebene führen. Jeder Eintrag ist ein abgetrenntes
COSE_Sign1 (RFC 9052) über den Datensatzkörper und belegt,
dass ein bestimmter Schlüssel für den Datensatz einsteht. Die Urheberschaft ist stets
optional: Der Standard verlangt nie eine Signatur, und ein Datensatz ohne
sigs-Feld ist ein vollständiger, uneingeschränkt verifizierbarer Existenznachweis (engl. Proof of Existence, PoE).
Eine Signatur kommt hinzu, sie ersetzt nichts: Sie beantwortet zusätzlich zur Zeitstempelaussage die Frage „und dieser Schlüssel steht dafür ein", niemals an deren Stelle. Der Inhalts-Hash ist der primäre Anspruch; eine Signatur ist ein Metadatum darüber, wer hinter diesem Anspruch steht. Entscheidend dabei ist, dass eine Signatur, die die prüfende Stelle nicht überprüfen kann, etwa wegen eines nicht unterstützten Algorithmus oder eines nicht auflösbaren Schlüssels, den Inhalts- oder Zeitstempelanspruch niemals ungültig macht. Signaturen scheitern sanft; die Existenz nicht.
Diese Seite legt fest, was eine Signatur abdeckt, welche Bytes genau signiert werden,
auf welchen beiden Wegen der öffentliche Schlüssel (Public Key) des Signierenden übermittelt wird und welche
strenge Verifizierung ein öffentlicher Verifizierer durchführt. Der Ed25519-Schlüssel selbst ist auf
Schlüssel definiert; das sigs-Feld im Übertragungsformat – in dem cose_sign1
und cose_key jeweils ein einzelner CBOR-Byte-String sind – ist auf
Der Datensatz beschrieben.
Was eine Signatur abdeckt
Ein einzelner sigs[i]-Eintrag bezeugt den gesamten Datensatzkörper, einheitlich. Es gibt keine
Signaturgranularität auf Ebene einzelner Items, URIs oder Felder: Eine Signatur bindet sich
an jedes Item, jede Speicher-URI, jeden Verschlüsselungsumschlag, den supersedes-Verweis,
sofern vorhanden, sowie jeden Erweiterungsschlüssel, den der Datensatz trägt. Ein Relay kann nach
der Signierung keines dieser Felder hinzufügen, entfernen oder umschreiben, ohne die Signatur zu
zerstören.
Der signierte Körper ist die Datensatz-Map mit entferntem sigs-Feld,
also remove_keys(record_map, ["sigs"]), hier als record_body bezeichnet. Das sigs-Array ist
ausgeschlossen, weil eine Signatur sich nicht selbst abdecken kann und weil sich jeder Signierende
nur an den Anspruch bindet, nicht an die Liste der Mitunterzeichner. Konkret signiert jeder
Eintrag {v, items?, merkle?, supersedes?, crit?, <extensions?>}, also dieselben record_body-Bytes
für jeden Eintrag, doch keiner signiert die anderen Einträge in sigs. Ein Signierender bestätigt
damit, dass der von ihm signierte Körper derselbe ist, an den alle anderen Einträge gebunden sind;
kein Signierender bestätigt, welche anderen Signierenden mitunterzeichnet haben.
Der Umfang der Signatur ist der Datensatzkörper, nicht die Transaktion
Eine verifizierte Signatur beweist, dass ein Schlüssel eine Signatur über den Datensatzkörper erstellt hat. Sie beweist nicht, dass derselbe Schlüssel die tragende Transaktion eingereicht, ihre Gebühr bezahlt oder ihre Blockzeit gewählt hat. Derselbe Datensatzkörper KANN von jeder Partei in einer späteren Transaktion erneut veröffentlicht werden, das ist die beabsichtigte Portabilität von Datensätzen. Eine verifizierte Signatur ist als „signiert von <Schlüssel>" darzustellen, niemals als „<Schlüssel> hat dies eingereicht" oder „veröffentlicht von <Schlüssel> zum Zeitpunkt <Zeit>".
Der signierte Inhalt
Jeder Eintrag trägt ein abgetrenntes COSE_Sign1, sodass das COSE-Payload-Feld leer ist und die tatsächlich signierten Bytes vom Verifizierer aus dem on-chain (in der Blockchain) liegenden Datensatz rekonstruiert werden. Der Signierende berechnet:
record_body = remove_keys(record_map, ["sigs"])
record_body_bytes = canonical_cbor(record_body)
SIG_DOMAIN_RECORD = utf8("cardano-poe-record-sig-v1") ; 25 bytes
to_sign = SIG_DOMAIN_RECORD || record_body_bytes ; concatenation
Sig_structure = [ "Signature1", protected, h'', to_sign ]
signature = Sign(canonical_cbor(Sig_structure), signer_key)record_body wird als kanonisches CBOR gemäß
RFC 8949 §4.2.1 serialisiert, mit derselben
deterministischen Kodierung, die der gesamte Datensatz verwendet. Erst der Determinismus macht eine
Signatur interoperabel: Zwei Implementierungen, die denselben logischen Körper kodieren,
erzeugen byteidentische record_body_bytes, sodass eine Signatur, die von einer Implementierung
erstellt wurde, unter einer anderen verifiziert werden kann.
Das Präfix zur Domänentrennung
to_sign ist der 25 Byte lange UTF-8-String cardano-poe-record-sig-v1, dem record_body_bytes
vorangestellt wird. Das Präfix bindet die Signatur an ihre Label 309-Rolle und verhindert
protokollübergreifende Replay-Angriffe. Ein künftiges Cardano-Metadatenschema, das zufällig
dieselbe CBOR-Form des Körpers aufwiese (gleiche Schlüssel, gleiche Typen), könnte eine
Label 309-Signatur nicht gegen sich selbst wiederverwenden: Sein to_sign trüge ein anderes Präfix
oder gar keines, sodass die signierte Bytefolge abwiche und die Signatur scheiterte.
Implementierungen MÜSSEN diese exakte Bytefolge als führende Bytes von to_sign einbetten;
das bloße kanonische CBOR ohne Präfix zu signieren, ist nicht standardkonform.
Warum external_aad leer ist
Label 309 platziert den Domänentrenner innerhalb von to_sign, nicht in COSE
external_aad. Der external_aad-Slot (Sig_structure[2]) ist stets der leere Byte-String
h''. Dies ist eine bewusste Abweichung vom üblichen COSE-Muster, einen Domänen-String in
external_aad zu legen, und der Grund dafür ist die Interoperabilität mit Wallets:
CIP-30
signData, der Standardweg für die Wallet-Signatur auf Cardano, legt fest, dass kein
external_aad verwendet wird, und gibt einer dApp keine Möglichkeit, eines beizusteuern. Ein
nicht leeres external_aad würde jede von einer Wallet erzeugte Signatur scheitern lassen. Das Einbetten
des Präfix in die Nutzdaten bewahrt dieselbe Replay-Schutzeigenschaft und hält dabei
die von der Wallet erzeugten und die von der prüfenden Stelle neu berechneten Bytes Byte für Byte gleich.
Die Sig_structure
Sig_structure ist das 4-elementige COSE_Sign1-Signier-Array aus
RFC 9052 §4.4:
| Slot | Wert | Hinweise |
|---|---|---|
[0] | "Signature1" | Fester COSE-Kontextbezeichner, als vollständiger CBOR-Textstring (11 Bytes) ausgegeben, nie als bloßes UTF-8. |
[1] | protected | Die bstr-verpackten, kanonisch CBOR-kodierten Protected-Header-Bytes des Signierenden, unverändert verwendet, vom Verifizierer nie neu kanonisiert. |
[2] | external_aad | Stets h'' (Länge null, bstr). |
[3] | to_sign | Das 25 Byte lange Präfix, verkettet mit record_body_bytes. |
Der veröffentlichte COSE_Sign1 trägt sein Payload-Feld (COSE_Sign1[2]) als CBOR
null (0xF6), also in der abgetrennten Form. Ein angehängtes Payload, einschließlich eines
null Byte langen Byte-Strings, wird abgelehnt. Das Abtrennen der Nutzdaten bindet die signierten Bytes an
den Datensatzkörper, den der Verifizierer unabhängig rekonstruiert; eine angehängte Form ließe es
einem Produzenten zu, geliehene Bytes zu signieren, die keinen Bezug zu den on-chain liegenden Aussagen
haben.
Gehashter Modus für Hardware-Wallets
CIP-30 / CIP-8
definieren ein optionales "hashed": true-Flag im Unprotected Header, das ein in seinen
Ressourcen eingeschränkter Hardware-Mitunterzeichner setzen kann. Ist es vorhanden und true, ist
Sig_structure[3] der 28 Byte lange Digest Blake2b-224(to_sign) anstelle von to_sign selbst;
die anderen drei Slots bleiben unverändert. Ein Verifizierer MUSS den Unprotected Header
prüfen und diese Ersetzung vor der strengen Ed25519-Verifizierung vornehmen. Software- und SDK-
Produzenten SOLLTEN es nicht setzen, es spart keine Bytes im Übertragungsformat und
verkompliziert die Verifizierer-Codepfade.
Signaturalgorithmus
Der einzige Signaturalgorithmus in v1 ist EdDSA über Ed25519
(RFC 8032), identifiziert durch COSE
alg = -8 (RFC 9053 §2.2),
der im Protected Header des COSE_Sign1 steht. Die verpflichtende Mindestunterstützung eines v1-Verifizierers ist
{-8}; er KANN zusätzlich -19 (Ed25519, vollständig spezifiziert) akzeptieren und beide
Codepunkte unter demselben Ed25519-Primitiv verifizieren. Die Registry ist erweiterbar: Künftige
Revisionen fügen Post-Quanten-Signaturen additiv hinzu, niemals als Bruch der Abwärtskompatibilität.
Auflösung des Signaturschlüssels
Ein öffentlicher Verifizierer muss den öffentlichen Schlüssel des Signierenden ohne Kontaktaufnahme mit einem Dienst auflösen, daher trägt jede Signatur ihren Schlüssel oder einen eindeutigen, in der Signatur liegenden Verweis darauf on-chain. Es gibt in v1 genau zwei Übertragungsformen, und sie schließen sich innerhalb eines einzelnen Eintrags gegenseitig aus: Ein Eintrag, der beide verwendet, ist ein struktureller Fehler.
Pfad 1: Identitätssignatur (kid in der Signatur)
Der 32 Byte lange rohe öffentliche Ed25519-Schlüssel wird am COSE-Header-Label 4 (kid,
RFC 9052 §3.1) innerhalb des
Protected Headers des COSE_Sign1 platziert. Der Eintrag trägt kein cose_key-Feld. Gemäß der
Label 309-Konvention ist ein Protected-Header-kid von genau 32 Bytes der öffentliche Schlüssel
selbst, kein undurchsichtiger Verweis auf einen außerhalb des Bandes nachgeschlagenen Schlüssel. Die Länge von 32 Bytes ist ein
eindeutiges Unterscheidungsmerkmal: Öffentliche Ed25519-Schlüssel sind immer 32 Bytes lang.
Die Platzierung des Schlüssels im Protected Header (nicht im Unprotected Header) bindet ihn an die
Signatur; wer ihn umschriebe, zerstörte damit die Verifizierung.
Diese Konvention ist eine bewusste, dokumentierte Abweichung von der Lesart des kid als
undurchsichtiger Bezeichner in RFC 9052; sie macht den Identitätspfad dienstunabhängig, ohne dass ein Schlüsselverzeichnis
erforderlich ist. Das Schlüsselmodell ist auf Schlüssel definiert.
Pfad 2: Wallet-Signatur (Inline-cose_key)
Eine CIP-30-signData-Signatur gibt den öffentlichen Schlüssel des Signierenden als separaten
cbor<COSE_Key>-Blob zurück, nicht innerhalb des COSE_Sign1. Ein Produzent, der eine solche Signatur
in einen Datensatz einbindet, MUSS diesen COSE_Key in denselben
sigs[i]-Eintrag unter dem Schlüssel cose_key legen, als einzelnen CBOR-Byte-String. Der
Verifizierer dekodiert ihn als COSE_Key und liest den öffentlichen Ed25519-Schlüssel aus Label -2. Der COSE_Key MUSS ausschließlich
die öffentliche Hälfte beschreiben, also kty = OKP (1), crv = Ed25519 (6) und den 32 Byte langen Wert x an
Label -2, und DARF KEIN privates Schlüsselmaterial enthalten (Label -4 und Ähnliches);
einen privaten Skalar auf einem permanenten Ledger zu veröffentlichen, ist ein unwiderrufliches Durchsickern eines Schlüssels.
Gegenseitiger Ausschluss
Die beiden Pfade schließen sich auf Übertragungsebene aus. Ein Eintrag trägt entweder einen
32 Byte langen Protected-Header-kid und kein cose_key (Pfad 1), oder ein cose_key-Feld und
keinen 32 Byte langen Protected-Header-kid (Pfad 2), niemals beides.
Ein Eintrag, der beides trägt, wird abgelehnt; ein Verifizierer muss bei der Verifizierung nie
disambiguieren. Die Auflösung ist daher eine Unterscheidung auf Übertragungsebene, keine priorisierte
Rangfolge:
| Pfad | Bedingung | Signaturschlüssel |
|---|---|---|
| 1 | 32 Byte langer Protected-kid, kein cose_key | Der 32 Byte lange kid-Wert, direkt verwendet. |
| 2 | cose_key vorhanden, kein 32 Byte langer kid | Der Ed25519-Schlüssel an COSE_Key-Label -2. |
Ein kid, der nur im Unprotected Header geführt wird, ist kein zulässiger
Auflösungspfad: Er liegt außerhalb des signierten Umschlags, sodass ein Relay ihn umschreiben könnte,
ohne die Signatur zu zerstören. Ein Verifizierer MUSS kid-Werte aus dem Unprotected Header für die
Auflösung ignorieren. Ergibt kein zulässiger Pfad einen 32 Byte langen Ed25519-Schlüssel, wird der Eintrag
als nicht aufgelöst gemeldet und trägt keinen Urheberschaftsnachweis bei.
Verifizierung
Ein öffentlicher Verifizierer prüft jeden sigs[i]-Eintrag unabhängig, in dieser Reihenfolge:
- Dekodieren. Den Byte-String
sigs[i].cose_sign1als COSE_Sign1 parsen. Das Payload-Feld MUSSnullsein (abgetrennt); jedes nicht-null- oder nicht-leere Payload ist fehlerhaft. - Algorithmus. Den Protected-Header-
alglesen. Liegt er außerhalb des vom Verifizierer unterstützten Satzes, ist der Eintrag nicht unterstützt (siehe unten), kein Fehler am Datensatz. - Schlüssel auflösen. Die obige Unterscheidung zwischen Pfad 1 und Pfad 2 anwenden, um den 32 Byte langen öffentlichen Ed25519-Schlüssel zu ermitteln. Ergibt kein Pfad einen solchen, ist der Eintrag nicht aufgelöst.
- Rekonstruieren und verifizieren.
to_signundSig_structure = ["Signature1", protected, h'', to_sign]neu aufbauen, kanonisch CBOR-kodieren und die Signatur mit strengem Ed25519 verifizieren. (Dabei zuvorBlake2b-224(to_sign)fürto_signeinsetzen, wenn der Unprotected Header"hashed": trueträgt.) - Wallet-Bindung (nur Pfad 2). Die Stake-Adresse aus dem aufgelösten Schlüssel neu berechnen
und Byte für Byte gegen die Protected-Header-
addressvergleichen; eine Abweichung lässt die Bindung scheitern, auch wenn die Ed25519-Signatur selbst verifiziert wurde. Erst diese nur in Pfad 2 vorkommende Prüfung ermöglicht es einer Oberfläche, einen Datensatz als wallet-gebunden darzustellen; Pfad-1-Einträge überspringen sie.
Strenges Ed25519
Die Verifizierung folgt den strengen Regeln aus RFC 8032 §5.1.7: Für ein gegebenes Schlüssel-, Nachrichten- und Signatur-Tripel gibt es genau eine zulässige Antwort:
- Nicht-kanonische Kodierungen von
Roder dem Signatur-SkalarS(insbesondere jedesS ≥ ℓ, die Gruppenordnung) MÜSSEN abgelehnt werden. - Öffentliche Schlüssel und
R-Werte mit kleiner Ordnung, in einer kleinen Untergruppe oder mit Torsionskomponente MÜSSEN abgelehnt werden. - Die kofaktorisierte Verifikationsgleichung (die ZIP-215-/Batch-freundliche Form) DARF NICHT an die Stelle der strengen Gleichung treten.
Erst die Strenge macht das Ergebnis über Implementierungen hinweg reproduzierbar: Ein kofaktorisierter Verifizierer akzeptierte Signaturen, die ein strenger ablehnt, sodass zwei konforme Verifizierer uneins wären. Implementierungen müssen eine Bibliothek, oder einen Bibliotheksmodus, wählen, die bzw. der eine strenge, nicht kofaktorisierte Verifizierung durchführt.
Semantik der Ergebnisse
Signaturen kommen hinzu, sodass eine nicht verifizierbare Signatur am Eintrag gemeldet und
nicht zu einem Fehler auf Datensatzebene erhoben wird. Jedes sigs[i] führt zu einem dieser typisierten
Ergebnisse pro Eintrag; der vollständige Fehlerkatalog und die Regeln für das Ergebnis auf
Datensatzebene sind auf Verifizierung beschrieben:
| Ergebnis | Bedeutung |
|---|---|
| verified | Strenges Ed25519 (und, bei Pfad 2, die Adressbindung) hat bestanden. |
| signature unsupported | Der Protected-Header-alg liegt außerhalb des Verifizierer-Satzes. Info, kein Fehler. |
| signer key unresolved | Kein zulässiger Pfad ergibt einen 32 Byte langen öffentlichen Ed25519-Schlüssel. |
| signature invalid | Strenges Ed25519 hat false über die rekonstruierte Sig_structure zurückgegeben. |
| wallet address mismatch | Pfad 2: Die Signatur wurde verifiziert, aber die neu berechnete Stake-Adresse ≠ die angegebene. |
Eine nicht unterstützte Signatur macht den Nachweis niemals ungültig
Ein unbekannter oder nicht unterstützter Signaturalgorithmus ergibt ein typisiertes
signature-unsupported-Ergebnis mit der Schwere „Info". Der Inhalts- und Zeitstempelanspruch,
also die on-chain liegende hashes-Bindung, ist unabhängig davon strukturell gültig, welche
Signaturalgorithmen ein Verifizierer implementiert. Ein Datensatz, der ausschließlich Signaturen
künftiger Algorithmen trägt, wird dennoch als gültiger Existenznachweis ausgewiesen, wobei jeder
solche Eintrag als nicht unterstützt gekennzeichnet ist. Signaturen kommen hinzu; die Existenz
hängt nicht von ihnen ab.
Verwandte Seiten
- Schlüssel: der Ed25519-Signierschlüssel, seine Ableitung und der
32 Byte lange öffentliche Schlüssel im
kidvon Pfad 1. - Der Datensatz: das
sigs-Feld auf oberster Ebene, die geschlossenesig-entry-Map (cose_sign1/cose_keyjeweils ein einzelner Byte-String) und der Transport über den gesamten Körper. - Verifizierung: die Ergebniscodes pro Eintrag, die Regeln für das Ergebnis auf Datensatzebene und die vollständige Validierungspipeline.
Schlüssel
Das Schlüsselmodell von Label 309. Ein 32-Byte-Seed, daraus drei Algorithmus-Schlüsselpaare, abgeleitet per domänengetrennter HKDF-SHA-256, die Slot-Schlüsselverschlüsselungsschlüssel, die ein versiegelter PoE darauf aufsetzt, sowie die Kodierung der öffentlichen Empfängerschlüssel und Geheimnisse.
Versiegelte PoE
Der Verschlüsselungsumschlag von Label 309 — wie ein Absender Inhalte für einen oder mehrere Empfängerschlüssel versiegelt, während die Blockchain nur den Klartext-Hash und die gewickelten Schlüssel-Slots trägt, niemals den Klartext und niemals die Empfänger.