Aller au contenu
ecdsa.com
WebCrypto · Node.js

WebCrypto (crypto.subtle)

L’API Web Cryptography intégrée à tous les navigateurs ainsi qu’à Node.js, Deno et Bun : ECDSA sur les trois courbes NIST avec des signatures r‖s brutes, Ed25519 partout et, pour l’instant, des algorithmes post-quantiques dans certains moteurs seulement.
Licence
W3C Software and Document License (specification); each engine ships under its own license
Version vérifiée
Node.js 24.22.0, Chromium 156.0.8078.4, Firefox 157.0, WebKit 27.2 ·
Code source
github.com/w3c/webcrypto
Documentation
w3c.github.io/webcrypto/

Compatibilité en un coup d’œil

ECDSAEdDSA

Courbes ECDSA
P-256P-384P-521
EdDSA
Ed25519
Formats de clé publique
spki-derjwksec1-uncompressed

crypto.subtle est l’interface cryptographique standard de la plateforme web. Les mêmes appels (generateKey, importKey, sign, verify, exportKey) fonctionnent dans Chromium, Firefox et WebKit ainsi que dans les environnements d’exécution serveur comme Node.js, ce qui en fait le choix naturel pour du code qui doit tourner des deux côtés. Elle n’existe que dans les contextes sécurisés : sur une page en simple http, crypto.subtle vaut undefined.

La spécification impose le format r‖s brut pour les signatures ECDSA, soit 64 octets sur P-256, et la vérification a trouvé exactement cela dans les quatre moteurs. Ce format correspond à JWS et à JWT, si bien que les jetons ES256 n’ont besoin d’aucune conversion, mais pas à OpenSSL, au SHA256withECDSA de Java, au SignASN1 de Go ni à node:crypto, qui utilisent tous ASN.1 DER. Comme une signature DER passée à verify() renvoie un simple false plutôt qu’une erreur, l’incompatibilité ressemble à une falsification ; convertissez avec le convertisseur DER ⇄ brut ou testez les deux formes dans le vérificateur de signatures.

Les clés sont l’autre source fréquente de friction. importKey("spki") prend des octets DER, jamais du texte PEM ; "raw" prend le point SEC1, et la forme compressée est acceptée par Node.js, Chromium et Firefox mais refusée par WebKit : pour du code portable, envoyez donc le point non compressé. Les clés privées ne s’importent qu’en PKCS#8 DER ou en JWK ; les fichiers SEC1 EC PRIVATE KEY qu’OpenSSL écrit par défaut doivent d’abord être convertis en PKCS#8, par exemple avec le convertisseur JWK.

La liste des courbes s’arrête à P-256, P-384 et P-521 : secp256k1 et Brainpool lèvent NotSupportedError partout, si bien que les clés Bitcoin et Ethereum exigent une bibliothèque comme @noble/curves. Ed25519 fonctionne dans les quatre moteurs, Ed448 uniquement dans Node.js. La prise en charge post-quantique est inégale : Node.js 24.22 implémente ML-DSA et ML-KEM derrière un avertissement de fonctionnalité expérimentale, Chromium 156 accepte ML-DSA et ML-KEM-768/1024 mais pas ML-KEM-512, et Firefox 157 et WebKit 27.2 n’ont ni l’un ni l’autre. Détectez leur disponibilité avant de vous y fier, et essayez les algorithmes dans le bac à sable des signatures post-quantiques.

L’exemple montre l’approche portable : nommer la fonction de hachage dans l’objet algorithme, importer les clés PEM externes après leur décodage en DER, et ne pas oublier que la sortie de 64 octets doit être convertie en DER avant qu’un vérificateur de type OpenSSL puisse la lire.

Les faits et les vérifications qui les fondent

