]> git.kaiwu.me - njs.git/commitdiff
WebCrypto: fix algorithm availability in nginx modules
authorDmitry Volyntsev <xeioex@nginx.com>
Tue, 13 Oct 2026 05:37:30 +0000 (22:37 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 15 Oct 2026 14:42:10 +0000 (07:42 -0700)
Previously, configure probes disabled Ed25519, X25519, and AES-KW in
nginx modules because the engine is configured with --no-openssl,
although WebCrypto uses nginx's OpenSSL and these algorithms were
available.

The fix is to detect 25519 APIs from OpenSSL headers and look up AES-KW
ciphers at runtime, removing the dependency on engine configuration
and direct references to wrap functions absent in some OpenSSL flavors.

This fixes #1139.

12 files changed:
auto/openssl
external/njs_openssl.h
external/njs_webcrypto_module.c
external/qjs_webcrypto_module.c
nginx/t/js_webcrypto.t
nginx/t/stream_js_webcrypto.t
test/harness/compatWebcrypto.js
test/harness/runTsuite.js
test/webcrypto/aes_kw.t.mjs
test/webcrypto/ed25519.t.mjs
test/webcrypto/wrap.t.mjs
test/webcrypto/x25519.t.mjs

index b5f65a7048e757a11730ba722c0c1af92a9d7aa2..0dbaf5c1479465084fc5b1b7326e30e1f9f421ac 100644 (file)
@@ -33,33 +33,6 @@ if [ $NJS_OPENSSL = YES ]; then
 
 
     if [ $njs_found = yes ]; then
-        njs_feature="EVP_aes_128_wrap()"
-        njs_feature_name=NJS_HAVE_AES_WRAP
-        njs_feature_run=
-        njs_feature_test="#include <openssl/evp.h>
-
-                          int main() {
-                              (void) EVP_aes_128_wrap();
-                              return 0;
-                         }"
-        . auto/feature
-
-        # Ed25519 and X25519 share the same Curve25519 implementation and
-        # were introduced together in OpenSSL 1.1.1, LibreSSL 3.7.0,
-        # BoringSSL, and AWS-LC.  No known build configuration enables
-        # one without the other, so a single probe is sufficient.
-
-        njs_feature="EVP_PKEY_ED25519"
-        njs_feature_name=NJS_HAVE_ED25519
-        njs_feature_run=
-        njs_feature_test="#include <openssl/evp.h>
-
-                          int main() {
-                              return EVP_PKEY_new_raw_public_key(
-                                  EVP_PKEY_ED25519, NULL, NULL, 0) == NULL;
-                         }"
-        . auto/feature
-
         njs_feature="OpenSSL version"
         njs_feature_name=NJS_OPENSSL_VERSION
         njs_feature_run=value
index 4fada5b6ed218bd1daf957ab8a71f0bec9beba59..c0074824fbe847f96b536f96fcf0044e6c9bdd26 100644 (file)
 #endif
 
 
+#if defined(EVP_PKEY_ED25519) && defined(EVP_PKEY_X25519)
+#define NJS_OPENSSL_HAS_25519  1
+#else
+#define NJS_OPENSSL_HAS_25519  0
+#endif
+
+
 #if (OPENSSL_VERSION_NUMBER >= 0x10002000L                            \
      && (!defined LIBRESSL_VERSION_NUMBER                            \
          || LIBRESSL_VERSION_NUMBER >= 0x3010000fL))
