[njs] WebCrypto: extended support for symmetric keys.

Dmitry Volyntsev xeioex at nginx.com
Thu Jan 5 04:42:47 UTC 2023


details:   https://hg.nginx.org/njs/rev/2e3bbe8743af
branches:  
changeset: 2021:2e3bbe8743af
user:      Dmitry Volyntsev <xeioex at nginx.com>
date:      Wed Jan 04 18:07:30 2023 -0800
description:
WebCrypto: extended support for symmetric keys.

The following functionality for HMAC and AES-* keys were added:
    importKey() supporting 'jwk' format,
    exportKey() supporting 'jwk' and 'raw' formats,
    generateKey().

diffstat:

 external/njs_webcrypto_module.c |  365 +++++++++++++++++++++++++++++++++++++--
 test/ts/test.ts                 |    8 +
 test/webcrypto/export.t.js      |  180 +++++++++++++++++++
 test/webcrypto/sign.t.js        |   19 ++
 ts/njs_webcrypto.d.ts           |   12 +-
 5 files changed, 555 insertions(+), 29 deletions(-)

diffs (806 lines):

diff -r 0681bf662222 -r 2e3bbe8743af external/njs_webcrypto_module.c
--- a/external/njs_webcrypto_module.c	Wed Jan 04 17:49:22 2023 -0800
+++ b/external/njs_webcrypto_module.c	Wed Jan 04 18:07:30 2023 -0800
@@ -167,7 +167,8 @@ static njs_webcrypto_entry_t njs_webcryp
                               NJS_KEY_USAGE_WRAP_KEY |
                               NJS_KEY_USAGE_UNWRAP_KEY |
                               NJS_KEY_USAGE_GENERATE_KEY,