Rien de ce qui suit n’est copié d’une documentation. La vérification content/libraries/verify/webcrypto/run.sh a exécuté WebCrypto (crypto.subtle) Node.js 24.22.0, Chromium 156.0.8078.4, Firefox 157.0, WebKit 27.2 dans l’image de conteneur node:24.22.0-bookworm-slim le 20 octobre 2026 ; la page doit correspondre à ce que la vérification a affiché, et chaque fait cite les lignes en question.

  • Encodage des signatures ECDSA

    raw

    sign() a renvoyé exactement 64 octets de r‖s brut sur P-256 dans Node.js, Chromium, Firefox et WebKit ; la spécification ne définit aucune option DER.

    Relevé par la vérification
    fact ecdsa-signature-encoding = raw
  • Low-S à la signature

    either

    Les signatures ne sont pas normalisées : des valeurs high-S sont apparues parmi 64 signatures dans chaque moteur.

    Relevé par la vérification
    fact ecdsa-low-s-sign = either
  • High-S à la vérification

    accepts-high-s

    Chaque moteur a accepté une signature dont s avait été remplacé par n − s.

    Relevé par la vérification
    fact ecdsa-low-s-verify = accepts-high-s
  • Nonce ECDSA

    random

    Nonce aléatoire dans chaque moteur : signer deux fois les mêmes données donne des octets différents, et il n’existe aucune option déterministe.

    Relevé par la vérification
    fact ecdsa-nonce = random
  • Ce que l’appel de signature hache

    hashes-message

    sign() hache les données avec la fonction de hachage indiquée dans l’objet algorithme ; la signature de chaque moteur a été vérifiée sur le SHA-256 du message dans une implémentation indépendante. Il est impossible de signer un condensat précalculé.

    Relevé par la vérification
    fact ecdsa-prehash = hashes-message
  • Signature ECDSA mal formée

    returns-false

    Une signature DER, une signature de 63 octets et un blob de 3 octets ont tous fait renvoyer false à verify() dans les quatre moteurs ; aucune exception n’est levée.

    Relevé par la vérification
    fact ecdsa-malformed-signature = returns-false
    evidence ecdsa-malformed-signature: Node.js: DER false, 63 bytes false, 3 bytes false
    evidence ecdsa-malformed-signature: Chromium: DER false, 63 bytes false, 3 bytes false
    evidence ecdsa-malformed-signature: Firefox: DER false, 63 bytes false, 3 bytes false
    evidence ecdsa-malformed-signature: WebKit: DER false, 63 bytes false, 3 bytes false
  • Courbes ECDSA

    P-256P-384P-521

    P-256, P-384 et P-521 uniquement ; secp256k1, K-256 et brainpoolP256r1 ont levé NotSupportedError dans chaque moteur.

    Relevé par la vérification
    fact ecdsa-curves = P-256, P-384, P-521
    evidence ecdsa-curves: Node.js: P-256 true; P-384 true; P-521 true; secp256k1 NotSupportedError; K-256 NotSupportedError; brainpoolP256r1 NotSupportedError
    evidence ecdsa-curves: Chromium: P-256 true; P-384 true; P-521 true; secp256k1 NotSupportedError; K-256 NotSupportedError; brainpoolP256r1 NotSupportedError
    evidence ecdsa-curves: Firefox: P-256 true; P-384 true; P-521 true; secp256k1 NotSupportedError; K-256 NotSupportedError; brainpoolP256r1 NotSupportedError
    evidence ecdsa-curves: WebKit: P-256 true; P-384 true; P-521 true; secp256k1 NotSupportedError; K-256 NotSupportedError; brainpoolP256r1 NotSupportedError
  • EdDSA

    Ed25519

    Ed25519 fonctionne pour la signature et la vérification dans les quatre moteurs.

    Relevé par la vérification
    fact eddsa = Ed25519
  • Prise en charge d’Ed448 par moteur

    Node.js

    Ed448 n’a fonctionné que dans Node.js, qui le signale comme expérimental ; Chromium, Firefox et WebKit l’ont refusé.

    Relevé par la vérification
    fact ed448-engines = Node.js
  • Algorithmes post-quantiques

    none

    Aucun algorithme post-quantique ne fonctionne encore dans les quatre moteurs : du code portable ne peut donc s’appuyer sur aucun d’eux.

    Relevé par la vérification
    fact pqc = none
    evidence pqc: Node.js: ML-DSA-44 true; ML-DSA-65 true; ML-DSA-87 true; ML-KEM-512 true; ML-KEM-768 true; ML-KEM-1024 true
    evidence pqc: Chromium: ML-DSA-44 true; ML-DSA-65 true; ML-DSA-87 true; ML-KEM-512 NotSupportedError; ML-KEM-768 true; ML-KEM-1024 true
    evidence pqc: Firefox: ML-DSA-44 NotSupportedError; ML-DSA-65 NotSupportedError; ML-DSA-87 NotSupportedError; ML-KEM-512 NotSupportedError; ML-KEM-768 NotSupportedError; ML-KEM-1024 NotSupportedError
    evidence pqc: WebKit: ML-DSA-44 NotSupportedError; ML-DSA-65 NotSupportedError; ML-DSA-87 NotSupportedError; ML-KEM-512 TypeError; ML-KEM-768 TypeError; ML-KEM-1024 TypeError
  • Prise en charge de ML-DSA par moteur

    Node.jsChromium

    Signature et vérification réussies avec ML-DSA-44, ML-DSA-65 et ML-DSA-87 dans Node.js (expérimental) et Chromium 156 ; Firefox et WebKit ont levé NotSupportedError.

    Relevé par la vérification
    fact ml-dsa-engines = Node.js, Chromium
  • Prise en charge de ML-KEM par moteur

    Node.jsChromium

    encapsulateBits et decapsulateBits avec ML-KEM-768 ont fonctionné dans Node.js et Chromium 156 ; Chromium a refusé ML-KEM-512, et Firefox et WebKit ne prenaient en charge aucun ML-KEM.

    Relevé par la vérification
    fact ml-kem-engines = Node.js, Chromium
  • Formats de clé publique

    spki-derjwksec1-uncompressed

    Chaque moteur a importé du SPKI en octets DER, une JWK et le point non compressé SEC1 ("raw") ; du texte PEM et un x‖y de 64 octets sans le préfixe 0x04 ont levé DataError.

    Relevé par la vérification
    fact public-key-formats = spki-der, jwk, sec1-uncompressed
    evidence public-key-formats: Node.js: spki-pem DataError; spki-der true; jwk true; sec1-uncompressed true; sec1-compressed true; raw-xy DataError
    evidence public-key-formats: Chromium: spki-pem DataError; spki-der true; jwk true; sec1-uncompressed true; sec1-compressed true; raw-xy DataError
    evidence public-key-formats: Firefox: spki-pem DataError; spki-der true; jwk true; sec1-uncompressed true; sec1-compressed true; raw-xy DataError
    evidence public-key-formats: WebKit: spki-pem DataError; spki-der true; jwk true; sec1-uncompressed true; sec1-compressed DataError; raw-xy DataError
  • Points compressés par moteur

    Node.jsChromiumFirefox

    Un point compressé de 33 octets s’est importé avec "raw" dans Node.js, Chromium et Firefox, mais WebKit a levé DataError.

    Relevé par la vérification
    fact compressed-point-engines = Node.js, Chromium, Firefox
  • Formats de clé privée

    pkcs8-derjwk

    Les clés privées ne s’importent qu’en PKCS#8 DER ou en JWK ; un scalaire nu de 32 octets a été rejeté par tous les moteurs.

    Relevé par la vérification
    fact private-key-formats = pkcs8-der, jwk
    evidence private-key-formats: Node.js: pkcs8-der true; jwk true; raw-scalar SyntaxError
    evidence private-key-formats: Chromium: pkcs8-der true; jwk true; raw-scalar SyntaxError
    evidence private-key-formats: Firefox: pkcs8-der true; jwk true; raw-scalar DataError
    evidence private-key-formats: WebKit: pkcs8-der true; jwk true; raw-scalar SyntaxError

