Signatures
L’array `sigs` optionnel au niveau de l’enregistrement — une COSE_Sign1 détachée sur l’intégralité du corps de l’enregistrement, sa charge utile signée avec séparation de domaine, les deux modes de transport de la clé du signataire et la vérification stricte d’Ed25519.
Un enregistrement Label 309 PEUT porter une ou plusieurs signatures de paternité dans
un array sigs optionnel de premier niveau. Chaque entrée est une
COSE_Sign1 (RFC 9052) détachée sur le corps
de l’enregistrement, qui atteste qu’une clé donnée se porte garante de
l’enregistrement. La paternité est toujours optionnelle : le standard n’exige jamais
de signature, et un enregistrement dépourvu du champ sigs constitue une preuve
d’existence (Proof of Existence, PoE) complète et pleinement vérifiable.
Une signature est additive : elle répond « et cette clé s’en porte garante » par-dessus l’affirmation d’horodatage, jamais à sa place. L’empreinte du contenu est l’affirmation principale ; une signature est une métadonnée sur l’identité de qui se tient derrière cette affirmation. Fait essentiel, une signature que le vérificateur ne peut pas contrôler — un algorithme non pris en charge, une clé impossible à résoudre — n’invalide jamais l’affirmation de contenu ni d’horodatage. Les signatures échouent en douceur ; l’existence, elle, ne faillit pas.
Cette page définit ce que couvre une signature, les octets exacts qui sont signés, les deux
manières dont la clé publique d’un signataire est transportée, et la vérification stricte
que réalise un vérificateur public. La clé Ed25519 elle-même est définie sur
Clés ; le champ sigs tel qu’il circule sur la chaîne — où cose_sign1 et
cose_key sont chacun une unique chaîne d’octets CBOR — est défini sur
L’enregistrement.
Ce que couvre une signature
Une seule entrée sigs[i] atteste l’intégralité du corps de l’enregistrement, de manière
uniforme. Il n’existe aucune granularité de signature par item, par URI ou par champ : une
signature s’engage sur chaque item, chaque URI de stockage, chaque enveloppe de chiffrement,
le pointeur supersedes s’il est présent, et chaque clé d’extension que l’enregistrement
porte. Un relais ne peut, après coup, ajouter, retirer ou réécrire aucun de ces éléments sans
briser la signature.
Le corps signé est la map de l’enregistrement dont le champ sigs a été retiré —
remove_keys(record_map, ["sigs"]), désigné ici par record_body. L’array sigs est exclu
de ce que signe chaque entrée parce qu’une signature ne peut pas se couvrir elle-même, et
parce que chaque signataire s’engage uniquement sur l’affirmation, et non sur la liste des
cosignataires. Concrètement, chaque entrée signe {v, items?, merkle?, supersedes?, crit?, <extensions?>} — les mêmes octets record_body pour chaque entrée — mais aucune entrée ne
signe les autres entrées de sigs. Un signataire atteste donc que le corps qu’il a signé est
le corps auquel chaque autre entrée est liée ; aucun signataire n’atteste quels autres
signataires ont cosigné.
La portée de la signature est le corps de l’enregistrement, pas la transaction
Une signature vérifiée prouve qu’une clé a produit une signature sur le corps de l’enregistrement. Elle ne prouve pas que cette même clé a soumis la transaction porteuse, en a payé les frais ou en a choisi l’heure du bloc. Un corps d’enregistrement identique PEUT être republié par n’importe quelle partie dans une transaction ultérieure — c’est une portabilité intentionnelle de l’enregistrement. Présentez une signature vérifiée comme « signé par <clé> », jamais comme « <clé> a soumis ceci » ni « publié par <clé> à <heure> ».
La charge utile signée
Chaque entrée porte une COSE_Sign1 détachée : le champ de charge utile de COSE est donc vide, et les octets effectivement signés sont reconstruits par le vérificateur à partir de l’enregistrement on-chain. Le signataire calcule :
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 est sérialisé en CBOR canonique conformément à la
RFC 8949 §4.2.1 — le même encodage
déterministe qu’emploie l’ensemble de l’enregistrement. C’est le déterminisme qui rend une
signature interopérable : deux implémentations qui encodent le même corps logique produisent
des record_body_bytes identiques octet pour octet, si bien qu’une signature produite par
l’une se vérifie avec l’autre.
Le préfixe de séparation de domaine
to_sign est la chaîne UTF-8 de 25 octets cardano-poe-record-sig-v1 préfixée à
record_body_bytes. Le préfixe lie la signature à son rôle dans Label 309 et empêche le rejeu
entre protocoles. Un futur schéma de métadonnées Cardano qui partagerait par hasard la forme
CBOR du corps (mêmes clés, mêmes types) ne pourrait pas réutiliser une signature Label 309
contre lui-même : son to_sign porterait un préfixe différent, ou aucun, de sorte que la
séquence d’octets signés différerait et que la signature échouerait. Les implémentations
DOIVENT intégrer cette séquence d’octets littérale exactement comme octets de tête de
to_sign ; signer uniquement le CBOR canonique nu, sans préfixe, n’est pas conforme.
Pourquoi external_aad est vide
Label 309 place le séparateur de domaine à l’intérieur de to_sign, et non dans
l’external_aad de COSE. L’emplacement external_aad (Sig_structure[2]) est toujours la
chaîne d’octets vide h''. Il s’agit d’un écart délibéré par rapport au schéma COSE habituel,
qui consiste à placer une chaîne de domaine dans external_aad, et la raison en est
l’interopérabilité avec les portefeuilles :
CIP-30
signData — le chemin standard de signature par portefeuille sur Cardano — stipule qu’aucun
external_aad n’est utilisé et n’offre à une dApp aucun moyen d’en fournir un. Un
external_aad non vide ferait échouer toute signature produite par un portefeuille. Intégrer
le préfixe dans la charge utile préserve la même propriété d’anti-rejeu tout en maintenant
identiques, octet pour octet, les octets produits par le portefeuille et ceux recalculés par
le vérificateur.
La Sig_structure
Sig_structure est l’array de signature COSE_Sign1 à 4 éléments défini en
RFC 9052 §4.4 :
| Emplacement | Valeur | Notes |
|---|---|---|
[0] | "Signature1" | Identifiant de contexte COSE fixe, émis comme chaîne de texte CBOR complète (11 octets), jamais comme UTF-8 brut. |
[1] | protected | Les octets d’en-tête protégé du signataire, encapsulés en bstr et en CBOR canonique, utilisés tels quels — jamais re-canonicalisés par le vérificateur. |
[2] | external_aad | Toujours h'' (bstr de longueur nulle). |
[3] | to_sign | Le préfixe de 25 octets concaténé avec record_body_bytes. |
La COSE_Sign1 publiée porte son champ de charge utile (COSE_Sign1[2]) sous la forme du CBOR
null (0xF6) — la forme détachée. Une charge utile attachée, y compris une chaîne
d’octets de longueur nulle, est rejetée. Détacher la charge utile est ce qui arrime les octets
signés au corps de l’enregistrement que le vérificateur recalcule de manière indépendante ;
une forme attachée permettrait à un producteur de signer des octets empruntés sans aucun
rapport avec les affirmations on-chain.
Mode haché des portefeuilles matériels
CIP-30 / CIP-8
définissent un drapeau optionnel "hashed": true dans l’en-tête non protégé, qu’un cosignataire
matériel aux ressources limitées peut positionner. Lorsqu’il est présent et vaut true,
Sig_structure[3] est le condensé Blake2b-224(to_sign) de 28 octets plutôt que to_sign
lui-même ; les trois autres emplacements restent inchangés. Un vérificateur DOIT inspecter
l’en-tête non protégé et effectuer cette substitution avant la vérification stricte d’Ed25519. Les
producteurs logiciels et SDK NE DEVRAIENT PAS le positionner — il n’économise aucun octet
on-wire et complique les chemins de code du vérificateur.
Algorithme de signature
Le seul algorithme de signature en v1 est EdDSA sur Ed25519
(RFC 8032), identifié par COSE
alg = -8 (RFC 9053 §2.2),
qui réside dans l’en-tête protégé de la COSE_Sign1. La base obligatoire d’un vérificateur v1
est {-8} ; il PEUT accepter en outre -19 (Ed25519, entièrement spécifié) et vérifier
les deux points de code sous la même primitive Ed25519. Le registre est extensible — les
révisions futures ajoutent des signatures post-quantiques de manière additive, jamais comme un
changement incompatible.
Résolution de la clé du signataire
Un vérificateur public doit résoudre la clé publique du signataire sans contacter aucun service ; chaque signature porte donc sa clé, ou une référence non ambiguë à celle-ci interne à la signature, on-chain. Il existe exactement deux modes de transport en v1, et ils sont mutuellement exclusifs au sein d’une même entrée — une entrée qui les utilise tous deux constitue une erreur structurelle.
Mode 1 — signature d’identité (kid interne à la signature)
La clé publique Ed25519 brute de 32 octets est placée à l’étiquette d’en-tête COSE 4 (kid,
RFC 9052 §3.1) à l’intérieur de l’en-tête
protégé de la COSE_Sign1. L’entrée ne porte aucun champ cose_key. Par convention de
Label 309, un kid d’en-tête protégé d’exactement 32 octets est la clé publique — et non
un pointeur opaque vers une clé à consulter hors bande. La longueur de 32 octets est un
discriminant non ambigu : les clés publiques Ed25519 font toujours 32 octets. Placer la clé
dans l’en-tête protégé (et non dans l’en-tête non protégé) la lie à la signature ; un
adversaire qui la réécrirait briserait la vérification.
Cette convention est un écart délibéré et documenté par rapport à la lecture de kid comme
identifiant opaque prévue par la RFC 9052 ; c’est ce qui rend le mode d’identité indépendant
de tout service, sans qu’aucun annuaire de clés ne soit requis. Le modèle de clés est défini
sur Clés.
Mode 2 — signature par portefeuille (cose_key inline)
Une signature signData de CIP-30 renvoie la clé publique du signataire sous la forme d’un
blob cbor<COSE_Key> distinct, et non à l’intérieur de la COSE_Sign1. Un producteur qui
chaîne une telle signature dans un enregistrement DOIT placer cette COSE_Key dans la
même entrée sigs[i] sous la clé cose_key, en tant qu’unique chaîne d’octets CBOR. Le
vérificateur la décode comme une COSE_Key et lit la clé publique Ed25519 à l’étiquette -2.
La COSE_Key DOIT décrire uniquement la moitié publique — kty = OKP (1),
crv = Ed25519 (6), les 32 octets de x à l’étiquette -2 — et NE DOIT PAS porter de
matériel de clé privée (étiquette -4 et assimilées) ; publier un scalaire privé sur un
registre permanent constitue une fuite de clé irréversible.
Exclusion mutuelle
Les deux modes sont exclusifs au niveau du wire. Une entrée porte soit un kid d’en-tête
protégé de 32 octets et aucun cose_key (mode 1), soit un champ cose_key et
aucun kid d’en-tête protégé de 32 octets (mode 2) — jamais les deux. Une entrée qui porte
les deux est rejetée ; un vérificateur n’a jamais à lever d’ambiguïté au moment de la
vérification. La résolution est donc une discrimination au niveau du wire, et non une
précédence ordonnée :
| Mode | Condition | Clé du signataire |
|---|---|---|
| 1 | kid protégé de 32 octets, sans cose_key | La valeur kid de 32 octets, utilisée directement. |
| 2 | cose_key présent, sans kid de 32 octets | La clé Ed25519 à l’étiquette -2 de la COSE_Key. |
Un kid transporté uniquement dans l’en-tête non protégé n’est pas un mode de
résolution sanctionné : il se trouve hors de l’enveloppe signée, de sorte qu’un relais pourrait
le réécrire sans briser la signature. Un vérificateur DOIT ignorer les valeurs kid de
l’en-tête non protégé aux fins de la résolution. Si aucun mode autorisé ne produit une clé
Ed25519 de 32 octets, l’entrée est signalée comme non résolue et n’apporte aucune affirmation
de paternité.
Vérification
Un vérificateur public contrôle chaque sigs[i] de manière indépendante, dans cet ordre :
- Décoder. Analyser la chaîne d’octets
sigs[i].cose_sign1comme une COSE_Sign1. Le champ de charge utile DOIT êtrenull(détaché) ; toute charge utile non nulle ou non vide est malformée. - Algorithme. Lire l’
algde l’en-tête protégé. S’il est hors de l’ensemble pris en charge par le vérificateur, l’entrée est non prise en charge (voir ci-dessous) — et non une erreur sur l’enregistrement. - Résoudre la clé. Appliquer la discrimination mode 1 / mode 2 ci-dessus pour obtenir la clé publique Ed25519 de 32 octets. Si aucun mode n’en produit une, l’entrée est non résolue.
- Reconstruire et vérifier. Reconstruire
to_signetSig_structure = ["Signature1", protected, h'', to_sign], l’encoder en CBOR canonique, et vérifier la signature avec Ed25519 strict. (Substituer d’abordBlake2b-224(to_sign)àto_signsi l’en-tête non protégé porte"hashed": true.) - Liaison au portefeuille (mode 2 uniquement). Recalculer l’adresse de stake à partir de
la clé résolue et la comparer octet pour octet à l’
addressde l’en-tête protégé ; une divergence fait échouer la liaison même si la signature Ed25519 elle-même a été vérifiée. Ce contrôle, propre au mode 2, est ce qui permet à une interface de présenter un enregistrement comme lié à un portefeuille ; les entrées de mode 1 l’omettent.
Ed25519 strict
La vérification suit les règles strictes de RFC 8032 §5.1.7 — il existe exactement une réponse acceptable pour toute combinaison donnée de clé, de message et de signature :
- Les encodages non canoniques de
Rou du scalaire de signatureS(en particulier toutS ≥ ℓ, l’ordre du groupe) DOIVENT être rejetés. - Les clés publiques et les valeurs
Rd’ordre petit, de sous-groupe petit ou comportant une composante de torsion DOIVENT être rejetées. - L’équation de vérification avec cofacteur (la forme ZIP-215 / adaptée à la vérification par lots) NE DOIT PAS être substituée à l’équation stricte.
C’est la stricte conformité qui rend le verdict reproductible d’une implémentation à l’autre : un vérificateur avec cofacteur accepterait des signatures qu’un vérificateur strict rejette, de sorte que deux vérificateurs conformes seraient en désaccord. Les implémentations doivent choisir une bibliothèque — ou un mode de bibliothèque — qui effectue une vérification stricte, sans cofacteur.
Sémantique du verdict
Les signatures sont additives : une signature non vérifiable est donc signalée sur l’entrée, et
non promue en un échec au niveau de l’enregistrement. Chaque sigs[i] se résout en l’un de ces
résultats typés par entrée ; le catalogue complet des erreurs et les règles du verdict au
niveau de l’enregistrement figurent sur Vérification :
| Résultat | Signification |
|---|---|
| vérifiée | Ed25519 strict (et, pour le mode 2, la liaison d’adresse) a réussi. |
| signature non prise en charge | L’alg de l’en-tête protégé est hors de l’ensemble du vérificateur. Information, jamais une erreur. |
| clé du signataire non résolue | Aucun mode autorisé ne produit de clé publique Ed25519 de 32 octets. |
| signature invalide | Ed25519 strict a renvoyé false sur la Sig_structure reconstruite. |
| adresse de portefeuille non concordante | Mode 2 : la signature s’est vérifiée, mais l’adresse de stake recalculée ≠ celle déclarée. |
Une signature non prise en charge n’invalide jamais la preuve
Un algorithme de signature non reconnu ou non pris en charge produit un résultat typé
signature-non-prise-en-charge au niveau de sévérité « information ». L’affirmation de contenu
et d’horodatage — l’engagement hashes on-chain — est structurellement valide quels que soient
les algorithmes de signature qu’un vérificateur implémente. Un enregistrement ne portant que des
signatures à algorithme futur se présente toujours comme une preuve d’existence valide, chacune de
ces entrées étant marquée comme non prise en charge. Les signatures sont additives ; l’existence
ne dépend pas d’elles.
Pages associées
- Clés — la clé de signature Ed25519, sa dérivation et la clé publique de
32 octets transportée dans le
kiddu mode 1. - L’enregistrement — le champ
sigsde premier niveau, la map ferméesig-entry(cose_sign1/cose_key, chacun une unique chaîne d’octets) et le transport sur l’intégralité du corps. - Vérification — les codes de résultat par entrée, les règles du verdict au niveau de l’enregistrement et l’intégralité de la chaîne de validation.
Clés
Le modèle de clés de Label 309 : un seed unique de 32 octets, trois paires de clés d’algorithme dérivées de celui-ci par HKDF-SHA-256 avec séparation de domaine, les clés de chiffrement de clé par emplacement qu’un enregistrement scellé dérive par-dessus, et la façon dont les clés publiques des destinataires et les secrets sont encodés.
PoE scellée
L’enveloppe de chiffrement de Label 309 : comment un expéditeur scelle un contenu vers une ou plusieurs clés de destinataire, tandis que la chaîne ne transporte que l’empreinte du texte en clair et les emplacements de clé enveloppés, jamais le texte en clair et jamais les destinataires.