node:crypto est le module cryptographique livré avec chaque version de Node.js. Ses fonctions en un seul appel crypto.sign() et crypto.verify() et la classe KeyObject couvrent ECDSA, Ed25519, Ed448, RSA et, depuis Node.js 24.22, ML-DSA, SLH-DSA et ML-KEM. Tout repose sur l’OpenSSL que Node.js embarque (3.5.8 dans Node.js 24.22.0) : la plupart des comportements reproduisent donc ceux de la ligne de commande OpenSSL, et diffèrent de l’API WebCrypto que le même processus expose sous crypto.subtle.
Le bug d’interopérabilité que vous rencontrerez le plus probablement concerne l’encodage de la signature. crypto.sign() renvoie de l’ASN.1 DER, alors que WebCrypto, JWS/JWT, .NET et la plupart des portefeuilles matériels utilisent la forme r‖s brute de largeur fixe. Une signature dans l’autre encodage ne lève pas d’exception : crypto.verify() renvoie simplement false, ce qui ressemble en tout point à une signature falsifiée. Définissez dsaEncoding: "ieee-p1363" dans l’objet clé, côté signature comme côté vérification, ou convertissez les octets avec le convertisseur DER ⇄ brut.
Deux comportements par défaut plus discrets comptent quand l’autre côté est strict. Les signatures ne sont jamais normalisées en low-S, si bien qu’environ la moitié d’entre elles sont rejetées par les vérificateurs qui l’exigent, comme libsecp256k1 et @noble/curves 2.x. De plus, le nonce est aléatoire : la même clé et le même message donnent des octets différents à chaque appel. Dans les tests, comparez donc les résultats de vérification, jamais les octets des signatures. Si un signataire répète un jour un nonce, le vérificateur de réutilisation de nonce retrouve la clé ; c’est pourquoi un nonce aléatoire exige une source d’aléa saine.
Si vous passez null comme algorithme, la signature avec une clé EC utilise discrètement SHA-256, quelle que soit sa courbe ; une clé P-384 échoue alors face à tout pair qui attend SHA-384. Indiquez la fonction de hachage à chaque appel, chargez les clés une seule fois avec crypto.createPublicKey() ou crypto.createPrivateKey() (elles lisent PEM, DER, JWK et même le point SEC1 nu) et conservez le KeyObject obtenu au lieu de réanalyser des chaînes.
Pour contrôler une signature produite ailleurs, collez-la avec la clé dans le vérificateur de signatures ; pour voir quels comportements par défaut entrent en conflit avec une autre pile, choisissez les deux côtés dans la matrice d’interopérabilité des bibliothèques.