Utilisation

Signer avec P-256, importer une clé publique PEM par décodage en DER, puis vérifier : la signature est sous forme brute de 64 octets.
// The same code runs in browsers, Deno, Bun and Node.js (globalThis.crypto).
const { subtle } = globalThis.crypto;
const curve = { name: "ECDSA", namedCurve: "P-256" };
const ecdsa = { name: "ECDSA", hash: "SHA-256" };

const { privateKey, publicKey } = await subtle.generateKey(curve, true, ["sign", "verify"]);
const data = new TextEncoder().encode("order #1042 approved");
const signature = new Uint8Array(await subtle.sign(ecdsa, privateKey, data));

// A PEM public key from another system: importKey("spki") wants the DER bytes,
// so strip the armour and base64-decode first. PEM text itself is a DataError.
const spki = new Uint8Array(await subtle.exportKey("spki", publicKey));
const pem = `-----BEGIN PUBLIC KEY-----\n${btoa(String.fromCharCode(...spki))}\n-----END PUBLIC KEY-----`;
const der = Uint8Array.from(atob(pem.replace(/-----[^-]+-----|\s/g, "")), (c) => c.charCodeAt(0));
const imported = await subtle.importKey("spki", der, curve, false, ["verify"]);

// Always 64 bytes on P-256: raw r||s, never DER. Convert before handing it to OpenSSL or Java.
console.log("signature bytes:", signature.length);
console.log("verified:", await subtle.verify(ecdsa, imported, signature, data));

Le banc de test des extraits de code exécute chaque exemple tel quel dans l’image de conteneur node:24.22.0-bookworm-slim, et l’exécution échoue si la sortie ne correspond pas.

Commande
node usage.mjs
La sortie doit correspondre à cette expression régulière
signature bytes: 64\nverified: true

Bibliothèques