index 1c8afc8840b8d9b16ee7ad177ea160c13cd26e13..ce6e6fe38651209577cdd55b9b396bf983162503 100644 (file)
@@ -114,7 +114,7 @@ static njs_int_t njs_cipher_aes_ctr(njs_vm_t *vm, njs_str_t *data,
 static njs_int_t njs_cipher_aes_cbc(njs_vm_t *vm, njs_str_t *data,
     njs_webcrypto_key_t *key, njs_value_t *options, njs_bool_t encrypt,
     njs_value_t *retval);
-#if (NJS_HAVE_AES_WRAP)
+#if defined(EVP_CIPHER_CTX_FLAG_WRAP_ALLOW)
 static njs_int_t njs_cipher_aes_kw(njs_vm_t *vm, njs_str_t *data,
     njs_webcrypto_key_t *key, njs_bool_t encrypt, njs_value_t *retval);
 #endif
@@ -126,7 +126,7 @@ static njs_int_t njs_ext_export_key(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
 static njs_int_t njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
 static njs_int_t njs_webcrypto_generate_25519_keypair(njs_vm_t *vm,
     int pkey_id, njs_webcrypto_key_t *key, njs_webcrypto_algorithm_t *alg,
     unsigned usage, njs_bool_t extractable, unsigned priv_usage,
@@ -287,7 +287,7 @@ static njs_webcrypto_entry_t njs_webcrypto_alg[] = {
                               1)
     },
 
-#if (NJS_HAVE_AES_WRAP)
+#if defined(EVP_CIPHER_CTX_FLAG_WRAP_ALLOW)
     {
       njs_str("AES-KW"),
       njs_webcrypto_algorithm(NJS_ALGORITHM_AES_KW,
@@ -326,7 +326,7 @@ static njs_webcrypto_entry_t njs_webcrypto_alg[] = {
                               0)
     },
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
     {
       njs_str("Ed25519"),
       njs_webcrypto_algorithm(NJS_ALGORITHM_ED25519,
@@ -1545,7 +1545,7 @@ fail:
 }
 
 
-#if (NJS_HAVE_AES_WRAP)
+#if defined(EVP_CIPHER_CTX_FLAG_WRAP_ALLOW)
 static njs_int_t
 njs_cipher_aes_kw(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
     njs_bool_t encrypt, njs_value_t *retval)
@@ -1581,15 +1581,15 @@ njs_cipher_aes_kw(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
 
     switch (key->u.s.raw.length) {
     case 16:
-        cipher = EVP_aes_128_wrap();
+        cipher = EVP_get_cipherbyname("id-aes128-wrap");
         break;
 
     case 24:
-        cipher = EVP_aes_192_wrap();
+        cipher = EVP_get_cipherbyname("id-aes192-wrap");
         break;
 
     case 32:
-        cipher = EVP_aes_256_wrap();
+        cipher = EVP_get_cipherbyname("id-aes256-wrap");
         break;
 
     default:
@@ -1597,6 +1597,11 @@ njs_cipher_aes_kw(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
         return NJS_ERROR;
     }
 
+    if (njs_slow_path(cipher == NULL)) {
+        njs_vm_type_error(vm, "AES-KW is not supported by this OpenSSL build");
+        return NJS_ERROR;
+    }
+
     ctx = EVP_CIPHER_CTX_new();
     if (njs_slow_path(ctx == NULL)) {
         njs_webcrypto_error(vm, "EVP_CIPHER_CTX_new() failed");
@@ -2623,7 +2628,7 @@ njs_export_jwk_oct(njs_vm_t *vm, njs_webcrypto_key_t *key, njs_value_t *retval)
 }
 
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
 static njs_int_t
 njs_export_jwk_okp(njs_vm_t *vm, njs_webcrypto_key_t *key,
     njs_value_t *retval)
@@ -2779,7 +2784,7 @@ fail:
 }
 
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
 static njs_int_t
 njs_webcrypto_generate_25519_keypair(njs_vm_t *vm, int pkey_id,
     njs_webcrypto_key_t *key, njs_webcrypto_algorithm_t *alg, unsigned usage,
@@ -3100,7 +3105,7 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 
         break;
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
     case NJS_ALGORITHM_ED25519:
         ret = njs_webcrypto_generate_25519_keypair(vm, EVP_PKEY_ED25519,
                                                    key, alg, usage,
@@ -3691,7 +3696,7 @@ fail:
 }
 
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
 static EVP_PKEY *
 njs_import_jwk_okp(njs_vm_t *vm, njs_value_t *jwk, njs_webcrypto_key_t *key)
 {
@@ -4058,7 +4063,7 @@ njs_webcrypto_import_key_internal(njs_vm_t *vm, njs_webcrypto_key_format_t fmt,
                 goto fail;
             }
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
         } else if (njs_strstr_eq(&kty, &njs_str_value("OKP"))) {
             if (alg->type != NJS_ALGORITHM_ED25519
                 && alg->type != NJS_ALGORITHM_X25519)
@@ -4209,7 +4214,7 @@ njs_webcrypto_import_key_internal(njs_vm_t *vm, njs_webcrypto_key_format_t fmt,
 
         break;
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
     case NJS_ALGORITHM_ED25519:
         if (fmt == NJS_KEY_FORMAT_RAW) {
             pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL,
@@ -4701,7 +4706,7 @@ njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     outlen = 0;
 
     switch (alg->type) {
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
     case NJS_ALGORITHM_ED25519:
         mctx = njs_evp_md_ctx_new();
         if (njs_slow_path(mctx == NULL)) {
@@ -4947,7 +4952,7 @@ njs_webcrypto_export_key_raw(njs_vm_t *vm, njs_webcrypto_key_t *key,
         case NJS_ALGORITHM_ECDH:
             return njs_export_jwk_asymmetric(vm, key, retval);
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
         case NJS_ALGORITHM_ED25519:
         case NJS_ALGORITHM_X25519:
             return njs_export_jwk_okp(vm, key, retval);
@@ -5042,7 +5047,7 @@ njs_webcrypto_export_key_raw(njs_vm_t *vm, njs_webcrypto_key_t *key,
             return njs_export_raw_ec(vm, key, retval);
         }
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
         if (key->alg->type == NJS_ALGORITHM_ED25519
             || key->alg->type == NJS_ALGORITHM_X25519)
         {
@@ -5087,7 +5092,7 @@ njs_webcrypto_cipher_core(njs_vm_t *vm, njs_str_t *data,
     case NJS_ALGORITHM_AES_CBC:
         return njs_cipher_aes_cbc(vm, data, key, options, encrypt, retval);
 
-#if (NJS_HAVE_AES_WRAP)
+#if defined(EVP_CIPHER_CTX_FLAG_WRAP_ALLOW)
     case NJS_ALGORITHM_AES_KW:
         return njs_cipher_aes_kw(vm, data, key, encrypt, retval);
 #endif
index 3bc1b6a1e5b0769b59241f44aa9e5467068fa58b..8249a39e5b1a57d4301fe624aeea16d200000890 100644 (file)
@@ -119,7 +119,7 @@ static JSValue qjs_cipher_aes_ctr(JSContext *cx, njs_str_t *data,
     qjs_webcrypto_key_t *key, JSValue options, int encrypt);
 static JSValue qjs_cipher_aes_cbc(JSContext *cx, njs_str_t *data,
     qjs_webcrypto_key_t *key, JSValue options, int encrypt);
-#if (NJS_HAVE_AES_WRAP)
+#if defined(EVP_CIPHER_CTX_FLAG_WRAP_ALLOW)
 static JSValue qjs_cipher_aes_kw(JSContext *cx, njs_str_t *data,
     qjs_webcrypto_key_t *key, int encrypt);
 #endif
@@ -133,7 +133,7 @@ static JSValue qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv);
 static JSValue qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv);
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
 static int qjs_webcrypto_generate_25519_keypair(JSContext *cx, int pkey_id,
     qjs_webcrypto_key_t *wkey, qjs_webcrypto_algorithm_t *alg,
     unsigned usage, int extractable, unsigned priv_usage,
@@ -285,7 +285,7 @@ static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = {
                               1)
     },
 
-#if (NJS_HAVE_AES_WRAP)
+#if defined(EVP_CIPHER_CTX_FLAG_WRAP_ALLOW)
     {
       njs_str("AES-KW"),
       qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_KW,
@@ -324,7 +324,7 @@ static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = {
                               0)
     },
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
     {
       njs_str("Ed25519"),
       qjs_webcrypto_algorithm(QJS_ALGORITHM_ED25519,
@@ -1330,7 +1330,7 @@ fail:
 }
 
 
-#if (NJS_HAVE_AES_WRAP)
+#if defined(EVP_CIPHER_CTX_FLAG_WRAP_ALLOW)
 static JSValue
 qjs_cipher_aes_kw(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
     int encrypt)
@@ -1366,15 +1366,15 @@ qjs_cipher_aes_kw(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
 
     switch (key->u.s.raw.length) {
     case 16:
-        cipher = EVP_aes_128_wrap();
+        cipher = EVP_get_cipherbyname("id-aes128-wrap");
         break;
 
     case 24:
-        cipher = EVP_aes_192_wrap();
+        cipher = EVP_get_cipherbyname("id-aes192-wrap");
         break;
 
     case 32:
-        cipher = EVP_aes_256_wrap();
+        cipher = EVP_get_cipherbyname("id-aes256-wrap");
         break;
 
     default:
@@ -1382,6 +1382,11 @@ qjs_cipher_aes_kw(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
         return JS_EXCEPTION;
     }
 
+    if (cipher == NULL) {
+        return JS_ThrowTypeError(cx,
+                                "AES-KW is not supported by this OpenSSL build");
+    }
+
     ctx = EVP_CIPHER_CTX_new();
     if (ctx == NULL) {
         qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed");
@@ -1805,7 +1810,7 @@ fail:
 }
 
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
 static JSValue
 qjs_export_jwk_okp(JSContext *cx, qjs_webcrypto_key_t *key)
 {
@@ -2504,7 +2509,7 @@ qjs_webcrypto_export_key_raw(JSContext *cx, qjs_webcrypto_key_t *key,
         case QJS_ALGORITHM_ECDH:
             return qjs_export_jwk_asymmetric(cx, key);
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
         case QJS_ALGORITHM_ED25519:
         case QJS_ALGORITHM_X25519:
             return qjs_export_jwk_okp(cx, key);
@@ -2599,7 +2604,7 @@ qjs_webcrypto_export_key_raw(JSContext *cx, qjs_webcrypto_key_t *key,
             return qjs_export_raw_ec(cx, key);
         }
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
         if (key->alg->type == QJS_ALGORITHM_ED25519
             || key->alg->type == QJS_ALGORITHM_X25519)
         {
@@ -2642,7 +2647,7 @@ qjs_webcrypto_cipher_core(JSContext *cx, njs_str_t *data,
     case QJS_ALGORITHM_AES_CBC:
         return qjs_cipher_aes_cbc(cx, data, key, options, encrypt);
 
-#if (NJS_HAVE_AES_WRAP)
+#if defined(EVP_CIPHER_CTX_FLAG_WRAP_ALLOW)
     case QJS_ALGORITHM_AES_KW:
         return qjs_cipher_aes_kw(cx, data, key, encrypt);
 #endif
@@ -2691,7 +2696,7 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc,
 }
 
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
 static int
 qjs_webcrypto_generate_25519_keypair(JSContext *cx, int pkey_id,
     qjs_webcrypto_key_t *wkey, qjs_webcrypto_algorithm_t *alg, unsigned usage,
@@ -2986,7 +2991,7 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val,
 
         break;
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
     case QJS_ALGORITHM_ED25519:
         n = qjs_webcrypto_generate_25519_keypair(cx, EVP_PKEY_ED25519,
                                                  wkey, alg, usage,
@@ -3638,7 +3643,7 @@ qjs_import_raw_ec(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key)
 }
 
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
 static EVP_PKEY *
 qjs_import_jwk_okp(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
 {
@@ -4045,7 +4050,7 @@ qjs_webcrypto_import_key_internal(JSContext *cx, qjs_webcrypto_key_format_t fmt,
 
             break;
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
         case QJS_KEY_JWK_KTY_OKP:
             if (alg->type != QJS_ALGORITHM_ED25519
                 && alg->type != QJS_ALGORITHM_X25519)
@@ -4193,7 +4198,7 @@ qjs_webcrypto_import_key_internal(JSContext *cx, qjs_webcrypto_key_format_t fmt,
         wkey->u.a.pkey = pkey;
         break;
 
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
     case QJS_ALGORITHM_ED25519:
         if (fmt == QJS_KEY_FORMAT_RAW) {
             pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL,
@@ -4673,7 +4678,7 @@ qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, int argc,
     outlen = 0;
 
     switch (alg->type) {
-#if (NJS_HAVE_ED25519)
+#if (NJS_OPENSSL_HAS_25519)
     case QJS_ALGORITHM_ED25519:
         mctx = njs_evp_md_ctx_new();
         if (mctx == NULL) {
@@ -5948,6 +5953,10 @@ qjs_webcrypto_init(JSContext *cx, const char *name)
     JSValue      crypto, proto, global_obj;
     JSModuleDef  *m;
 
+#if (OPENSSL_VERSION_NUMBER < 0x10100003L)
+    OpenSSL_add_all_algorithms();
+#endif
+
     if (!JS_IsRegisteredClass(JS_GetRuntime(cx),
                               QJS_CORE_CLASS_ID_WEBCRYPTO_KEY))
     {
index 33869574d9f27fa2bd2e645ede2ab4a2d72ad8ec..94f916a46476070f56d3f6fb62cd75bc52ef87e3 100644 (file)
@@ -48,6 +48,10 @@ http {
         location /random_values_test {
             js_content test.random_values_test;
         }
+
+        location /algorithms {
+            js_content test.algorithms;
+        }
     }
 }
 
@@ -80,7 +84,70 @@ $t->write_file('test.js', <<EOF);
         r.return(200, bits1 > (mean - 10 * stdd) && bits1 < (mean + 10 * stdd));
     }
 
-    export default {has_crypto, random_values_test};
+    async function ed25519() {
+        let data = new Uint8Array(16);
+        let ed = await crypto.subtle.generateKey("Ed25519", false,
+                                                ["sign", "verify"]);
+        let signature = await crypto.subtle.sign("Ed25519",
+                                                 ed.privateKey, data);
+        if (!await crypto.subtle.verify("Ed25519", ed.publicKey,
+                                         signature, data))
+        {
+            throw Error("Ed25519 verification failed");
+        }
+    }
+
+    async function x25519() {
+        let x = await crypto.subtle.generateKey("X25519", false,
+                                               ["deriveBits"]);
+        let bits = await crypto.subtle.deriveBits(
+            {name: "X25519", public: x.publicKey}, x.privateKey, 256);
+        if (bits.byteLength !== 32) {
+            throw Error("X25519 derivation failed");
+        }
+    }
+
+    async function aes_kw() {
+        let data = new Uint8Array(16);
+        let key = await crypto.subtle.importKey("raw", data,
+            "AES-GCM", true, ["encrypt"]);
+        for (let length = 128; length <= 256; length += 64) {
+            let kw = await crypto.subtle.generateKey(
+                {name: "AES-KW", length}, false, ["wrapKey", "unwrapKey"]);
+            let wrapped = await crypto.subtle.wrapKey("raw", key, kw,
+                                                     "AES-KW");
+            let unwrapped = await crypto.subtle.unwrapKey("raw", wrapped,
+                kw, "AES-KW", "AES-GCM", true, ["encrypt"]);
+            let raw = await crypto.subtle.exportKey("raw", unwrapped);
+            if (Buffer.from(raw).compare(Buffer.from(data)) !== 0) {
+                throw Error("AES-KW roundtrip failed");
+            }
+        }
+    }
+
+    async function algorithms(r) {
+        try {
+            switch (r.args.algorithm) {
+            case "Ed25519":
+                await ed25519();
+                break;
+            case "X25519":
+                await x25519();
+                break;
+            case "AES-KW":
+                await aes_kw();
+                break;
+            default:
+                throw Error("unknown test algorithm");
+            }
+
+            r.return(200, "SUCCESS");
+        } catch (e) {
+            r.return(500, e.message);
+        }
+    }
+
+    export default {has_crypto, random_values_test, algorithms};
 
 EOF
 
@@ -89,10 +156,28 @@ $t->try_run('no njs');
 plan(skip_all => 'njs crypto module not available')
        if http_get('/has_crypto') !~ /true/;
 
-$t->plan(1);
+$t->plan(4);
 
 ###############################################################################
 
 like(http_get('/random_values_test'), qr/true/, 'random_values_test');
 
+SKIP: {
+       skip 'no 25519 support in SSL library', 2
+               unless $t->has_feature('openssl:1.1.1')
+               || $t->has_feature('libressl:3.7.0')
+               || $t->has_module('BoringSSL|AWS-LC');
+
+       like(http_get('/algorithms?algorithm=Ed25519'), qr/SUCCESS/, 'Ed25519');
+       like(http_get('/algorithms?algorithm=X25519'), qr/SUCCESS/, 'X25519');
+}
+
+SKIP: {
+       skip 'no AES-KW support in SSL library', 1
+               unless $t->has_feature('openssl:1.0.2')
+               || $t->has_feature('libressl:3.0.0');
+
+       like(http_get('/algorithms?algorithm=AES-KW'), qr/SUCCESS/, 'AES-KW');
+}
+
 ###############################################################################
index 619a093d6252b70012aafe7217844622a40ecd50..0d9272c7c41d1ab82b6454e1b2686e5d91033a42 100644 (file)
@@ -59,6 +59,27 @@ stream {
         listen  127.0.0.1:8081;
         return  $test;
     }
+
+    server {
+        listen  127.0.0.1:8082;
+        js_preread test.test_ed25519;
+        js_var $result;
+        return $result;
+    }
+
+    server {
+        listen  127.0.0.1:8083;
+        js_preread test.test_x25519;
+        js_var $result;
+        return $result;
+    }
+
+    server {
+        listen  127.0.0.1:8084;
+        js_preread test.test_aes_kw;
+        js_var $result;
+        return $result;
+    }
 }
 
 EOF
@@ -90,7 +111,73 @@ $t->write_file('test.js', <<EOF);
         return bits1 > (mean - 10 * stdd) && bits1 < (mean + 10 * stdd);
     }
 
-    export default {has_crypto, random_values_test};
+    async function ed25519() {
+        let data = new Uint8Array(16);
+        let ed = await crypto.subtle.generateKey("Ed25519", false,
+                                                ["sign", "verify"]);
+        let signature = await crypto.subtle.sign("Ed25519",
+                                                 ed.privateKey, data);
+        if (!await crypto.subtle.verify("Ed25519", ed.publicKey,
+                                         signature, data))
+        {
+            throw Error("Ed25519 verification failed");
+        }
+    }
+
+    async function x25519() {
+        let x = await crypto.subtle.generateKey("X25519", false,
+                                               ["deriveBits"]);
+        let bits = await crypto.subtle.deriveBits(
+            {name: "X25519", public: x.publicKey}, x.privateKey, 256);
+        if (bits.byteLength !== 32) {
+            throw Error("X25519 derivation failed");
+        }
+    }
+
+    async function aes_kw() {
+        let data = new Uint8Array(16);
+        let key = await crypto.subtle.importKey("raw", data,
+            "AES-GCM", true, ["encrypt"]);
+        for (let length = 128; length <= 256; length += 64) {
+            let kw = await crypto.subtle.generateKey(
+                {name: "AES-KW", length}, false, ["wrapKey", "unwrapKey"]);
+            let wrapped = await crypto.subtle.wrapKey("raw", key, kw,
+                                                     "AES-KW");
+            let unwrapped = await crypto.subtle.unwrapKey("raw", wrapped,
+                kw, "AES-KW", "AES-GCM", true, ["encrypt"]);
+            let raw = await crypto.subtle.exportKey("raw", unwrapped);
+            if (Buffer.from(raw).compare(Buffer.from(data)) !== 0) {
+                throw Error("AES-KW roundtrip failed");
+            }
+        }
+    }
+
+    async function algorithms(s, operation) {
+        try {
+            await operation();
+
+            s.variables.result = "SUCCESS";
+        } catch (e) {
+            s.variables.result = e.message;
+        }
+
+        s.done();
+    }
+
+    async function test_ed25519(s) {
+        await algorithms(s, ed25519);
+    }
+
+    async function test_x25519(s) {
+        await algorithms(s, x25519);
+    }
+
+    async function test_aes_kw(s) {
+        await algorithms(s, aes_kw);
+    }
+
+    export default {has_crypto, random_values_test,
+                    test_ed25519, test_x25519, test_aes_kw};
 EOF
 
 $t->try_run('no stream js_var');
@@ -98,10 +185,28 @@ $t->try_run('no stream js_var');
 plan(skip_all => 'njs crypto module not available')
        if http_get('/has_crypto') !~ /true/;
 
-$t->plan(1);
+$t->plan(4);
 
 ###############################################################################
 
 is(stream('127.0.0.1:' . port(8081))->io('###'), 'true', 'random values test');
 
+SKIP: {
+       skip 'no 25519 support in SSL library', 2
+               unless $t->has_feature('openssl:1.1.1')
+               || $t->has_feature('libressl:3.7.0')
+               || $t->has_module('BoringSSL|AWS-LC');
+
+       is(stream('127.0.0.1:' . port(8082))->read(), 'SUCCESS', 'Ed25519');
+       is(stream('127.0.0.1:' . port(8083))->read(), 'SUCCESS', 'X25519');
+}
+
+SKIP: {
+       skip 'no AES-KW support in SSL library', 1
+               unless $t->has_feature('openssl:1.0.2')
+               || $t->has_feature('libressl:3.0.0');
+
+       is(stream('127.0.0.1:' . port(8084))->read(), 'SUCCESS', 'AES-KW');
+}
+
 ###############################################################################
index adc0fb41686f09ff29c6a8e499c5eafb31453fe0..033d3eff73b0d52a71d813b029997206fc1b1165 100644 (file)
@@ -6,3 +6,35 @@ function has_webcrypto() {
     return (typeof crypto != 'undefined') ? crypto : null;
 }
 
+async function has_webcrypto_algorithm(name, length) {
+    if (name !== 'Ed25519' && name !== 'X25519' && name !== 'AES-KW') {
+        throw Error('unknown test algorithm: ' + name);
+    }
+
+    if (!has_buffer() || !has_webcrypto()) {
+        return false;
+    }
+
+    try {
+        if (name === 'AES-KW') {
+            await crypto.subtle.generateKey(
+                {name, length: length || 128}, false, ['wrapKey']);
+
+        } else {
+            await crypto.subtle.generateKey(name, false,
+                name === 'Ed25519' ? ['sign', 'verify'] : ['deriveBits']);
+        }
+
+    } catch (e) {
+        if (e.message === 'unknown algorithm name: "' + name + '"'
+            || (e.name === 'NotSupportedError'
+                && e.message === 'Unrecognized algorithm name'))
+        {
+            return false;
+        }
+
+        throw e;
+    }
+
+    return true;
+}
index 14deebd95150ee26801dcb4e101e60665a5ea557..6effd049612f047a0269398acf3fdda3500b52be 100644 (file)
@@ -19,10 +19,18 @@ async function run(tlist) {
         return false;
     }
 
-    function map(ts) {
-        return ts.tests.map(t => {
+    async function map(ts) {
+        let skipped = [];
+
+        for (let i = 0; i < ts.tests.length; i++) {
+            let t = ts.tests[i];
+
+            skipped[i] = t.skip && await t.skip();
+        }
+
+        return ts.tests.map((t, i) => {
             try {
-                if (t.skip && t.skip()) {
+                if (skipped[i]) {
                     return Promise.resolve("SKIPPED");
                 }
 
@@ -40,11 +48,11 @@ async function run(tlist) {
     for (let k = 0; k < tlist.length; k++) {
         let ts = tlist[k];
 
-        if (ts.skip && ts.skip()) {
+        if (ts.skip && await ts.skip()) {
             continue;
         }
 
-        let results = await Promise.allSettled(map(ts));
+        let results = await Promise.allSettled(await map(ts));
         let r = results.map((r, i) => { r.passed = validate(ts.tests, r, i); return r; });
 
         let passed = r.filter(r => r.passed).length;
index ea10a8e7e87a894198cfdc61a8e48955b3d249af..50d89720f6678d0ca493ffa305e22d66d35a9481 100644 (file)
@@ -10,17 +10,6 @@ async function generate_kek(usages) {
 
 
 async function test(params) {
-    try {
-        await crypto.subtle.generateKey(
-            {name: "AES-KW", length: 128}, true, ["wrapKey", "unwrapKey"]);
-    } catch (e) {
-        if (e.message.indexOf("AES-KW") !== -1) {
-            return 'SKIPPED';
-        }
-
-        throw e;
-    }
-
     let key;
 
     if (params.generate) {
@@ -85,6 +74,21 @@ async function test(params) {
         throw Error(`AES-KW unwrap failed`);
     }
 
+    let tampered = Buffer.from(expected);
+    tampered[0] ^= 1;
+    let rejected = false;
+
+    try {
+        await crypto.subtle.unwrapKey("raw", tampered, kek,
+            "AES-KW", "AES-GCM", true, ["encrypt"]);
+    } catch (e) {
+        rejected = true;
+    }
+
+    if (!rejected) {
+        throw Error("AES-KW accepted a tampered key");
+    }
+
     return 'SUCCESS';
 }
 
@@ -149,7 +153,7 @@ async function test_no_unwrapkey_usage() {
 
 let aes_kw_tsuite = {
     name: "AES-KW wrap/unwrap",
-    skip: () => (!has_buffer() || !has_webcrypto()),
+    skip: async () => !await has_webcrypto_algorithm("AES-KW"),
     T: test,
     prepare_args: (args) => args,
 
@@ -199,7 +203,7 @@ let aes_kw_tsuite = {
 
 let aes_kw_error_tsuite = {
     name: "AES-KW errors",
-    skip: () => (!has_buffer() || !has_webcrypto()),
+    skip: async () => !await has_webcrypto_algorithm("AES-KW"),
     T: (params) => params.T(),
     prepare_args: (args) => args,
 
index fd8695e9e278495cf27c198835af173be5ec2131..fdb2057dcc91c11083f38e385abbe7040d59864e 100644 (file)
@@ -4,16 +4,6 @@ flags: [async]
 ---*/
 
 async function test(params) {
-    try {
-        await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
-    } catch (e) {
-        if (e.message.indexOf("Ed25519") !== -1) {
-            return 'SKIPPED';
-        }
-
-        throw e;
-    }
-
     /* generateKey + sign/verify roundtrip */
     if (params.generate) {
         let kp = await crypto.subtle.generateKey("Ed25519", true,
@@ -139,7 +129,7 @@ async function test(params) {
         return 'SUCCESS';
     }
 
-    /* RFC 8032 test vector: sign-only (verify uses derived public key) */
+    /* RFC 8032 test vector */
     if (params.rfc8032) {
         let priv_jwk = {
             kty: "OKP",
@@ -161,6 +151,19 @@ async function test(params) {
                 + "expected:\n" + params.expected);
         }
 
+        let pub = await crypto.subtle.importKey("jwk",
+            {kty: "OKP", crv: "Ed25519", x: params.x},
+            "Ed25519", false, ["verify"]);
+
+        if (!await crypto.subtle.verify("Ed25519", pub, expected, data)) {
+            throw Error("RFC 8032 signature verification failed");
+        }
+
+        expected[0] ^= 1;
+        if (await crypto.subtle.verify("Ed25519", pub, expected, data)) {
+            throw Error("Ed25519 accepted a tampered signature");
+        }
+
         return 'SUCCESS';
     }
 
@@ -169,7 +172,7 @@ async function test(params) {
 
 let ed25519_tsuite = {
     name: "Ed25519 sign/verify",
-    skip: () => (!has_buffer() || !has_webcrypto()),
+    skip: async () => !await has_webcrypto_algorithm("Ed25519"),
     T: test,
     prepare_args: (args) => args,
 
@@ -198,16 +201,6 @@ let ed25519_tsuite = {
 ]};
 
 async function test_sign_with_x25519() {
-    try {
-        await crypto.subtle.generateKey("X25519", true, ["deriveBits"]);
-    } catch (e) {
-        if (e.message.indexOf("X25519") !== -1) {
-            return 'SKIPPED';
-        }
-
-        throw e;
-    }
-
     let kp = await crypto.subtle.generateKey("X25519", true,
                                               ["deriveBits"]);
     let data = new TextEncoder().encode("test");
@@ -215,17 +208,6 @@ async function test_sign_with_x25519() {
 }
 
 async function test_derive_with_ed25519() {
-    try {
-        await crypto.subtle.generateKey("Ed25519", true,
-                                         ["sign", "verify"]);
-    } catch (e) {
-        if (e.message.indexOf("Ed25519") !== -1) {
-            return 'SKIPPED';
-        }
-
-        throw e;
-    }
-
     let kp = await crypto.subtle.generateKey("Ed25519", true,
                                               ["sign", "verify"]);
     await crypto.subtle.deriveBits(
@@ -235,7 +217,8 @@ async function test_derive_with_ed25519() {
 
 let ed25519_error_tsuite = {
     name: "Ed25519 errors",
-    skip: () => (!has_buffer() || !has_webcrypto()),
+    skip: async () => !await has_webcrypto_algorithm("Ed25519")
+                     || !await has_webcrypto_algorithm("X25519"),
     T: (params) => params.T(),
     prepare_args: (args) => args,
 
index b5fce41d5c0ab27507c9ce311ed61a29e5718e10..d6ad8eb7c1749b0c8eb16a7e0d8df92e4b32fd7d 100644 (file)
@@ -4,20 +4,8 @@ flags: [async]
 ---*/
 
 async function test(params) {
-    let wrapping_key;
-
-    try {
-        wrapping_key = await crypto.subtle.generateKey(
-            params.wrap_alg, true, ["wrapKey", "unwrapKey"]);
-    } catch (e) {
-        if (e.message.indexOf("unknown algorithm") !== -1
-            || e.message.indexOf("Unrecognized algorithm") !== -1)
-        {
-            return 'SKIPPED';
-        }
-
-        throw e;
-    }
+    let wrapping_key = await crypto.subtle.generateKey(
+        params.wrap_alg, true, ["wrapKey", "unwrapKey"]);
 
     let key = await crypto.subtle.generateKey(
         params.key_alg, true, params.key_usage);
@@ -109,6 +97,7 @@ let wrap_tsuite = {
     tests: [
         /* AES-KW wrapping an AES-GCM key (raw format) */
         { wrap_alg: {name: "AES-KW", length: 256},
+          skip: async () => !await has_webcrypto_algorithm("AES-KW", 256),
           key_alg: {name: "AES-GCM", length: 128},
           key_usage: ["encrypt", "decrypt"],
           format: "raw",
@@ -119,6 +108,7 @@ let wrap_tsuite = {
 
         /* AES-KW wrapping an AES-CBC key */
         { wrap_alg: {name: "AES-KW", length: 128},
+          skip: async () => !await has_webcrypto_algorithm("AES-KW", 128),
           key_alg: {name: "AES-CBC", length: 256},
           key_usage: ["encrypt", "decrypt"],
           format: "raw",
@@ -127,6 +117,7 @@ let wrap_tsuite = {
 
         /* AES-KW wrapping an HMAC key */
         { wrap_alg: {name: "AES-KW", length: 256},
+          skip: async () => !await has_webcrypto_algorithm("AES-KW", 256),
           key_alg: {name: "HMAC", hash: "SHA-256"},
           key_usage: ["sign", "verify"],
           format: "raw",
@@ -194,7 +185,8 @@ let wrap_tsuite = {
 
 let wrap_error_tsuite = {
     name: "wrapKey/unwrapKey errors",
-    skip: () => (!has_buffer() || !has_webcrypto()),
+    skip: async () => !await has_webcrypto_algorithm("AES-KW", 128)
+                     || !await has_webcrypto_algorithm("AES-KW", 256),
     T: (params) => params.T(),
     prepare_args: (args) => args,
 
index 5489e4ec7129ecf6c0488b06a58bfffb8af5b4f1..3e4fd76b636fedebd45ccde0bb481c3d159070d6 100644 (file)
@@ -19,16 +19,6 @@ async function import_key_pair(params) {
 
 
 async function test(params) {
-    try {
-        await crypto.subtle.generateKey("X25519", true, ["deriveBits"]);
-    } catch (e) {
-        if (e.message.indexOf("X25519") !== -1) {
-            return 'SKIPPED';
-        }
-
-        throw e;
-    }
-
     /* generateKey + deriveBits roundtrip */
     if (params.derive_bits) {
         let alice = await crypto.subtle.generateKey("X25519", true,
@@ -106,6 +96,19 @@ async function test(params) {
                         + `${params.expected}`);
         }
 
+        let jwk = await crypto.subtle.exportKey("jwk", alice.privateKey);
+        let raw = await crypto.subtle.exportKey("raw", bob.publicKey);
+        let privateKey = await crypto.subtle.importKey("jwk", jwk,
+            "X25519", false, ["deriveBits"]);
+        let publicKey = await crypto.subtle.importKey("raw", raw,
+            "X25519", false, []);
+        let secret = await crypto.subtle.deriveBits(
+            {name: "X25519", public: publicKey}, privateKey, 256);
+
+        if (Buffer.from(secret).toString("base64url") !== params.expected) {
+            throw Error("X25519 JWK/raw key agreement failed");
+        }
+
         return 'SUCCESS';
     }
 
@@ -188,7 +191,7 @@ async function test(params) {
 
 let x25519_tsuite = {
     name: "X25519 key agreement",
-    skip: () => (!has_buffer() || !has_webcrypto()),
+    skip: async () => !await has_webcrypto_algorithm("X25519"),
     T: test,
     prepare_args: (args) => args,
 
@@ -211,16 +214,6 @@ let x25519_tsuite = {
 ]};
 
 async function test_derive_with_ed25519_key() {
-    try {
-        await crypto.subtle.generateKey("Ed25519", true, ["sign"]);
-    } catch (e) {
-        if (e.message.indexOf("Ed25519") !== -1) {
-            return 'SKIPPED';
-        }
-
-        throw e;
-    }
-
     let x_kp = await crypto.subtle.generateKey("X25519", true,
                                                 ["deriveBits"]);
     let ed_kp = await crypto.subtle.generateKey("Ed25519", true,
@@ -232,16 +225,6 @@ async function test_derive_with_ed25519_key() {
 }
 
 async function test_sign_with_x25519_key() {
-    try {
-        await crypto.subtle.generateKey("X25519", true, ["deriveBits"]);
-    } catch (e) {
-        if (e.message.indexOf("X25519") !== -1) {
-            return 'SKIPPED';
-        }
-
-        throw e;
-    }
-
     let kp = await crypto.subtle.generateKey("X25519", true,
                                               ["deriveBits"]);
     let data = new TextEncoder().encode("test");
@@ -250,7 +233,8 @@ async function test_sign_with_x25519_key() {
 
 let x25519_error_tsuite = {
     name: "X25519 errors",
-    skip: () => (!has_buffer() || !has_webcrypto()),
+    skip: async () => !await has_webcrypto_algorithm("X25519")
+                     || !await has_webcrypto_algorithm("Ed25519"),
     T: (params) => params.T(),
     prepare_args: (args) => args,