Ceci est une traduction à titre informatif. La version anglaise est normative et prévaut. Lire la version anglaise

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 :

EmplacementValeurNotes
[0]"Signature1"Identifiant de contexte COSE fixe, émis comme chaîne de texte CBOR complète (11 octets), jamais comme UTF-8 brut.
[1]protectedLes 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_aadToujours h'' (bstr de longueur nulle).
[3]to_signLe 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 :

ModeConditionClé du signataire
1kid protégé de 32 octets, sans cose_keyLa valeur kid de 32 octets, utilisée directement.
2cose_key présent, sans kid de 32 octetsLa 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 :

  1. Décoder. Analyser la chaîne d’octets sigs[i].cose_sign1 comme une COSE_Sign1. Le champ de charge utile DOIT être null (détaché) ; toute charge utile non nulle ou non vide est malformée.
  2. Algorithme. Lire l’alg de 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.
  3. 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.
  4. Reconstruire et vérifier. Reconstruire to_sign et Sig_structure = ["Signature1", protected, h'', to_sign], l’encoder en CBOR canonique, et vérifier la signature avec Ed25519 strict. (Substituer d’abord Blake2b-224(to_sign) à to_sign si l’en-tête non protégé porte "hashed": true.)
  5. Liaison au portefeuille (mode 2 uniquement). Recalculer l’adresse de stake à partir de la clé résolue et la comparer octet pour octet à l’address de 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 R ou du scalaire de signature S (en particulier tout S ≥ ℓ, l’ordre du groupe) DOIVENT être rejetés.
  • Les clés publiques et les valeurs R d’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ésultatSignification
vérifiéeEd25519 strict (et, pour le mode 2, la liaison d’adresse) a réussi.
signature non prise en chargeL’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ésolueAucun mode autorisé ne produit de clé publique Ed25519 de 32 octets.
signature invalideEd25519 strict a renvoyé false sur la Sig_structure reconstruite.
adresse de portefeuille non concordanteMode 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 kid du mode 1.
  • L’enregistrement — le champ sigs de premier niveau, la map fermée sig-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.