-                              NJS_KEY_FORMAT_RAW)
+                              NJS_KEY_FORMAT_RAW |
+                              NJS_KEY_FORMAT_JWK)
     },
 
     {
@@ -178,7 +179,8 @@ static njs_webcrypto_entry_t njs_webcryp
                               NJS_KEY_USAGE_WRAP_KEY |
                               NJS_KEY_USAGE_UNWRAP_KEY |
                               NJS_KEY_USAGE_GENERATE_KEY,
-                              NJS_KEY_FORMAT_RAW)
+                              NJS_KEY_FORMAT_RAW |
+                              NJS_KEY_FORMAT_JWK)
     },
 
     {
@@ -189,7 +191,8 @@ static njs_webcrypto_entry_t njs_webcryp
                               NJS_KEY_USAGE_WRAP_KEY |
                               NJS_KEY_USAGE_UNWRAP_KEY |
                               NJS_KEY_USAGE_GENERATE_KEY,
-                              NJS_KEY_FORMAT_RAW)
+                              NJS_KEY_FORMAT_RAW |
+                              NJS_KEY_FORMAT_JWK)
     },
 
     {
@@ -258,7 +261,8 @@ static njs_webcrypto_entry_t njs_webcryp
                               NJS_KEY_USAGE_GENERATE_KEY |
                               NJS_KEY_USAGE_SIGN |
                               NJS_KEY_USAGE_VERIFY,
-                              NJS_KEY_FORMAT_RAW)
+                              NJS_KEY_FORMAT_RAW |
+                              NJS_KEY_FORMAT_JWK)
     },
 
     {
@@ -325,7 +329,7 @@ static njs_webcrypto_entry_t njs_webcryp
 
 
 static njs_str_t
-    njs_webcrypto_alg_name[NJS_ALGORITHM_RSA_OAEP + 1][NJS_HASH_SHA512 + 1] = {
+    njs_webcrypto_alg_name[NJS_ALGORITHM_HMAC + 1][NJS_HASH_SHA512 + 1] = {
     {
         njs_null_str,
         njs_str("RS1"),
@@ -349,6 +353,37 @@ static njs_str_t
         njs_str("RSA-OAEP-384"),
         njs_str("RSA-OAEP-512"),
     },
+
+    {
+        njs_null_str,
+        njs_str("HS1"),
+        njs_str("HS256"),
+        njs_str("HS384"),
+        njs_str("HS512"),
+    },
+};
+
+static njs_str_t njs_webcrypto_alg_aes_name[3][3 + 1] = {
+    {
+        njs_str("A128GCM"),
+        njs_str("A192GCM"),
+        njs_str("A256GCM"),
+        njs_null_str,
+    },
+
+    {
+        njs_str("A128CTR"),
+        njs_str("A192CTR"),
+        njs_str("A256CTR"),
+        njs_null_str,
+    },
+
+    {
+        njs_str("A128CBC"),
+        njs_str("A192CBC"),
+        njs_str("A256CBC"),
+        njs_null_str,
+    },
 };
 
 
@@ -560,6 +595,7 @@ static const njs_value_t  string_d = njs
 static const njs_value_t  string_dp = njs_string("dp");
 static const njs_value_t  string_dq = njs_string("dq");
 static const njs_value_t  string_e = njs_string("e");
+static const njs_value_t  string_k = njs_string("k");
 static const njs_value_t  string_n = njs_string("n");
 static const njs_value_t  string_p = njs_string("p");
 static const njs_value_t  string_q = njs_string("q");
@@ -570,6 +606,7 @@ static const njs_value_t  string_ext = n
 static const njs_value_t  string_crv = njs_string("crv");
 static const njs_value_t  string_kty = njs_string("kty");
 static const njs_value_t  key_ops = njs_string("key_ops");
+static const njs_value_t  string_length = njs_string("length");
 
 
 static njs_int_t    njs_webcrypto_crypto_key_proto_id;
@@ -1036,7 +1073,6 @@ njs_cipher_aes_ctr(njs_vm_t *vm, njs_str
     u_char            iv2[16];
 
     static const njs_value_t  string_counter = njs_string("counter");
-    static const njs_value_t  string_length = njs_string("length");
 
     switch (key->raw.length) {
     case 16:
@@ -1356,7 +1392,6 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t
 
     static const njs_value_t  string_info = njs_string("info");
     static const njs_value_t  string_salt = njs_string("salt");
-    static const njs_value_t  string_length = njs_string("length");
     static const njs_value_t  string_iterations = njs_string("iterations");
 
     aobject = njs_arg(args, nargs, 1);
@@ -2032,6 +2067,71 @@ njs_export_jwk_asymmetric(njs_vm_t *vm, 
 
 
 static njs_int_t
+njs_export_jwk_oct(njs_vm_t *vm, njs_webcrypto_key_t *key, njs_value_t *retval)
+{
+    njs_int_t            ret;
+    njs_str_t            *nm;
+    njs_value_t          k, alg, ops, extractable;
+    njs_webcrypto_alg_t  type;
+
+    static const njs_value_t  oct_str = njs_string("oct");
+
+    njs_assert(key->raw.start != NULL)
+
+    ret = njs_string_base64url(vm, &k, &key->raw);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    type = key->alg->type;
+
+    if (key->alg->type == NJS_ALGORITHM_HMAC) {
+        nm = &njs_webcrypto_alg_name[type][key->hash];
+        (void) njs_vm_value_string_set(vm, &alg, nm->start, nm->length);
+
+    } else {
+        switch (key->raw.length) {
+        case 16:
+        case 24:
+        case 32:
+            nm = &njs_webcrypto_alg_aes_name
+                    [type - NJS_ALGORITHM_AES_GCM][(key->raw.length - 16) / 8];
+            (void) njs_vm_value_string_set(vm, &alg, nm->start, nm->length);
+            break;
+
+        default:
+            njs_value_undefined_set(&alg);
+            break;
+        }
+    }
+
+    ret = njs_key_ops(vm, &ops, key->usage);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    njs_value_boolean_set(&extractable, key->extractable);
+
+    ret = njs_vm_object_alloc(vm, retval, &string_kty, &oct_str, &string_k,
+                               &k, &key_ops, &ops, &string_ext, &extractable,
+                               NULL);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    if (njs_is_defined(&alg)) {
+        ret = njs_value_property_set(vm, retval, njs_value_arg(&string_alg),
+                                     &alg);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
 njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused)
 {
@@ -2081,6 +2181,17 @@ njs_ext_export_key(njs_vm_t *vm, njs_val
 
             break;
 
+        case NJS_ALGORITHM_AES_GCM:
+        case NJS_ALGORITHM_AES_CTR:
+        case NJS_ALGORITHM_AES_CBC:
+        case NJS_ALGORITHM_HMAC:
+            ret = njs_export_jwk_oct(vm, key, &value);
+            if (njs_slow_path(ret != NJS_OK)) {
+                goto fail;
+            }
+
+            break;
+
         default:
             break;
         }
@@ -2175,9 +2286,13 @@ njs_ext_export_key(njs_vm_t *vm, njs_val
             break;
         }
 
-        njs_internal_error(vm, "exporting as \"%V\" fmt is not implemented",
-                           njs_format_string(fmt));
-        goto fail;
+        ret = njs_vm_value_array_buffer_set(vm, &value, key->raw.start,
+                                            key->raw.length);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        break;
     }
 
     return njs_webcrypto_result(vm, &value, NJS_OK);
@@ -2386,6 +2501,57 @@ njs_ext_generate_key(njs_vm_t *vm, njs_v
 
         break;
 
+    case NJS_ALGORITHM_AES_GCM:
+    case NJS_ALGORITHM_AES_CTR:
+    case NJS_ALGORITHM_AES_CBC:
+    case NJS_ALGORITHM_HMAC:
+
+        if (alg->type == NJS_ALGORITHM_HMAC) {
+            ret = njs_algorithm_hash(vm, aobject, &key->hash);
+            if (njs_slow_path(ret == NJS_ERROR)) {
+                goto fail;
+            }
+
+            key->raw.length = EVP_MD_size(njs_algorithm_hash_digest(key->hash));
+
+        } else {
+            ret = njs_value_property(vm, aobject, njs_value_arg(&string_length),
+                                     &value);
+            if (njs_slow_path(ret == NJS_ERROR)) {
+                goto fail;
+            }
+
+            key->raw.length = njs_number(&value) / 8;
+
+            if (key->raw.length != 16
+                && key->raw.length != 24
+                && key->raw.length != 32)
+            {
+                njs_type_error(vm, "length for \"%V\" key should be one of "
+                               "128, 192, 256", njs_algorithm_string(alg));
+                goto fail;
+            }
+        }
+
+        key->raw.start = njs_mp_alloc(njs_vm_memory_pool(vm), key->raw.length);
+        if (njs_slow_path(key->raw.start == NULL)) {
+            njs_memory_error(vm);
+            goto fail;
+        }
+
+        if (RAND_bytes(key->raw.start, key->raw.length) <= 0) {
+            njs_webcrypto_error(vm, "RAND_bytes() failed");
+            goto fail;
+        }
+
+        ret = njs_vm_external_create(vm, &value,
+                                     njs_webcrypto_crypto_key_proto_id, key, 0);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        break;
+
     default:
         njs_internal_error(vm, "not implemented generateKey"
                            "algorithm: \"%V\"", njs_algorithm_string(alg));
@@ -2901,6 +3067,124 @@ fail:
 
 
 static njs_int_t
+njs_import_jwk_oct(njs_vm_t *vm, njs_value_t *jwk, njs_webcrypto_key_t *key)
+{
+    size_t                 size;
+    unsigned               usage;
+    njs_int_t              ret;
+    njs_str_t              *a, alg, b64;
+    njs_value_t            value;
+    njs_webcrypto_alg_t    type;
+    njs_webcrypto_entry_t  *w;
+
+    static njs_webcrypto_entry_t hashes[] = {
+        { njs_str("HS1"), NJS_HASH_SHA1 },
+        { njs_str("HS256"), NJS_HASH_SHA256 },
+        { njs_str("HS384"), NJS_HASH_SHA384 },
+        { njs_str("HS512"), NJS_HASH_SHA512 },
+        { njs_null_str, 0 }
+    };
+
+    ret = njs_value_property(vm, jwk, njs_value_arg(&string_k), &value);
+    if (njs_slow_path(ret == NJS_ERROR)) {
+        return NJS_ERROR;
+    }
+
+    if (!njs_value_is_string(&value)) {
+        njs_type_error(vm, "Invalid JWK oct key");
+        return NJS_ERROR;
+    }
+
+    njs_string_get(&value, &b64);
+
+    (void) njs_decode_base64url_length(&b64, &key->raw.length);
+
+    key->raw.start = njs_mp_alloc(njs_vm_memory_pool(vm), key->raw.length);
+    if (njs_slow_path(key->raw.start == NULL)) {
+        njs_memory_error(vm);
+        return NJS_ERROR;
+    }
+
+    njs_decode_base64url(&key->raw, &b64);
+
+    ret = njs_value_property(vm, jwk, njs_value_arg(&string_alg), &value);
+    if (njs_slow_path(ret == NJS_ERROR)) {
+        return NJS_ERROR;
+    }
+
+    size = 16;
+
+    if (njs_value_is_string(&value)) {
+        njs_string_get(&value, &alg);
+
+        if (key->alg->type == NJS_ALGORITHM_HMAC) {
+            for (w = &hashes[0]; w->name.length != 0; w++) {
+                if (njs_strstr_eq(&alg, &w->name)) {
+                    key->hash = w->value;
+                    goto done;
+                }
+            }
+
+        } else {
+            type = key->alg->type;
+            a = &njs_webcrypto_alg_aes_name[type - NJS_ALGORITHM_AES_GCM][0];
+            for (; a->length != 0; a++) {
+                if (njs_strstr_eq(&alg, a)) {
+                    goto done;
+                }
+
+                size += 8;
+            }
+        }
+
+        njs_type_error(vm, "unexpected \"alg\" value \"%V\" for JWK key", &alg);
+        return NJS_ERROR;
+    }
+
+done:
+
+    if (key->alg->type != NJS_ALGORITHM_HMAC) {
+        if (key->raw.length != size) {
+            njs_type_error(vm, "key size and \"alg\" value \"%V\" mismatch",
+                           &alg);
+            return NJS_ERROR;
+        }
+    }
+
+    ret = njs_value_property(vm, jwk, njs_value_arg(&key_ops), &value);
+    if (njs_slow_path(ret == NJS_ERROR)) {
+        return NJS_ERROR;
+    }
+
+    if (njs_is_defined(&value)) {
+        ret = njs_key_usage(vm, &value, &usage);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+
+        if ((key->usage & usage) != key->usage) {
+            njs_type_error(vm, "Key operations and usage mismatch");
+            return NJS_ERROR;
+        }
+    }
+
+    if (key->extractable) {
+        ret = njs_value_property(vm, jwk, njs_value_arg(&string_ext), &value);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            return NJS_ERROR;
+        }
+
+        if (njs_is_defined(&value) && !njs_value_bool(&value)) {
+            njs_type_error(vm, "JWK oct is not extractable");
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
 njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused)
 {
@@ -3057,6 +3341,12 @@ njs_ext_import_key(njs_vm_t *vm, njs_val
                 goto fail;
             }
 
+        } else if (njs_strstr_eq(&kty, &njs_str_value("oct"))) {
+            ret = njs_import_jwk_oct(vm, jwk, key);
+            if (njs_slow_path(ret != NJS_OK)) {
+                goto fail;
+            }
+
         } else {
             njs_type_error(vm, "invalid JWK key type: %V", &kty);
             goto fail;
@@ -3182,34 +3472,54 @@ njs_ext_import_key(njs_vm_t *vm, njs_val
         break;
 
     case NJS_ALGORITHM_HMAC:
-        ret = njs_algorithm_hash(vm, options, &key->hash);
-        if (njs_slow_path(ret == NJS_ERROR)) {
-            goto fail;
+        if (fmt == NJS_KEY_FORMAT_RAW) {
+            ret = njs_algorithm_hash(vm, options, &key->hash);
+            if (njs_slow_path(ret == NJS_ERROR)) {
+                goto fail;
+            }
+
+            key->raw = key_data;
+
+        } else {
+            /* NJS_KEY_FORMAT_JWK. */
+
+            ret = njs_algorithm_hash(vm, options, &hash);
+            if (njs_slow_path(ret == NJS_ERROR)) {
+                goto fail;
+            }
+
+            if (key->hash != NJS_HASH_UNSET && key->hash != hash) {
+                njs_type_error(vm, "HMAC JWK hash mismatch");
+                goto fail;
+            }
         }
 
-        key->raw = key_data;
         break;
 
     case NJS_ALGORITHM_AES_GCM:
     case NJS_ALGORITHM_AES_CTR:
     case NJS_ALGORITHM_AES_CBC:
-        switch (key_data.length) {
-        case 16:
-        case 24:
-        case 32:
-            break;
-
-        default:
-            njs_type_error(vm, "Invalid key length");
-            goto fail;
+        if (fmt == NJS_KEY_FORMAT_RAW) {
+            switch (key_data.length) {
+            case 16:
+            case 24:
+            case 32:
+                break;
+
+            default:
+                njs_type_error(vm, "AES Invalid key length");
+                goto fail;
+            }
+
+            key->raw = key_data;
         }
 
-        /* Fall through. */
+        break;
 
     case NJS_ALGORITHM_PBKDF2:
     case NJS_ALGORITHM_HKDF:
+    default:
         key->raw = key_data;
-    default:
         break;
     }
 
@@ -3869,6 +4179,11 @@ njs_key_usage(njs_vm_t *vm, njs_value_t 
     njs_int_t            ret;
     njs_iterator_args_t  args;
 
+    if (!njs_value_is_object(value)) {
+        njs_type_error(vm, "\"keyUsages\" argument must be an Array");
+        return NJS_ERROR;
+    }
+
     ret = njs_object_length(vm, value, &length);
     if (njs_slow_path(ret != NJS_OK)) {
         return NJS_ERROR;
diff -r 0681bf662222 -r 2e3bbe8743af test/ts/test.ts
--- a/test/ts/test.ts	Wed Jan 04 17:49:22 2023 -0800
+++ b/test/ts/test.ts	Wed Jan 04 18:07:30 2023 -0800
@@ -188,6 +188,14 @@ async function crypto_object(keyData: Ar
                                                 modulusLength: 2048,
                                                 publicExponent: new Uint8Array([1, 0, 1])},
                                                 true, ['sign', 'verify']);
+
+    let hkey = await crypto.subtle.generateKey({name: "HMAC",
+                                                hash: "SHA-384"},
+                                                true, ['sign', 'verify']);
+
+    let akey = await crypto.subtle.generateKey({name: "AES-GCM",
+                                                length: 256},
+                                                true, ['encrypt', 'decrypt']);
 }
 
 function buffer(b: Buffer) {
diff -r 0681bf662222 -r 2e3bbe8743af test/webcrypto/export.t.js
--- a/test/webcrypto/export.t.js	Wed Jan 04 17:49:22 2023 -0800
+++ b/test/webcrypto/export.t.js	Wed Jan 04 18:07:30 2023 -0800
@@ -17,6 +17,12 @@ async function load_key(params) {
         return params.generate_keys.keys[type];
     }
 
+    if (params.generate_key) {
+        return await crypto.subtle.generateKey(params.generate_key.alg,
+                                               params.generate_key.extractable,
+                                               params.generate_key.usage);
+    }
+
     return await crypto.subtle.importKey(params.key.fmt,
                                          params.key.key,
                                          params.key.alg,
@@ -43,6 +49,7 @@ async function test(params) {
     }
 
     if (!params.generate_keys
+        && !params.generate_key
         && (exp.startsWith && !exp.startsWith("ArrayBuffer:")))
     {
         /* Check that exported key can be imported back. */
@@ -73,6 +80,9 @@ function p(args, default_opts) {
     case "jwk":
         key = load_jwk(params.key.key);
         break;
+    case "raw":
+        key = Buffer.from(params.key.key, "base64url");
+        break;
     default:
         throw Error("Unknown encoding key format");
     }
@@ -291,8 +301,178 @@ let ec_tsuite = {
         expected: { kty: "EC", ext: true, key_ops: [ "sign" ], crv: "P-384" } },
 ]};
 
+function validate_hmac_jwk(exp, params) {
+    let hash = params.generate_key.alg.hash;
+    let expected_len = Number(hash.slice(2)) / 8 * (4 / 3);
+    expected_len = Math.round(expected_len);
+
+    validate_property(exp, 'k', expected_len);
+
+    return true;
+}
+
+let hmac_tsuite = {
+    name: "HMAC exporting",
+    skip: () => (!has_fs() || !has_webcrypto()),
+    T: test,
+    prepare_args: p,
+    opts: {
+        key: { fmt: "raw",
+               key: "c2VjcmV0LUtleTE",
+               alg: { name: "HMAC", hash: "SHA-256" },
+               extractable: true,
+               usage: [ "sign", "verify" ] },
+        export: { fmt: "jwk" },
+        expected: { kty: "oct", ext: true },
+    },
+
+    tests: [
+      { expected: { key_ops: [ "sign", "verify" ],
+                    alg: "HS256",
+                    k: "c2VjcmV0LUtleTE" } },
+      { export: { fmt: "raw" },
+        expected: "ArrayBuffer:c2VjcmV0LUtleTE" },
+      { export: { fmt: "spki" },
+        exception: "TypeError: unsupported key fmt \"spki\" for \"HMAC\"" },
+      { export: { fmt: "pksc8" },
+        exception: "TypeError: unsupported key fmt \"pksc8\" for \"HMAC\"" },
+      { key: { key: "cDBzc3dE",
+               alg: { hash: "SHA-384" } },
+        expected: { key_ops: [ "sign", "verify" ],
+                    alg: "HS384",
+                    k: "cDBzc3dE" } },
+      { key: { extractable: false },
+        exception: "TypeError: provided key cannot be extracted" },
+
+      { key: { fmt: "jwk",
+               key: { kty: "oct", ext: true, k: "c2VjcmV0LUtleTE", alg: "HS256" } },
+        expected: { key_ops: [ "sign", "verify" ],
+                    alg: "HS256",
+                    k: "c2VjcmV0LUtleTE" } },
+      { key: { fmt: "jwk",
+               alg: { hash: "SHA-512" },
+               key: { kty: "oct", ext: true, k: "c2VjcmV0LUtleTE", alg: "HS512" } },
+        expected: { key_ops: [ "sign", "verify" ],
+                    alg: "HS512",
+                    k: "c2VjcmV0LUtleTE" } },
+      { key: { fmt: "jwk",
+               key: { kty: "oct", ext: true, k: "c2VjcmV0LUtleTE", alg: "HS256" },
+               alg: { hash: "SHA-384" } },
+        exception: "TypeError: HMAC JWK hash mismatch" },
+
+      { generate_key: { alg: { name: "HMAC",
+                               hash: "SHA-256" },
+                        extractable: true,
+                        usage: [ "sign", "verify" ] },
+        check: validate_hmac_jwk,
+        expected: { key_ops: [ "sign", "verify" ], alg: "HS256" } },
+      { generate_key: { alg: { name: "HMAC",
+                               hash: "SHA-1" },
+                        extractable: true,
+                        usage: [ "verify" ] },
+        check: validate_hmac_jwk,
+        expected: { key_ops: [ "verify" ], alg: "HS1" } },
+      { generate_key: { alg: { name: "HMAC",
+                               hash: "SHA-384" },
+                        extractable: true,
+                        usage: [ "sign" ] },
+        check: validate_hmac_jwk,
+        expected: { key_ops: [ "sign" ], alg: "HS384" } },
+      { generate_key: { alg: { name: "HMAC",
+                               hash: "SHA-512" },
+                        extractable: true,
+                        usage: [ "sign" ] },
+        check: validate_hmac_jwk,
+        expected: { key_ops: [ "sign" ], alg: "HS512" } },
+]};
+
+function validate_aes_jwk(exp, params) {
+    let expected_len = params.generate_key.alg.length;
+    expected_len = expected_len / 8 * (4 / 3);
+    expected_len = Math.round(expected_len);
+
+    validate_property(exp, 'k', expected_len);
+
+    return true;
+}
+
+let aes_tsuite = {
+    name: "AES exporting",
+    skip: () => (!has_fs() || !has_webcrypto()),
+    T: test,
+    prepare_args: p,
+    opts: {
+        key: { fmt: "raw",
+               key: "ABEiMwARIjMAESIzABEiMw",
+               alg: { name: "AES-GCM" },
+               extractable: true,
+               usage: [ "encrypt" ] },
+        export: { fmt: "jwk" },
+        expected: { kty: "oct", ext: true },
+    },
+
+    tests: [
+      { expected: { key_ops: [ "encrypt" ],
+                    alg: "A128GCM",
+                    k: "ABEiMwARIjMAESIzABEiMw" } },
+      { export: { fmt: "raw" },
+        expected: "ArrayBuffer:ABEiMwARIjMAESIzABEiMw" },
+      { key: { key: "ABEiMwARIjMAESIzABEiMwARIjMAESIz",
+               alg: { name: "AES-CBC" },
+               usage: [ "decrypt" ] },
+        expected: { key_ops: [ "decrypt" ],
+                    alg: "A192CBC",
+                    k: "ABEiMwARIjMAESIzABEiMwARIjMAESIz" } },
+      { key: { key: "ABEiMwARIjMAESIzABEiMwARIjMAESIz",
+               alg: { name: "AES-CBC" },
+               usage: [ "decrypt" ] },
+        export: { fmt: "raw" },
+        expected: "ArrayBuffer:ABEiMwARIjMAESIzABEiMwARIjMAESIz" },
+      { key: { key: "ABEiMwARIjMAESIzABEiMwARIjMAESIzABEiMwARIjM",
+               alg: { name: "AES-CTR" },
+               usage: [ "decrypt" ] },
+        expected: { key_ops: [ "decrypt" ],
+                    alg: "A256CTR",
+                    k: "ABEiMwARIjMAESIzABEiMwARIjMAESIzABEiMwARIjM" } },
+      { key: { key: "ABEiMwARIjMAESIzABEiMwARIjMAESIzABEiMwARIjM",
+               alg: { name: "AES-CTR" },
+               usage: [ "decrypt" ] },
+        export: { fmt: "raw" },
+        expected: "ArrayBuffer:ABEiMwARIjMAESIzABEiMwARIjMAESIzABEiMwARIjM" },
+
+      { generate_key: { alg: { name: "AES-GCM", length: 128 },
+                        extractable: true,
+                        usage: [ "encrypt" ] },
+        check: validate_aes_jwk,
+        expected: { key_ops: [ "encrypt" ], alg: "A128GCM" } },
+      { generate_key: { alg: { name: "AES-CTR", length: 192 },
+                        extractable: true,
+                        usage: [ "decrypt" ] },
+        check: validate_aes_jwk,
+        expected: { key_ops: [ "decrypt" ], alg: "A192CTR" } },
+      { generate_key: { alg: { name: "AES-CBC", length: 256 },
+                        extractable: true,
+                        usage: [ "decrypt" ] },
+        check: validate_aes_jwk,
+        expected: { key_ops: [ "decrypt" ], alg: "A256CBC" } },
+      { generate_key: { alg: { name: "AES-GCM", length: 128 },
+                        extractable: false,
+                        usage: [ "decrypt" ] },
+        exception: "TypeError: provided key cannot be extracted" },
+      { generate_key: { alg: { name: "AES-GCM" },
+                        extractable: false,
+                        usage: [ "decrypt" ] },
+        exception: "TypeError: length for \"AES-GCM\" key should be one of 128, 192, 256" },
+      { generate_key: { alg: { name: "AES-GCM", length: 25 },
+                        extractable: false,
+                        usage: [ "decrypt" ] },
+        exception: "TypeError: length for \"AES-GCM\" key should be one of 128, 192, 256" },
+]};
+
 run([
     rsa_tsuite,
     ec_tsuite,
+    hmac_tsuite,
+    aes_tsuite,
 ])
 .then($DONE, $DONE);
diff -r 0681bf662222 -r 2e3bbe8743af test/webcrypto/sign.t.js
--- a/test/webcrypto/sign.t.js	Wed Jan 04 17:49:22 2023 -0800
+++ b/test/webcrypto/sign.t.js	Wed Jan 04 18:07:30 2023 -0800
@@ -180,6 +180,25 @@ let hmac_tsuite = {
           expected: "0540c587e7ee607fb4fd5e814438ed50f261c244" },
         { sign_alg: { name: "ECDSA" }, exception: "TypeError: cannot sign using \"HMAC\" with \"ECDSA\" key" },
 
+        { sign_key: { fmt: "jwk",
+                      key: { kty: "oct",
+                             alg: "HS256",
+                             k: "c2VjcmV0S0VZ" } },
+          expected: "76d4f1b22d7544c34e86380c9ab7c756311810dc31e4af3b705045d263db1212" },
+        { sign_key: { fmt: "jwk",
+                      key: { kty: "oct",
+                             alg: "HS256",
+                             key_ops: [ "sign" ],
+                             k: "c2VjcmV0S0VZ" } },
+          verify: true,
+          expected: true },
+        { sign_key: { fmt: "jwk",
+                      key: { kty: "oct",
+                             alg: "HS256",
+                             key_ops: [ "verify" ],
+                             k: "c2VjcmV0S0VZ" } },
+          exception: "TypeError: Key operations and usage mismatch" },
+
         { verify: true, expected: true },
         { verify: true, import_alg: { hash: "SHA-384" }, expected: true },
         { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
diff -r 0681bf662222 -r 2e3bbe8743af ts/njs_webcrypto.d.ts
--- a/ts/njs_webcrypto.d.ts	Wed Jan 04 17:49:22 2023 -0800
+++ b/ts/njs_webcrypto.d.ts	Wed Jan 04 18:07:30 2023 -0800
@@ -72,11 +72,14 @@ type ImportAlgorithm =
 
 type GenerateAlgorithm =
     | RsaHashedKeyGenParams
-    | EcKeyGenParams;
+    | EcKeyGenParams
+    | HmacKeyGenParams
+    | AesKeyGenParams;
 
 type JWK =
     | { kty: "RSA"; }
-    | { kty: "EC"; };
+    | { kty: "EC"; }
+    | { kty: "oct"; };
 
 type KeyData =
     | NjsStringOrBuffer
@@ -230,7 +233,8 @@ interface SubtleCrypto {
               key: CryptoKey): Promise<ArrayBuffer|Object>;
 
     /**
-     * Generates a keypair for asymmetric algorithms.
+     * Generates a key for symmetric algorithms or a keypair
+     *  for asymmetric algorithms.
      *
      * @since 0.7.10
      * @param algorithm Dictionary object defining the type of key to generate
@@ -242,7 +246,7 @@ interface SubtleCrypto {
      */
     generateKey(algorithm: GenerateAlgorithm,
                 extractable: boolean,
-                usage: Array<string>): Promise<CryptoKeyPair>;
+                usage: Array<string>): Promise<CryptoKey|CryptoKeyPair>;
 
     /**
      * Generates a digital signature.


More information about the nginx-devel mailing list