]> git.kaiwu.me - njs.git/commitdiff
WebCrypto: clear OpenSSL errors after a signature mismatch
authorDmitry Volyntsev <xeioex@nginx.com>
Wed, 14 Oct 2026 05:18:37 +0000 (22:18 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 15 Oct 2026 14:42:10 +0000 (07:42 -0700)
Previously, crypto.subtle.verify(false) could leave errors for an
unrelated TLS handshake or WebCrypto operation.

The fix is to clear the queue when returning false, while retaining
diagnostics for operation failures.

This fixes #1141.

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

index ce6e6fe38651209577cdd55b9b396bf983162503..c1cbada13d1f90ed55282df510f801bce7122e5f 100644 (file)
@@ -4914,6 +4914,11 @@ njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
         }
 
     } else {
+        if (ret == 0) {
+            /* A signature mismatch may leave errors in the OpenSSL queue. */
+            ERR_clear_error();
+        }
+
         njs_value_boolean_set(njs_value_arg(&result), ret != 0);
     }
 
index 8249a39e5b1a57d4301fe624aeea16d200000890..13cd41f5926cc6bdbd5eee59a436265a6e9858bb 100644 (file)
@@ -4895,6 +4895,11 @@ qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, int argc,
         }
 
     } else {
+        if (rc == 0) {
+            /* A signature mismatch may leave errors in the OpenSSL queue. */
+            ERR_clear_error();
+        }
+
         ret = JS_NewBool(cx, rc != 0);
     }
 
index 2999569f5806315b8acb8bb61ad981141e8395b4..091f2989a173509a78f87c777b56f090370b7030 100644 (file)
@@ -1,5 +1,5 @@
 /*---
-includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, runTsuite.js, webCryptoUtils.js]
+includes: [compatFs.js, compatBuffer.js, compatNjs.js, compatWebcrypto.js, runTsuite.js, webCryptoUtils.js]
 flags: [async]
 ---*/
 
@@ -153,6 +153,55 @@ function p(args, default_opts) {
     return params;
 }
 
+async function decrypt_error(key) {
+    try {
+        await crypto.subtle.decrypt(
+            {name: "AES-CBC", iv: new Uint8Array(16)}, key,
+            new Uint8Array(16));
+    } catch (e) {
+        return e.toString();
+    }
+
+    throw Error("decrypt accepted invalid padding");
+}
+
+async function test_verify_error_queue() {
+    let pair = await crypto.subtle.generateKey(
+        {name: "RSA-PSS", modulusLength: 2048, hash: "SHA-256",
+         publicExponent: new Uint8Array([1, 0, 1])},
+        false, ["sign", "verify"]);
+    let key = await crypto.subtle.importKey("raw", new Uint8Array(16),
+                                           "AES-CBC", false, ["decrypt"]);
+    let options = {name: "RSA-PSS", saltLength: 32};
+    let data = new Uint8Array([1, 2, 3]);
+    let signature = await crypto.subtle.sign(options, pair.privateKey, data);
+
+    /* Drain errors left by preceding tests before taking the baseline. */
+    await decrypt_error(key);
+    let expected = await decrypt_error(key);
+
+    data[0] ^= 1;
+    let verified = await crypto.subtle.verify(options, pair.publicKey,
+                                             signature, data);
+    if (verified !== false) {
+        throw Error("verify accepted an invalid signature");
+    }
+
+    let actual = await decrypt_error(key);
+    if (actual !== expected) {
+        throw Error(`verify polluted OpenSSL errors: ${actual} != ${expected}`);
+    }
+
+    return "SUCCESS";
+}
+
+let verify_error_queue_tsuite = {
+    name: "verify OpenSSL error queue",
+    skip: () => !has_njs() || !has_webcrypto(),
+    T: test_verify_error_queue,
+    tests: [{}],
+};
+
 let hmac_tsuite = {
     name: "HMAC sign",
     skip: () => (!has_buffer() || !has_webcrypto()),
@@ -353,6 +402,7 @@ run([
     hmac_tsuite,
     rsassa_pkcs1_v1_5_tsuite,
     rsa_pss_tsuite,
-    ecdsa_tsuite
+    ecdsa_tsuite,
+    verify_error_queue_tsuite
 ])
 .then($DONE, $DONE);