Repository: nginx/njs (c)

Issue:
HMAC not implemented in crypto.subtle.deriveKey
### Describe the bug

`deriveKey` only accepts AES options for `derivedKeyAlgorithm`.

- [x] The bug is reproducible with the latest version of njs.
- [x] I minimized the code and NGINX configuration to the smallest
possible to reproduce the issue.

### To reproduce

Steps to reproduce the behavior:

- JS script
```js
async function main() {
    const c = global.crypto;
    const algorithm = {
        name: "HKDF",
        hash: "SHA-256",
        salt: c.getRandomValues(new Int8Array(32)),
        info: Buffer.alloc(1).buffer
    };
    const key = await global.crypto.subtle.importKey(
        'raw',
        c.getRandomValues(new Int8Array(32)),
        'HKDF',
        false,
        ['deriveKey']
    );
    const derivedKeyAlgorithm = {
        name: "HMAC",
        hash: "SHA-256",
        length: 256
    };
    const keyUsages = ['sign', 'verify'];
    c.subtle.deriveKey(algorithm, key, derivedKeyAlgorithm, false, keyUsages);
}
main();
```

- `njs` output
```
Error: unhandled promise rejection: InternalError: not implemented deriveKey: "HMAC"
```

- With optional `length` property omitted:
```
Error: unhandled promise rejection: TypeError: derivedKeyAlgorithm.length is not provided
```

### Expected behavior

Exit status 0, as when `derivedKeyAlgorithm.name = "AES-GCM"` and `keyUsages = ['encrypt', 'decrypt']`.

### Your environment

```
nginx-module-njs.aarch64             1.28.0+0.8.10-1.amzn2023.ngx       @nginx-stable
```

### Additional context

[njs/external/njs_webcrypto_module.c:1716-1719](https://github.com/nginx/njs/blob/e3cfb4f70e203866c1bd06e5fb28fcdc7dd967f8/external/njs_webcrypto_module.c#L1716-L1719) is unreachable because of [njs/external/njs_webcrypto_module.c:1522-1539](https://github.com/nginx/njs/blob/e3cfb4f70e203866c1bd06e5fb28fcdc7dd967f8/external/njs_webcrypto_module.c#L1522-L1539).


Discussion:

Patch:
diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c
--- a/external/njs_webcrypto_module.c
+++ b/external/njs_webcrypto_module.c
@@ -1532,6 +1532,9 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 
             break;
 
+        case NJS_ALGORITHM_HMAC:
+            break;
+
         default:
             njs_vm_internal_error(vm, "not implemented deriveKey: \"%V\"",
                                   njs_algorithm_string(dalg));
@@ -1719,6 +1722,7 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
             }
         }
 
+        dkey->extractable = njs_value_bool(njs_arg(args, nargs, 4));
         dkey->u.s.raw.start = k;
         dkey->u.s.raw.length = length;
 
diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c
--- a/external/qjs_webcrypto_module.c
+++ b/external/qjs_webcrypto_module.c
@@ -1756,6 +1756,9 @@ qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc,
 
             break;
 
+        case QJS_ALGORITHM_HMAC:
+            break;
+
         default:
             JS_ThrowTypeError(cx, "not implemented deriveKey: \"%s\"",
                               qjs_algorithm_string(dalg));
@@ -1945,6 +1948,7 @@ qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc,
             }
         }
 
+        dkey->extractable = JS_ToBool(cx, argv[3]);
         dkey->u.s.raw.start = k;
         dkey->u.s.raw.length = length;
 
diff --git a/src/njs.h b/src/njs.h
--- a/src/njs.h
+++ b/src/njs.h
@@ -11,8 +11,8 @@
 
 #include <njs_auto_config.h>
 
-#define NJS_VERSION                 "0.9.0"
-#define NJS_VERSION_NUMBER          0x000900
+#define NJS_VERSION                 "0.9.1"
+#define NJS_VERSION_NUMBER          0x000901
 
 
 #include <string.h>


Test patch:
diff --git a/test/webcrypto/derive.t.mjs b/test/webcrypto/derive.t.mjs
--- a/test/webcrypto/derive.t.mjs
+++ b/test/webcrypto/derive.t.mjs
@@ -3,6 +3,16 @@ includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, runTsuite.js, webCr
 flags: [async]
 ---*/
 
+function has_usage(usage, x) {
+    for (let i = 0; i < usage.length; i++) {
+        if (x === usage[i]) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
 async function test(params) {
     let r;
     let encoder = new TextEncoder();
@@ -12,10 +22,21 @@ async function test(params) {
     if (params.derive === "key") {
         let key = await crypto.subtle.deriveKey(params.algorithm, keyMaterial,
                                                 params.derivedAlgorithm,
-                                                true, [ "encrypt", "decrypt" ]);
+                                                params.extractable, params.usage);
+
+        if (key.extractable !== params.extractable) {
+            throw Error(`${params.algorithm.name} failed extractable ${params.extractable} vs ${key.extractable}`);
+        }
+
+        if (has_usage(params.usage, "encrypt")) {
+            r = await crypto.subtle.encrypt(params.derivedAlgorithm, key,
+                                            encoder.encode(params.text));
+
+        } else if (has_usage(params.usage, "sign")) {
+            r = await crypto.subtle.sign(params.derivedAlgorithm, key,
+                                         encoder.encode(params.text));
+        }
 
-        r = await crypto.subtle.encrypt(params.derivedAlgorithm, key,
-                                        encoder.encode(params.text));
     } else {
 
         r = await crypto.subtle.deriveBits(params.algorithm, keyMaterial, params.length);
@@ -63,11 +84,14 @@ let derive_tsuite = {
           name: "AES-GCM",
           length: 256,
           iv: "55667788556677885566778855667788"
-        }
+        },
+        extractable: true,
+        usage: [ "encrypt", "decrypt" ]
     },
 
     tests: [
         { expected: "e7b55c9f9fda69b87648585f76c58109174aaa400cfa" },
+        { extractable: false, expected: "e7b55c9f9fda69b87648585f76c58109174aaa400cfa" },
         { pass: "pass2", expected: "e87d1787f2807ea0e1f7e1cb265b23004c575cf2ad7e" },
         { algorithm: { iterations: 10000 }, expected: "5add0059931ed1db1ca24c26dbe4de5719c43ed18a54" },
         { algorithm: { hash: "SHA-512" }, expected: "544d64e5e246fdd2ba290ea932b2d80ef411c76139f4" },
@@ -92,6 +116,10 @@ let derive_tsuite = {
 
         { algorithm: { name: "HKDF" }, optional: true,
           expected: "18ea069ee3317d2db02e02f4a228f50dc80d9a2396e6" },
+        { algorithm: { name: "HKDF" },
+          derivedAlgorithm: { name: "HMAC", hash: "SHA-256", length: 256 },
+          usage: [ "sign", "verify" ], optional: true,
+          expected: "0b06bd37de54c08cedde2cbb649d6f26d066acfd51717d83b52091e2ae6829c2" },
         { derive: "bits", algorithm: { name: "HKDF" }, optional: true,
           expected: "e089c7491711306c69e077aa19fae6bfd2d4a6d240b0d37317d50472d7291a3e" },
 ]};