]> git.kaiwu.me - njs.git/commitdiff
WebCrypto: return false for wrong-length ECDSA signatures
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 15 Oct 2026 06:06:08 +0000 (23:06 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 15 Oct 2026 14:42:10 +0000 (07:42 -0700)
ECDSA signatures of an invalid length cause verify() to throw instead
of returning false.  njs passes an empty DER signature to
EVP_PKEY_verify(), while QuickJS rejects the length during conversion.

The njs behavior dates back to 86c202c7 (0.7.7); QuickJS rejects these
signatures since 75ca26f8 (0.8.10).

Treat a length mismatch as an invalid signature, as required by
WebCrypto ECDSA Verify, while preserving diagnostics for conversion
failures.  Test all three supported curves with invalid lengths and
a valid signature.

external/njs_webcrypto_module.c
external/qjs_webcrypto_module.c
test/webcrypto/sign.t.mjs

index c1cbada13d1f90ed55282df510f801bce7122e5f..547917a4501af6746a3f83a9adf2669baaf1cf25 100644 (file)
@@ -4552,8 +4552,13 @@ njs_convert_p1363_to_der(njs_vm_t *vm, EVP_PKEY *pkey, u_char *p1363,
 
     n = njs_ec_rs_size(pkey);
 
-    if (njs_slow_path(n == 0 || p1363_len != 2 * n)) {
-        goto fail;
+    if (njs_slow_path(n == 0)) {
+        njs_webcrypto_error(vm, "cannot determine ECDSA signature length");
+        return NJS_ERROR;
+    }
+
+    if (p1363_len != 2 * n) {
+        return NJS_DECLINED;
     }
 
     ec_sig = ECDSA_SIG_new();
@@ -4887,6 +4892,11 @@ njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
                 ret = njs_convert_p1363_to_der(vm, key->u.a.pkey, sig.start,
                                                sig.length, &sig.start,
                                                &sig.length);
+                if (ret == NJS_DECLINED) {
+                    ret = 0;
+                    goto verify_done;
+                }
+
                 if (njs_slow_path(ret != NJS_OK)) {
                     goto fail;
                 }
@@ -4899,6 +4909,8 @@ njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
             }
         }
 
+    verify_done:
+
         njs_evp_md_ctx_free(mctx);
 
         EVP_PKEY_CTX_free(pctx);
index 13cd41f5926cc6bdbd5eee59a436265a6e9858bb..4a70b9426a2f8a286674df1087629f4ce747e4cb 100644 (file)
@@ -4529,12 +4529,15 @@ qjs_convert_p1363_to_der(JSContext *cx, EVP_PKEY *pkey, u_char *p1363,
 
     n = qjs_ec_rs_size(pkey);
 
-    if (n == 0 || p1363_len != 2 * n) {
-        JS_ThrowTypeError(cx, "invalid ECDSA signature length %zu != %u",
-                          p1363_len, 2 * n);
+    if (n == 0) {
+        qjs_webcrypto_error(cx, "cannot determine ECDSA signature length");
         return -1;
     }
 
+    if (p1363_len != 2 * n) {
+        return 1;
+    }
+
     ec_sig = ECDSA_SIG_new();
     if (ec_sig == NULL) {
         JS_ThrowOutOfMemory(cx);
@@ -4862,6 +4865,11 @@ qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, int argc,
                 rc = qjs_convert_p1363_to_der(cx, key->u.a.pkey, sig.start,
                                               sig.length, &sig.start,
                                               &sig.length);
+                if (rc > 0) {
+                    rc = 0;
+                    goto verify_done;
+                }
+
                 if (rc < 0) {
                     goto fail;
                 }
@@ -4878,6 +4886,8 @@ qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, int argc,
                 goto fail;
             }
 
+        verify_done:
+
             js_free(cx, dst);
         }
 
index 091f2989a173509a78f87c777b56f090370b7030..9dec51fd612e8d75206ad0014aa00c14be85202e 100644 (file)
@@ -398,11 +398,56 @@ let ecdsa_tsuite = {
           exception: "TypeError: Unsupported key usage for a ECDSA key" },
 ]};
 
+async function test_ecdsa_signature_length(params) {
+    let pair = await crypto.subtle.generateKey(
+        {name: "ECDSA", namedCurve: params.curve}, false, ["sign", "verify"]);
+    let options = {name: "ECDSA", hash: "SHA-256"};
+    let data = new Uint8Array([1, 2, 3]);
+    let signature = await crypto.subtle.sign(options, pair.privateKey, data);
+
+    if (signature.byteLength !== params.length) {
+        throw Error("unexpected ECDSA signature length");
+    }
+
+    let lengths = [0, 3, params.length - 1, params.length + 1];
+    for (let i = 0; i < lengths.length; i++) {
+        let length = lengths[i];
+        let invalid = new Uint8Array(length);
+        invalid.set(new Uint8Array(signature).subarray(0, length));
+
+        let verified = await crypto.subtle.verify(options, pair.publicKey,
+                                                  invalid, data);
+        if (verified !== false) {
+            throw Error(`ECDSA accepted a signature of length ${length}`);
+        }
+    }
+
+    let verified = await crypto.subtle.verify(options, pair.publicKey,
+                                              signature, data);
+    if (verified !== true) {
+        throw Error("ECDSA rejected a valid signature");
+    }
+
+    return "SUCCESS";
+}
+
+let ecdsa_signature_length_tsuite = {
+    name: "ECDSA signature length",
+    skip: () => !has_webcrypto(),
+    T: test_ecdsa_signature_length,
+    tests: [
+        {curve: "P-256", length: 64},
+        {curve: "P-384", length: 96},
+        {curve: "P-521", length: 132},
+    ],
+};
+
 run([
     hmac_tsuite,
     rsassa_pkcs1_v1_5_tsuite,
     rsa_pss_tsuite,
     ecdsa_tsuite,
+    ecdsa_signature_length_tsuite,
     verify_error_queue_tsuite
 ])
 .then($DONE, $DONE);