From: Dmitry Volyntsev Date: Tue, 13 Oct 2026 05:37:30 +0000 (-0700) Subject: WebCrypto: fix algorithm availability in nginx modules X-Git-Url: https://git.kaiwu.me/%7B@url%7D?a=commitdiff_plain;h=233509254cc86a8d6a5e703052e3a8b272604ea5;p=njs.git WebCrypto: fix algorithm availability in nginx modules 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. --- diff --git a/auto/openssl b/auto/openssl index b5f65a70..0dbaf5c1 100644 --- a/auto/openssl +++ b/auto/openssl @@ -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 - - 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 - - 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 diff --git a/external/njs_openssl.h b/external/njs_openssl.h index 4fada5b6..c0074824 100644 --- a/external/njs_openssl.h +++ b/external/njs_openssl.h @@ -37,6 +37,13 @@ #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)) diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index 1c8afc88..ce6e6fe3 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -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 diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index 3bc1b6a1..8249a39e 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -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)) { diff --git a/nginx/t/js_webcrypto.t b/nginx/t/js_webcrypto.t index 33869574..94f916a4 100644 --- a/nginx/t/js_webcrypto.t +++ b/nginx/t/js_webcrypto.t @@ -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', < (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'); +} + ############################################################################### diff --git a/nginx/t/stream_js_webcrypto.t b/nginx/t/stream_js_webcrypto.t index 619a093d..0d9272c7 100644 --- a/nginx/t/stream_js_webcrypto.t +++ b/nginx/t/stream_js_webcrypto.t @@ -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', < (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'); +} + ############################################################################### diff --git a/test/harness/compatWebcrypto.js b/test/harness/compatWebcrypto.js index adc0fb41..033d3eff 100644 --- a/test/harness/compatWebcrypto.js +++ b/test/harness/compatWebcrypto.js @@ -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; +} diff --git a/test/harness/runTsuite.js b/test/harness/runTsuite.js index 14deebd9..6effd049 100644 --- a/test/harness/runTsuite.js +++ b/test/harness/runTsuite.js @@ -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; diff --git a/test/webcrypto/aes_kw.t.mjs b/test/webcrypto/aes_kw.t.mjs index ea10a8e7..50d89720 100644 --- a/test/webcrypto/aes_kw.t.mjs +++ b/test/webcrypto/aes_kw.t.mjs @@ -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, diff --git a/test/webcrypto/ed25519.t.mjs b/test/webcrypto/ed25519.t.mjs index fd8695e9..fdb2057d 100644 --- a/test/webcrypto/ed25519.t.mjs +++ b/test/webcrypto/ed25519.t.mjs @@ -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, diff --git a/test/webcrypto/wrap.t.mjs b/test/webcrypto/wrap.t.mjs index b5fce41d..d6ad8eb7 100644 --- a/test/webcrypto/wrap.t.mjs +++ b/test/webcrypto/wrap.t.mjs @@ -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, diff --git a/test/webcrypto/x25519.t.mjs b/test/webcrypto/x25519.t.mjs index 5489e4ec..3e4fd76b 100644 --- a/test/webcrypto/x25519.t.mjs +++ b/test/webcrypto/x25519.t.mjs @@ -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,