node:crypto ist das Kryptografiemodul, das mit jedem Node.js-Release ausgeliefert wird. Die One-Shot-Funktionen crypto.sign() und crypto.verify() und die Klasse KeyObject decken ECDSA, Ed25519, Ed448, RSA und – Stand Node.js 24.22 – ML-DSA, SLH-DSA und ML-KEM ab. Alles läuft auf dem OpenSSL, das Node.js mitbringt (3.5.8 in Node.js 24.22.0), daher entspricht das Verhalten meist dem der Befehlszeile von OpenSSL – und unterscheidet sich von der WebCrypto-API, die derselbe Prozess als crypto.subtle bereitstellt.
Der Interop-Fehler, auf den du am ehesten stößt, betrifft die Kodierung der Signatur. crypto.sign() liefert ASN.1-DER, während WebCrypto, JWS/JWT, .NET und die meisten Hardware-Wallets die rohe r‖s-Form mit fester Breite verwenden. Eine Signatur in der jeweils anderen Kodierung löst keine Ausnahme aus: crypto.verify() gibt einfach false zurück, was genauso aussieht wie eine gefälschte Signatur. Setze dsaEncoding: "ieee-p1363" im Schlüsselobjekt auf der signierenden und auf der prüfenden Seite oder konvertiere die Bytes mit dem Konverter DER ⇄ Raw.
Zwei weniger auffällige Standardeinstellungen spielen eine Rolle, wenn die Gegenseite streng ist. Signaturen werden nie auf Low-S normalisiert, daher lehnen Verifizierer, die darauf bestehen, etwa libsecp256k1 und @noble/curves 2.x, ungefähr die Hälfte davon ab. Außerdem ist die Nonce zufällig, sodass derselbe Schlüssel und dieselbe Nachricht bei jedem Aufruf andere Bytes ergeben – vergleiche in Tests die Ergebnisse der Prüfung, nie die Bytes der Signatur. Wiederholt ein Signierer jemals eine Nonce, stellt der Nonce-Wiederverwendungs-Checker den Schlüssel wieder her; deshalb braucht eine zufällige Nonce eine intakte Zufallsquelle.
Übergibst du null als Algorithmus, signiert ein EC-Schlüssel stillschweigend mit SHA-256, unabhängig von seiner Kurve; ein P-384-Schlüssel scheitert dann bei jeder Gegenstelle, die SHA-384 erwartet. Nenne den Hash in jedem Aufruf, lade Schlüssel einmal mit crypto.createPublicKey() oder crypto.createPrivateKey() – sie lesen PEM, DER, JWK und sogar den bloßen SEC1-Punkt – und behalte das resultierende KeyObject, statt Strings erneut zu parsen.
Um eine anderswo erzeugte Signatur zu prüfen, füge sie mit dem Schlüssel in den Signatur-Verifizierer ein; welche Standardeinstellungen mit einem anderen Stack kollidieren, siehst du, wenn du im Interop-Cheatsheet für Bibliotheken beide Seiten auswählst.