[njs] QuickJS: added WebCrypto module.

noreply at nginx.com noreply at nginx.com
Sat Jan 25 02:50:02 UTC 2025


details:   https://github.com/nginx/njs/commit/75ca26f8347123c3163439a3652c1218bcb3bfa7
branches:  master
commit:    75ca26f8347123c3163439a3652c1218bcb3bfa7
user:      Dmitry Volyntsev <xeioex at nginx.com>
date:      Thu, 9 Jan 2025 19:25:58 -0800
description:
QuickJS: added WebCrypto module.


---
 auto/qjs_modules                |    8 +
 external/qjs_webcrypto_module.c | 4886 +++++++++++++++++++++++++++++++++++++++
 nginx/config                    |    4 +
 nginx/ngx_http_js_module.c      |    3 +
 nginx/ngx_js.h                  |    1 +
 nginx/ngx_stream_js_module.c    |    3 +
 nginx/t/js_webcrypto.t          |   85 +
 nginx/t/stream_js_webcrypto.t   |   83 +
 src/qjs.c                       |   14 +
 src/qjs.h                       |   24 +-
 src/qjs_buffer.c                |   37 +-
 src/test/njs_unit_test.c        |   40 -
 test/webcrypto/sign.t.mjs       |    4 +-
 test/webcrypto/verify.t.mjs     |    4 +-
 14 files changed, 5127 insertions(+), 69 deletions(-)

diff --git a/auto/qjs_modules b/auto/qjs_modules
index e74679d3..50ce9476 100644
--- a/auto/qjs_modules
+++ b/auto/qjs_modules
@@ -13,6 +13,14 @@ njs_module_srcs=external/qjs_fs_module.c
 
 . auto/qjs_module
 
+if [ $NJS_OPENSSL = YES -a $NJS_HAVE_OPENSSL = YES ]; then
+	njs_module_name=qjs_webcrypto_module
+	njs_module_incs=
+	njs_module_srcs=external/qjs_webcrypto_module.c
+
+	. auto/qjs_module
+fi
+
 if [ $NJS_ZLIB = YES -a $NJS_HAVE_ZLIB = YES ]; then
     njs_module_name=qjs_zlib_module
     njs_module_incs=
diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c
new file mode 100644
index 00000000..22f5d108
--- /dev/null
+++ b/external/qjs_webcrypto_module.c
@@ -0,0 +1,4886 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) F5, Inc.
+ */
+
+
+#include <qjs.h>
+#include <njs_sprintf.h>
+#include "njs_openssl.h"
+
+typedef enum {
+    QJS_KEY_FORMAT_RAW          = 1 << 1,
+    QJS_KEY_FORMAT_PKCS8        = 1 << 2,
+    QJS_KEY_FORMAT_SPKI         = 1 << 3,
+    QJS_KEY_FORMAT_JWK          = 1 << 4,
+    QJS_KEY_FORMAT_UNKNOWN      = 1 << 5,
+} qjs_webcrypto_key_format_t;
+
+
+typedef enum {
+    QJS_KEY_JWK_KTY_RSA,
+    QJS_KEY_JWK_KTY_EC,
+    QJS_KEY_JWK_KTY_OCT,
+    QJS_KEY_JWK_KTY_UNKNOWN,
+} qjs_webcrypto_jwk_kty_t;
+
+
+typedef enum {
+    QJS_KEY_USAGE_DECRYPT       = 1 << 1,
+    QJS_KEY_USAGE_DERIVE_BITS   = 1 << 2,
+    QJS_KEY_USAGE_DERIVE_KEY    = 1 << 3,
+    QJS_KEY_USAGE_ENCRYPT       = 1 << 4,
+    QJS_KEY_USAGE_GENERATE_KEY  = 1 << 5,
+    QJS_KEY_USAGE_SIGN          = 1 << 6,
+    QJS_KEY_USAGE_VERIFY        = 1 << 7,
+    QJS_KEY_USAGE_WRAP_KEY      = 1 << 8,
+    QJS_KEY_USAGE_UNSUPPORTED   = 1 << 9,
+    QJS_KEY_USAGE_UNWRAP_KEY    = 1 << 10,
+} qjs_webcrypto_key_usage_t;
+
+
+typedef enum {
+    QJS_ALGORITHM_RSASSA_PKCS1_v1_5 = 0,
+    QJS_ALGORITHM_RSA_PSS,
+    QJS_ALGORITHM_RSA_OAEP,
+    QJS_ALGORITHM_HMAC,
+    QJS_ALGORITHM_AES_GCM,
+    QJS_ALGORITHM_AES_CTR,
+    QJS_ALGORITHM_AES_CBC,
+    QJS_ALGORITHM_ECDSA,
+    QJS_ALGORITHM_ECDH,
+    QJS_ALGORITHM_PBKDF2,
+    QJS_ALGORITHM_HKDF,
+    QJS_ALGORITHM_MAX,
+} qjs_webcrypto_alg_t;
+
+
+typedef enum {
+    QJS_HASH_UNSET = 0,
+    QJS_HASH_SHA1,
+    QJS_HASH_SHA256,
+    QJS_HASH_SHA384,
+    QJS_HASH_SHA512,
+    QJS_HASH_MAX,
+} qjs_webcrypto_hash_t;
+
+
+typedef struct {
+    njs_str_t                  name;
+    uintptr_t                  value;
+} qjs_webcrypto_entry_t;
+
+
+typedef struct {
+    qjs_webcrypto_alg_t        type;
+    unsigned                   usage;
+    unsigned                   fmt;
+    unsigned                   raw;
+} qjs_webcrypto_algorithm_t;
+
+
+typedef struct {
+    qjs_webcrypto_algorithm_t  *alg;
+    unsigned                   usage;
+    int                        extractable;
+
+    qjs_webcrypto_hash_t       hash;
+
+    union {
+        struct {
+            EVP_PKEY          *pkey;
+            int               privat;
+            int               curve;
+        } a;
+        struct {
+            njs_str_t         raw;
+        } s;
+    } u;
+
+} qjs_webcrypto_key_t;
+
+
+typedef int (*EVP_PKEY_cipher_init_t)(EVP_PKEY_CTX *ctx);
+typedef int (*EVP_PKEY_cipher_t)(EVP_PKEY_CTX *ctx, unsigned char *out,
+    size_t *outlen, const unsigned char *in, size_t inlen);
+
+static JSValue qjs_webcrypto_cipher(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int encrypt);
+static JSValue qjs_cipher_pkey(JSContext *cx, njs_str_t *data,
+    qjs_webcrypto_key_t *key, int encrypt);
+static JSValue qjs_cipher_aes_gcm(JSContext *cx, njs_str_t *data,
+    qjs_webcrypto_key_t *key, JSValue options, int encrypt);
+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);
+static JSValue qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int derive_key);
+static JSValue qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+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);
+static JSValue qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int verify);
+
+static JSValue qjs_webcrypto_key_to_string_tag(JSContext *cx,
+    JSValueConst this_val);
+static JSValue qjs_webcrypto_key_algorithm(JSContext *cx,
+    JSValueConst this_val);
+static JSValue qjs_webcrypto_key_extractable(JSContext *cx,
+    JSValueConst this_val);
+static JSValue qjs_webcrypto_key_type(JSContext *cx, JSValueConst this_val);
+static JSValue qjs_webcrypto_key_usages(JSContext *cx, JSValueConst this_val);
+
+static JSValue qjs_get_random_values(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+
+static JSValue qjs_webcrypto_key_make(JSContext *cx,
+  qjs_webcrypto_algorithm_t *alg, unsigned usage, int extractable);
+static void qjs_webcrypto_key_finalizer(JSRuntime *rt, JSValue val);
+
+static qjs_webcrypto_key_format_t qjs_key_format(JSContext *cx,
+    JSValueConst value);
+static qjs_webcrypto_jwk_kty_t qjs_jwk_kty(JSContext *cx, JSValueConst value);
+static qjs_webcrypto_algorithm_t *qjs_key_algorithm(JSContext *cx, JSValue val);
+static JSValue qjs_algorithm_curve(JSContext *cx, JSValue options, int *curve);
+static JSValue qjs_algorithm_hash(JSContext *cx, JSValue options,
+    qjs_webcrypto_hash_t *hash);
+static const EVP_MD *qjs_algorithm_hash_digest(qjs_webcrypto_hash_t hash);
+static njs_str_t *qjs_algorithm_curve_name(int curve);
+static const char *qjs_format_string(qjs_webcrypto_key_format_t fmt);
+static const char *qjs_algorithm_string(qjs_webcrypto_algorithm_t *algorithm);
+static const char *qjs_algorithm_hash_name(qjs_webcrypto_hash_t hash);
+static JSValue qjs_key_usage(JSContext *cx, JSValue value, unsigned *mask);
+static JSValue qjs_key_ops(JSContext *cx, unsigned mask);
+static JSValue qjs_webcrypto_result(JSContext *cx, JSValue result, int rc);
+static void qjs_webcrypto_error(JSContext *cx, const char *fmt, ...);
+
+static JSModuleDef *qjs_webcrypto_init(JSContext *cx, const char *name);
+
+
+static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = {
+
+#define qjs_webcrypto_algorithm(type, usage, fmt, raw)                       \
+    (uintptr_t) & (qjs_webcrypto_algorithm_t) { type, usage, fmt, raw }
+
+    {
+      njs_str("RSASSA-PKCS1-v1_5"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_RSASSA_PKCS1_v1_5,
+                              QJS_KEY_USAGE_SIGN |
+                              QJS_KEY_USAGE_VERIFY |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_PKCS8 |
+                              QJS_KEY_FORMAT_SPKI |
+                              QJS_KEY_FORMAT_JWK,
+                              0)
+    },
+
+    {
+      njs_str("RSA-PSS"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_RSA_PSS,
+                              QJS_KEY_USAGE_SIGN |
+                              QJS_KEY_USAGE_VERIFY |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_PKCS8 |
+                              QJS_KEY_FORMAT_SPKI |
+                              QJS_KEY_FORMAT_JWK,
+                              0)
+    },
+
+    {
+      njs_str("RSA-OAEP"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_RSA_OAEP,
+                              QJS_KEY_USAGE_ENCRYPT |
+                              QJS_KEY_USAGE_DECRYPT |
+                              QJS_KEY_USAGE_WRAP_KEY |
+                              QJS_KEY_USAGE_UNWRAP_KEY |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_PKCS8 |
+                              QJS_KEY_FORMAT_SPKI |
+                              QJS_KEY_FORMAT_JWK,
+                              0)
+    },
+
+    {
+      njs_str("HMAC"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_HMAC,
+                              QJS_KEY_USAGE_GENERATE_KEY |
+                              QJS_KEY_USAGE_SIGN |
+                              QJS_KEY_USAGE_VERIFY,
+                              QJS_KEY_FORMAT_RAW |
+                              QJS_KEY_FORMAT_JWK,
+                              1)
+    },
+
+    {
+      njs_str("AES-GCM"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_GCM,
+                              QJS_KEY_USAGE_ENCRYPT |
+                              QJS_KEY_USAGE_DECRYPT |
+                              QJS_KEY_USAGE_WRAP_KEY |
+                              QJS_KEY_USAGE_UNWRAP_KEY |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_RAW |
+                              QJS_KEY_FORMAT_JWK,
+                              1)
+    },
+
+    {
+      njs_str("AES-CTR"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_CTR,
+                              QJS_KEY_USAGE_ENCRYPT |
+                              QJS_KEY_USAGE_DECRYPT |
+                              QJS_KEY_USAGE_WRAP_KEY |
+                              QJS_KEY_USAGE_UNWRAP_KEY |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_RAW |
+                              QJS_KEY_FORMAT_JWK,
+                              1)
+    },
+
+    {
+      njs_str("AES-CBC"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_CBC,
+                              QJS_KEY_USAGE_ENCRYPT |
+                              QJS_KEY_USAGE_DECRYPT |
+                              QJS_KEY_USAGE_WRAP_KEY |
+                              QJS_KEY_USAGE_UNWRAP_KEY |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_RAW |
+                              QJS_KEY_FORMAT_JWK,
+                              1)
+    },
+
+    {
+      njs_str("ECDSA"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_ECDSA,
+                              QJS_KEY_USAGE_SIGN |
+                              QJS_KEY_USAGE_VERIFY |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_PKCS8 |
+                              QJS_KEY_FORMAT_SPKI |
+                              QJS_KEY_FORMAT_RAW |
+                              QJS_KEY_FORMAT_JWK,
+                              0)
+    },
+
+    {
+      njs_str("ECDH"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_ECDH,
+                              QJS_KEY_USAGE_DERIVE_KEY |
+                              QJS_KEY_USAGE_DERIVE_BITS |
+                              QJS_KEY_USAGE_GENERATE_KEY |
+                              QJS_KEY_USAGE_UNSUPPORTED,
+                              QJS_KEY_FORMAT_UNKNOWN,
+                              0)
+    },
+
+    {
+      njs_str("PBKDF2"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_PBKDF2,
+                              QJS_KEY_USAGE_DERIVE_KEY |
+                              QJS_KEY_USAGE_DERIVE_BITS,
+                              QJS_KEY_FORMAT_RAW,
+                              1)
+    },
+
+    {
+      njs_str("HKDF"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_HKDF,
+                              QJS_KEY_USAGE_DERIVE_KEY |
+                              QJS_KEY_USAGE_DERIVE_BITS,
+                              QJS_KEY_FORMAT_RAW,
+                              1)
+    },
+
+    {
+        njs_null_str,
+        0
+    }
+};
+
+
+static qjs_webcrypto_entry_t qjs_webcrypto_hash[] = {
+    { njs_str("SHA-256"), QJS_HASH_SHA256 },
+    { njs_str("SHA-384"), QJS_HASH_SHA384 },
+    { njs_str("SHA-512"), QJS_HASH_SHA512 },
+    { njs_str("SHA-1"), QJS_HASH_SHA1 },
+    { njs_null_str, 0 }
+};
+
+
+static qjs_webcrypto_entry_t qjs_webcrypto_curve[] = {
+    { njs_str("P-256"), NID_X9_62_prime256v1 },
+    { njs_str("P-384"), NID_secp384r1 },
+    { njs_str("P-521"), NID_secp521r1 },
+    { njs_null_str, 0 }
+};
+
+
+static qjs_webcrypto_entry_t qjs_webcrypto_format[] = {
+    { njs_str("raw"), QJS_KEY_FORMAT_RAW },
+    { njs_str("pkcs8"), QJS_KEY_FORMAT_PKCS8 },
+    { njs_str("spki"), QJS_KEY_FORMAT_SPKI },
+    { njs_str("jwk"), QJS_KEY_FORMAT_JWK },
+    { njs_null_str, QJS_KEY_FORMAT_UNKNOWN }
+};
+
+
+static qjs_webcrypto_entry_t qjs_webcrypto_jwk_kty[] = {
+    { njs_str("RSA"), QJS_KEY_JWK_KTY_RSA },
+    { njs_str("EC"), QJS_KEY_JWK_KTY_EC },
+    { njs_str("oct"), QJS_KEY_JWK_KTY_OCT },
+    { njs_null_str, QJS_KEY_JWK_KTY_UNKNOWN }
+};
+
+
+static qjs_webcrypto_entry_t qjs_webcrypto_usage[] = {
+    { njs_str("decrypt"), QJS_KEY_USAGE_DECRYPT },
+    { njs_str("deriveBits"), QJS_KEY_USAGE_DERIVE_BITS },
+    { njs_str("deriveKey"), QJS_KEY_USAGE_DERIVE_KEY },
+    { njs_str("encrypt"), QJS_KEY_USAGE_ENCRYPT },
+    { njs_str("sign"), QJS_KEY_USAGE_SIGN },
+    { njs_str("unwrapKey"), QJS_KEY_USAGE_UNWRAP_KEY },
+    { njs_str("verify"), QJS_KEY_USAGE_VERIFY },
+    { njs_str("wrapKey"), QJS_KEY_USAGE_WRAP_KEY },
+    { njs_null_str, 0 }
+};
+
+
+static qjs_webcrypto_entry_t qjs_webcrypto_alg_hash[] = {
+    { njs_str("RS1"), QJS_HASH_SHA1 },
+    { njs_str("RS256"), QJS_HASH_SHA256 },
+    { njs_str("RS384"), QJS_HASH_SHA384 },
+    { njs_str("RS512"), QJS_HASH_SHA512 },
+    { njs_str("PS1"), QJS_HASH_SHA1 },
+    { njs_str("PS256"), QJS_HASH_SHA256 },
+    { njs_str("PS384"), QJS_HASH_SHA384 },
+    { njs_str("PS512"), QJS_HASH_SHA512 },
+    { njs_str("RSA-OAEP"), QJS_HASH_SHA1 },
+    { njs_str("RSA-OAEP-256"), QJS_HASH_SHA256 },
+    { njs_str("RSA-OAEP-384"), QJS_HASH_SHA384 },
+    { njs_str("RSA-OAEP-512"), QJS_HASH_SHA512 },
+    { njs_null_str, 0 }
+};
+
+
+static njs_str_t
+    qjs_webcrypto_alg_name[QJS_ALGORITHM_HMAC + 1][QJS_HASH_MAX] = {
+    {
+        njs_null_str,
+        njs_str("RS1"),
+        njs_str("RS256"),
+        njs_str("RS384"),
+        njs_str("RS512"),
+    },
+
+    {
+        njs_null_str,
+        njs_str("PS1"),
+        njs_str("PS256"),
+        njs_str("PS384"),
+        njs_str("PS512"),
+    },
+
+    {
+        njs_null_str,
+        njs_str("RSA-OAEP"),
+        njs_str("RSA-OAEP-256"),
+        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 qjs_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,
+    },
+};
+
+
+static const JSCFunctionListEntry qjs_webcrypto_subtle[] = {
+    JS_CFUNC_DEF("importKey", 5, qjs_webcrypto_import_key),
+    JS_CFUNC_MAGIC_DEF("decrypt", 4, qjs_webcrypto_cipher, 0),
+    JS_CFUNC_MAGIC_DEF("deriveBits", 4, qjs_webcrypto_derive, 0),
+    JS_CFUNC_MAGIC_DEF("deriveKey", 4, qjs_webcrypto_derive, 1),
+    JS_CFUNC_DEF("digest", 3, qjs_webcrypto_digest),
+    JS_CFUNC_MAGIC_DEF("encrypt", 4, qjs_webcrypto_cipher, 1),
+    JS_CFUNC_DEF("exportKey", 3, qjs_webcrypto_export_key),
+    JS_CFUNC_DEF("generateKey", 3, qjs_webcrypto_generate_key),
+    JS_CFUNC_MAGIC_DEF("sign", 4, qjs_webcrypto_sign, 0),
+    JS_CFUNC_MAGIC_DEF("verify", 4, qjs_webcrypto_sign, 1),
+};
+
+
+static const JSCFunctionListEntry qjs_webcrypto_key_proto[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_webcrypto_key_to_string_tag,
+                   NULL),
+    JS_CGETSET_DEF("algorithm", qjs_webcrypto_key_algorithm, NULL),
+    JS_CGETSET_DEF("extractable", qjs_webcrypto_key_extractable, NULL),
+    JS_CGETSET_DEF("type", qjs_webcrypto_key_type, NULL),
+    JS_CGETSET_DEF("usages", qjs_webcrypto_key_usages, NULL),
+};
+
+
+static const JSCFunctionListEntry qjs_webcrypto_export[] = {
+    JS_CFUNC_DEF("getRandomValues", 1, qjs_get_random_values),
+    JS_OBJECT_DEF("subtle",
+                  qjs_webcrypto_subtle,
+                  njs_nitems(qjs_webcrypto_subtle),
+                  JS_PROP_CONFIGURABLE),
+};
+
+
+static JSClassDef qjs_webcrypto_key_class = {
+    "WebCryptoKey",
+    .finalizer = qjs_webcrypto_key_finalizer,
+};
+
+
+qjs_module_t  qjs_webcrypto_module = {
+    .name = "webcrypto",
+    .init = qjs_webcrypto_init,
+};
+
+
+static JSValue
+qjs_webcrypto_cipher(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int encrypt)
+{
+    unsigned                   mask;
+    JSValue                    ret, options;
+    njs_str_t                  data;
+    qjs_webcrypto_key_t        *key;
+    qjs_webcrypto_algorithm_t  *alg;
+
+    options = argv[0];
+    alg = qjs_key_algorithm(cx, options);
+    if (alg == NULL) {
+        goto fail;
+    }
+
+    key = JS_GetOpaque(argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (key == NULL) {
+        JS_ThrowTypeError(cx, "key is not a CryptoKey object");
+        goto fail;
+    }
+
+    mask = encrypt ? QJS_KEY_USAGE_ENCRYPT : QJS_KEY_USAGE_DECRYPT;
+    if ((key->usage & mask) != mask) {
+        JS_ThrowTypeError(cx, "key does not support %s operation",
+                          encrypt ? "encrypt" : "decrypt");
+        goto fail;
+    }
+
+    if (key->alg != alg) {
+        JS_ThrowTypeError(cx, "cannot %s use \"%s\" with \"%s\" key",
+                          encrypt ? "encrypt" : "decrypt",
+                          qjs_algorithm_string(key->alg),
+                          qjs_algorithm_string(alg));
+        goto fail;
+    }
+
+    ret = qjs_typed_array_data(cx, argv[2], &data);
+    if (JS_IsException(ret)) {
+        return ret;
+    }
+
+    switch (alg->type) {
+    case QJS_ALGORITHM_RSA_OAEP:
+        ret = qjs_cipher_pkey(cx, &data, key, encrypt);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_AES_GCM:
+        ret = qjs_cipher_aes_gcm(cx, &data, key, options, encrypt);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_AES_CTR:
+        ret = qjs_cipher_aes_ctr(cx, &data, key, options, encrypt);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_AES_CBC:
+    default:
+        ret = qjs_cipher_aes_cbc(cx, &data, key, options, encrypt);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+    }
+
+    return qjs_webcrypto_result(cx, ret, 0);
+
+fail:
+
+    return qjs_webcrypto_result(cx, JS_UNDEFINED, -1);
+}
+
+
+static JSValue
+qjs_cipher_pkey(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
+    int encrypt)
+{
+    int                     rc;
+    u_char                  *dst;
+    size_t                  outlen;
+    JSValue                 ret;
+    const EVP_MD            *md;
+    EVP_PKEY_CTX            *ctx;
+    EVP_PKEY_cipher_t       cipher;
+    EVP_PKEY_cipher_init_t  init;
+
+    ctx = EVP_PKEY_CTX_new(key->u.a.pkey, NULL);
+    if (ctx == NULL) {
+        qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed");
+        return JS_EXCEPTION;
+    }
+
+    if (encrypt) {
+        init = EVP_PKEY_encrypt_init;
+        cipher = EVP_PKEY_encrypt;
+
+    } else {
+        init = EVP_PKEY_decrypt_init;
+        cipher = EVP_PKEY_decrypt;
+    }
+
+    rc = init(ctx);
+    if (rc <= 0) {
+        qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt_init() failed",
+                            encrypt ? "en" : "de");
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    md = qjs_algorithm_hash_digest(key->hash);
+
+    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
+    EVP_PKEY_CTX_set_signature_md(ctx, md);
+    EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md);
+
+    rc = cipher(ctx, NULL, &outlen, data->start, data->length);
+    if (rc <= 0) {
+        qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt() failed",
+                            encrypt ? "en" : "de");
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    dst = js_malloc(cx, outlen);
+    if (dst == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    rc = cipher(ctx, dst, &outlen, data->start, data->length);
+    if (rc <= 0) {
+        qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt() failed",
+                            encrypt ? "en" : "de");
+        js_free(cx, dst);
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    ret = qjs_new_array_buffer(cx, dst, outlen);
+
+fail:
+
+    EVP_PKEY_CTX_free(ctx);
+
+    return ret;
+}
+
+
+static JSValue
+qjs_cipher_aes_gcm(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
+    JSValue options, int encrypt)
+{
+    int               len, outlen, dstlen;
+    u_char            *dst, *p;
+    JSValue           ret, value;
+    int64_t           taglen;
+    njs_str_t         iv, aad;
+    EVP_CIPHER_CTX    *ctx;
+    const EVP_CIPHER  *cipher;
+
+    switch (key->u.s.raw.length) {
+    case 16:
+        cipher = EVP_aes_128_gcm();
+        break;
+
+    case 24:
+        cipher = EVP_aes_192_gcm();
+        break;
+
+    case 32:
+        cipher = EVP_aes_256_gcm();
+        break;
+
+    default:
+        JS_ThrowTypeError(cx, "AES-GCM invalid key length");
+        return JS_EXCEPTION;
+    }
+
+    value = JS_GetPropertyStr(cx, options, "iv");
+    if (JS_IsException(value)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_IsUndefined(value)) {
+        JS_ThrowTypeError(cx, "AES-GCM algorithm.iv is not provided");
+        return JS_EXCEPTION;
+    }
+
+    ret = qjs_typed_array_data(cx, value, &iv);
+    if (JS_IsException(ret)) {
+        return JS_EXCEPTION;
+    }
+
+    JS_FreeValue(cx, value);
+
+    taglen = 128;
+
+    value = JS_GetPropertyStr(cx, options, "tagLength");
+    if (JS_IsException(value)) {
+        return JS_EXCEPTION;
+    }
+
+    if (!JS_IsUndefined(value)) {
+        if (JS_ToInt64(cx, &taglen, value) < 0) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    if (taglen != 32 && taglen != 64 && taglen != 96 && taglen != 104
+        && taglen != 112 && taglen != 120 && taglen != 128)
+    {
+        JS_ThrowTypeError(cx, "AES-GCM invalid tagLength");
+        return JS_EXCEPTION;
+    }
+
+    taglen /= 8;
+
+    if (!encrypt && data->length < (size_t) taglen) {
+        JS_ThrowTypeError(cx, "AES-GCM data is too short");
+        return JS_EXCEPTION;
+    }
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed");
+        return JS_EXCEPTION;
+    }
+
+    if (EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encrypt) <= 0) {
+        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        goto fail;
+    }
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.length, NULL) <= 0) {
+        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed");
+        goto fail;
+    }
+
+    if (EVP_CipherInit_ex(ctx, NULL, NULL, key->u.s.raw.start, iv.start,
+                          encrypt) <= 0)
+    {
+        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        goto fail;
+    }
+
+    if (!encrypt) {
+        if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, taglen,
+                                &data->start[data->length - taglen]) <= 0)
+        {
+            qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed");
+            goto fail;
+        }
+    }
+
+    aad.length = 0;
+
+    value = JS_GetPropertyStr(cx, options, "additionalData");
+    if (JS_IsException(value)) {
+        goto fail;
+    }
+
+    if (!JS_IsUndefined(value)) {
+        ret = qjs_typed_array_data(cx, value, &aad);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+    }
+
+    JS_FreeValue(cx, value);
+
+    if (aad.length != 0) {
+        if (EVP_CipherUpdate(ctx, NULL, &outlen, aad.start, aad.length) <= 0) {
+            qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
+                                encrypt ? "Encrypt" : "Decrypt");
+            goto fail;
+        }
+    }
+
+    dstlen = data->length + EVP_CIPHER_CTX_block_size(ctx) + taglen;
+    dst = js_malloc(cx, dstlen);
+    if (dst == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        goto fail;
+    }
+
+    if (EVP_CipherUpdate(ctx, dst, &outlen, data->start,
+                         data->length - (encrypt ? 0 : taglen)) <= 0)
+    {
+        qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        js_free(cx, dst);
+        goto fail;
+    }
+
+    p = &dst[outlen];
+    len = EVP_CIPHER_CTX_block_size(ctx);
+
+    if (EVP_CipherFinal_ex(ctx, p, &len) <= 0) {
+        qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        js_free(cx, dst);
+        goto fail;
+    }
+
+    outlen += len;
+    p += len;
+
+    if (encrypt) {
+        if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, taglen, p) <= 0) {
+            qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed");
+            js_free(cx, dst);
+            goto fail;
+        }
+
+        outlen += taglen;
+    }
+
+    ret = qjs_new_array_buffer(cx, dst, outlen);
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return ret;
+
+fail:
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return JS_EXCEPTION;
+}
+
+
+static int
+qjs_cipher_aes_ctr128(JSContext *cx, const EVP_CIPHER *cipher, u_char *key,
+    u_char *data, size_t dlen, u_char *counter, u_char *dst, int *olen,
+    int encrypt)
+{
+    int             len, outlen;
+    int             ret;
+    EVP_CIPHER_CTX  *ctx;
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed");
+        return -1;
+    }
+
+    ret = EVP_CipherInit_ex(ctx, cipher, NULL, key, counter, encrypt);
+    if (ret <= 0) {
+        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = -1;
+        goto fail;
+    }
+
+    ret = EVP_CipherUpdate(ctx, dst, &outlen, data, dlen);
+    if (ret <= 0) {
+        qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = -1;
+        goto fail;
+    }
+
+    ret = EVP_CipherFinal_ex(ctx, &dst[outlen], &len);
+    if (ret <= 0) {
+        qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = -1;
+        goto fail;
+    }
+
+    outlen += len;
+    *olen = outlen;
+
+    ret = 0;
+
+fail:
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return ret;
+}
+
+
+static BIGNUM *
+qjs_bn_counter128(njs_str_t *ctr, unsigned bits)
+{
+    unsigned  remainder, bytes;
+    uint8_t   buf[16];
+
+    remainder = bits % 8;
+
+    if (remainder == 0) {
+        bytes = bits / 8;
+
+        return BN_bin2bn(&ctr->start[ctr->length - bytes], bytes, NULL);
+    }
+
+    bytes = (bits + 7) / 8;
+
+    memcpy(buf, &ctr->start[ctr->length - bytes], bytes);
+
+    buf[0] &= ~(0xFF << remainder);
+
+    return BN_bin2bn(buf, bytes, NULL);
+}
+
+
+njs_inline unsigned
+qjs_ceiling_div(unsigned dend, unsigned dsor)
+{
+    return (dsor == 0) ? 0 : 1 + (dend - 1) / dsor;
+}
+
+
+static void
+qjs_counter128_reset(u_char *src, u_char *dst, unsigned bits)
+{
+    size_t    index;
+    unsigned  remainder, bytes;
+
+    bytes = bits / 8;
+    remainder = bits % 8;
+
+    memcpy(dst, src, 16);
+
+    index = 16 - bytes;
+
+    memset(&dst[index], 0, bytes);
+
+    if (remainder) {
+        dst[index - 1] &= 0xff << remainder;
+    }
+}
+
+
+static JSValue
+qjs_cipher_aes_ctr(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
+    JSValue options, int encrypt)
+{
+    int               len, len2;
+    u_char            *dst;
+    BIGNUM            *total, *blocks, *left, *ctr;
+    size_t            size1;
+    JSValue           ret, value;
+    int64_t           length;
+    njs_str_t         iv;
+    const EVP_CIPHER  *cipher;
+    u_char            iv2[16];
+
+    switch (key->u.s.raw.length) {
+    case 16:
+        cipher = EVP_aes_128_ctr();
+        break;
+
+    case 24:
+        cipher = EVP_aes_192_ctr();
+        break;
+
+    case 32:
+        cipher = EVP_aes_256_ctr();
+        break;
+
+    default:
+        JS_ThrowTypeError(cx, "AES-CTR invalid key length");
+        return JS_EXCEPTION;
+    }
+
+    value = JS_GetPropertyStr(cx, options, "counter");
+    if (JS_IsException(value)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_IsUndefined(value)) {
+        JS_ThrowTypeError(cx, "AES-CTR algorithm.counter is not provided");
+        return JS_EXCEPTION;
+    }
+
+    ret = qjs_typed_array_data(cx, value, &iv);
+    if (JS_IsException(ret)) {
+        return JS_EXCEPTION;
+    }
+
+    JS_FreeValue(cx, value);
+
+    if (iv.length != 16) {
+        JS_ThrowTypeError(cx, "AES-CTR algorithm.counter must be 16 bytes "
+                          "long");
+        return JS_EXCEPTION;
+    }
+
+    value = JS_GetPropertyStr(cx, options, "length");
+    if (JS_IsException(value)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_IsUndefined(value)) {
+        JS_ThrowTypeError(cx, "AES-CTR algorithm.length is not provided");
+        return JS_EXCEPTION;
+    }
+
+    if (JS_ToInt64(cx, &length, value) < 0) {
+        return JS_EXCEPTION;
+    }
+
+    if (length == 0 || length > 128) {
+        JS_ThrowTypeError(cx, "AES-CTR algorithm.length must be between "
+                          "1 and 128");
+        return JS_EXCEPTION;
+    }
+
+    ctr = NULL;
+    blocks = NULL;
+    left = NULL;
+
+    total = BN_new();
+    if (total == NULL) {
+        qjs_webcrypto_error(cx, "BN_new() failed");
+        return JS_EXCEPTION;
+    }
+
+    if (BN_lshift(total, BN_value_one(), length) != 1) {
+        qjs_webcrypto_error(cx, "BN_lshift() failed");
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    ctr = qjs_bn_counter128(&iv, length);
+    if (ctr == NULL) {
+        qjs_webcrypto_error(cx, "BN_bin2bn() failed");
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    blocks = BN_new();
+    if (blocks == NULL) {
+        qjs_webcrypto_error(cx, "BN_new() failed");
+        return JS_EXCEPTION;
+    }
+
+    if (BN_set_word(blocks, qjs_ceiling_div(data->length, AES_BLOCK_SIZE))
+        != 1)
+    {
+        qjs_webcrypto_error(cx, "BN_set_word() failed");
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    if (BN_cmp(blocks, total) > 0) {
+        JS_ThrowTypeError(cx, "AES-CTR repeated counter");
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    left = BN_new();
+    if (left == NULL) {
+        qjs_webcrypto_error(cx, "BN_new() failed");
+        return JS_EXCEPTION;
+    }
+
+    if (BN_sub(left, total, ctr) != 1) {
+        qjs_webcrypto_error(cx, "BN_sub() failed");
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    dst = js_malloc(cx, data->length + EVP_MAX_BLOCK_LENGTH);
+    if (dst == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    if (BN_cmp(left, blocks) >= 0) {
+        /*
+         * Doing a single run if a counter is not wrapped-around
+         * during the ciphering.
+         */
+        if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start,
+                                  data->start, data->length, iv.start, dst,
+                                  &len, encrypt) < 0)
+        {
+            js_free(cx, dst);
+            ret = JS_EXCEPTION;
+            goto fail;
+        }
+
+        goto done;
+    }
+
+    /*
+     * Otherwise splitting ciphering into two parts:
+     *  Until the wrapping moment
+     *  After the resetting counter to zero.
+     */
+
+    size1 = BN_get_word(left) * AES_BLOCK_SIZE;
+
+    if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start, data->start,
+                              size1, iv.start, dst, &len, encrypt) < 0)
+    {
+        js_free(cx, dst);
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    qjs_counter128_reset(iv.start, iv2, length);
+
+    if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start,
+                              &data->start[size1], data->length - size1,
+                              iv2, &dst[size1], &len2, encrypt) < 0)
+    {
+        js_free(cx, dst);
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    len += len2;
+
+done:
+
+    ret = qjs_new_array_buffer(cx, dst, len);
+
+fail:
+
+    BN_free(total);
+
+    if (ctr != NULL) {
+        BN_free(ctr);
+    }
+
+    if (blocks != NULL) {
+        BN_free(blocks);
+    }
+
+    if (left != NULL) {
+        BN_free(left);
+    }
+
+    return ret;
+}
+
+
+static JSValue
+qjs_cipher_aes_cbc(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
+    JSValue options, int encrypt)
+{
+    int               rc, olen_max, olen, olen2;
+    u_char            *dst;
+    JSValue           ret, value;
+    unsigned          remainder;
+    njs_str_t         iv;
+    EVP_CIPHER_CTX    *ctx;
+    const EVP_CIPHER  *cipher;
+
+    switch (key->u.s.raw.length) {
+    case 16:
+        cipher = EVP_aes_128_cbc();
+        break;
+
+    case 24:
+        cipher = EVP_aes_192_cbc();
+        break;
+
+    case 32:
+        cipher = EVP_aes_256_cbc();
+        break;
+
+    default:
+        JS_ThrowTypeError(cx, "AES-CBC invalid key length");
+        return JS_EXCEPTION;
+    }
+
+    value = JS_GetPropertyStr(cx, options, "iv");
+    if (JS_IsException(value)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_IsUndefined(value)) {
+        JS_ThrowTypeError(cx, "AES-CBC algorithm.iv is not provided");
+        return JS_EXCEPTION;
+    }
+
+    ret = qjs_typed_array_data(cx, value, &iv);
+    if (JS_IsException(ret)) {
+        return JS_EXCEPTION;
+    }
+
+    JS_FreeValue(cx, value);
+
+    if (iv.length != 16) {
+        JS_ThrowTypeError(cx, "AES-CBC algorithm.iv must be 16 bytes long");
+        return JS_EXCEPTION;
+    }
+
+    olen_max = data->length + AES_BLOCK_SIZE - 1;
+    remainder = olen_max % AES_BLOCK_SIZE;
+
+    if (remainder != 0) {
+        olen_max += AES_BLOCK_SIZE - remainder;
+    }
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed");
+        return JS_EXCEPTION;
+    }
+
+    rc = EVP_CipherInit_ex(ctx, cipher, NULL, key->u.s.raw.start, iv.start,
+                            encrypt);
+    if (rc <= 0) {
+        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    dst = js_malloc(cx, olen_max);
+    if (dst == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    rc = EVP_CipherUpdate(ctx, dst, &olen, data->start, data->length);
+    if (rc <= 0) {
+        qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        js_free(cx, dst);
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    rc = EVP_CipherFinal_ex(ctx, &dst[olen], &olen2);
+    if (rc <= 0) {
+        qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        js_free(cx, dst);
+        ret = JS_EXCEPTION;
+        goto fail;
+    }
+
+    olen += olen2;
+
+    ret = qjs_new_array_buffer(cx, dst, olen);
+
+fail:
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return ret;
+}
+
+
+static JSValue
+qjs_string_base64url(JSContext *cx, const njs_str_t *src)
+{
+    size_t     padding;
+    njs_str_t  dst;
+    u_char     buf[/* qjs_base64_encoded_length(512) */ 686];
+
+    if (src->length == 0) {
+        return JS_NewStringLen(cx, "", 0);
+    }
+
+    padding = src->length % 3;
+    padding = (4 >> padding) & 0x03;
+
+    dst.start = buf;
+    dst.length = qjs_base64_encode_length(cx, src) - padding;
+
+    qjs_base64url_encode(cx, src, &dst);
+
+    return JS_NewStringLen(cx, (const char *) dst.start, dst.length);
+}
+
+
+static JSValue
+qjs_export_base64url_bignum(JSContext *cx, const BIGNUM *v, size_t size)
+{
+    njs_str_t  src;
+    u_char     buf[512];
+
+    if (size == 0) {
+        size = BN_num_bytes(v);
+    }
+
+    if (njs_bn_bn2binpad(v, &buf[0], size) <= 0) {
+        JS_ThrowInternalError(cx, "njs_bn_bn2binpad() failed");
+        return JS_EXCEPTION;
+    }
+
+    src.start = buf;
+    src.length = size;
+
+    return qjs_string_base64url(cx, &src);
+}
+
+
+static int
+qjs_base64url_bignum_set(JSContext *cx, JSValue jwk, const char *key,
+    const BIGNUM *v, size_t size)
+{
+    JSValue  value;
+
+    value = qjs_export_base64url_bignum(cx, v, size);
+    if (JS_IsException(value)) {
+        return -1;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, key, value, JS_PROP_C_W_E) < 0) {
+        JS_FreeValue(cx, value);
+        return -1;
+    }
+
+    return 0;
+}
+
+
+static JSValue
+qjs_export_jwk_rsa(JSContext *cx, qjs_webcrypto_key_t *key)
+{
+    JSValue       jwk, alg;
+    njs_str_t     *nm;
+    const RSA     *rsa;
+    const BIGNUM  *n_bn, *e_bn, *d_bn, *p_bn, *q_bn, *dp_bn, *dq_bn, *qi_bn;
+
+    rsa = njs_pkey_get_rsa_key(key->u.a.pkey);
+
+    njs_rsa_get0_key(rsa, &n_bn, &e_bn, &d_bn);
+
+    jwk = JS_NewObject(cx);
+    if (JS_IsException(jwk)) {
+        return JS_EXCEPTION;
+    }
+
+    if (qjs_base64url_bignum_set(cx, jwk, "n", n_bn, 0) < 0) {
+        goto fail;
+    }
+
+    if (qjs_base64url_bignum_set(cx, jwk, "e", e_bn, 0) < 0) {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "RSA"),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        goto fail;
+    }
+
+    if (key->u.a.privat) {
+        njs_rsa_get0_factors(rsa, &p_bn, &q_bn);
+        njs_rsa_get0_ctr_params(rsa, &dp_bn, &dq_bn, &qi_bn);
+
+        if (qjs_base64url_bignum_set(cx, jwk, "d", d_bn, 0) < 0) {
+            goto fail;
+        }
+
+        if (qjs_base64url_bignum_set(cx, jwk, "p", p_bn, 0) < 0) {
+            goto fail;
+        }
+
+        if (qjs_base64url_bignum_set(cx, jwk, "q", q_bn, 0) < 0) {
+            goto fail;
+        }
+
+        if (qjs_base64url_bignum_set(cx, jwk, "dp", dp_bn, 0) < 0) {
+            goto fail;
+        }
+
+        if (qjs_base64url_bignum_set(cx, jwk, "dq", dq_bn, 0) < 0) {
+            goto fail;
+        }
+
+        if (qjs_base64url_bignum_set(cx, jwk, "qi", qi_bn, 0) < 0) {
+            goto fail;
+        }
+    }
+
+    nm = &qjs_webcrypto_alg_name[key->alg->type][key->hash];
+
+    alg = JS_NewStringLen(cx, (char *) nm->start, nm->length);
+    if (JS_IsException(alg)) {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "alg", alg, JS_PROP_C_W_E) < 0) {
+        JS_FreeValue(cx, alg);
+        goto fail;
+    }
+
+    return jwk;
+
+fail:
+
+    JS_FreeValue(cx, jwk);
+
+    return JS_EXCEPTION;
+}
+
+
+static JSValue
+qjs_export_jwk_ec(JSContext *cx, qjs_webcrypto_key_t *key)
+{
+    int             nid, group_bits, group_bytes;
+    BIGNUM          *x_bn, *y_bn;
+    JSValue         jwk, name;
+    njs_str_t       *cname;
+    const EC_KEY    *ec;
+    const BIGNUM    *d_bn;
+    const EC_POINT  *pub;
+    const EC_GROUP  *group;
+
+    x_bn = NULL;
+    y_bn = NULL;
+    d_bn = NULL;
+    jwk = JS_UNDEFINED;
+
+    ec = njs_pkey_get_ec_key(key->u.a.pkey);
+
+    pub = EC_KEY_get0_public_key(ec);
+    group = EC_KEY_get0_group(ec);
+
+    group_bits = EC_GROUP_get_degree(group);
+    group_bytes = (group_bits / 8) + (7 + (group_bits % 8)) / 8;
+
+    x_bn = BN_new();
+    if (x_bn == NULL) {
+        goto fail;
+    }
+
+    y_bn = BN_new();
+    if (y_bn == NULL) {
+        goto fail;
+    }
+
+    if (!njs_ec_point_get_affine_coordinates(group, pub, x_bn, y_bn)) {
+        qjs_webcrypto_error(cx, "EC_POINT_get_affine_coordinates() failed");
+        goto fail;
+    }
+
+    jwk = JS_NewObject(cx);
+    if (JS_IsException(jwk)) {
+        goto fail;
+    }
+
+    if (qjs_base64url_bignum_set(cx, jwk, "x", x_bn, group_bytes) < 0) {
+        goto fail;
+    }
+
+    BN_free(x_bn);
+    x_bn = NULL;
+
+    if (qjs_base64url_bignum_set(cx, jwk, "y", y_bn, group_bytes) < 0) {
+        goto fail;
+    }
+
+    BN_free(y_bn);
+    y_bn = NULL;
+
+    nid = EC_GROUP_get_curve_name(group);
+
+    cname = qjs_algorithm_curve_name(nid);
+    if (cname->length == 0) {
+        JS_ThrowTypeError(cx, "Unsupported JWK EC curve: %s", OBJ_nid2sn(nid));
+        goto fail;
+    }
+
+    name = JS_NewStringLen(cx, (char *) cname->start, cname->length);
+    if (JS_IsException(name)) {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "crv", name, JS_PROP_C_W_E) < 0) {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "EC"),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        goto fail;
+    }
+
+    if (key->u.a.privat) {
+        d_bn = EC_KEY_get0_private_key(ec);
+
+        if (qjs_base64url_bignum_set(cx, jwk, "d", d_bn, group_bytes) < 0) {
+            goto fail;
+        }
+    }
+
+    return jwk;
+
+fail:
+
+    JS_FreeValue(cx, jwk);
+
+    if (x_bn != NULL) {
+        BN_free(x_bn);
+    }
+
+    if (y_bn != NULL) {
+        BN_free(y_bn);
+    }
+
+    return JS_EXCEPTION;
+}
+
+
+static JSValue
+qjs_export_jwk_asymmetric(JSContext *cx, qjs_webcrypto_key_t *key)
+{
+    JSValue  jwk, ops;
+
+    njs_assert(key->u.a.pkey != NULL);
+
+    switch (EVP_PKEY_id(key->u.a.pkey)) {
+    case EVP_PKEY_RSA:
+#if (OPENSSL_VERSION_NUMBER >= 0x10101001L)
+    case EVP_PKEY_RSA_PSS:
+#endif
+        jwk = qjs_export_jwk_rsa(cx, key);
+        if (JS_IsException(jwk)) {
+            return JS_EXCEPTION;
+        }
+
+        break;
+
+    case EVP_PKEY_EC:
+        jwk = qjs_export_jwk_ec(cx, key);
+        if (JS_IsException(jwk)) {
+            return JS_EXCEPTION;
+        }
+
+        break;
+
+    default:
+        JS_ThrowTypeError(cx, "provided key cannot be exported as JWK");
+        return JS_EXCEPTION;
+    }
+
+    ops = qjs_key_ops(cx, key->usage);
+    if (JS_IsException(ops)) {
+        JS_FreeValue(cx, jwk);
+        return JS_EXCEPTION;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "key_ops", ops, JS_PROP_C_W_E) < 0) {
+        JS_FreeValue(cx, jwk);
+        JS_FreeValue(cx, ops);
+        return JS_EXCEPTION;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "ext",
+                                  JS_NewBool(cx, key->extractable),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        JS_FreeValue(cx, jwk);
+        return JS_EXCEPTION;
+    }
+
+    return jwk;
+}
+
+
+static JSValue
+qjs_export_jwk_oct(JSContext *cx, qjs_webcrypto_key_t *key)
+{
+    JSValue              val, jwk;
+    njs_str_t            *nm;
+    qjs_webcrypto_alg_t  type;
+
+    njs_assert(key->u.s.raw.start != NULL);
+
+    jwk = JS_NewObject(cx);
+    if (JS_IsException(jwk)) {
+        return JS_EXCEPTION;
+    }
+
+    val = qjs_string_base64url(cx, &key->u.s.raw);
+    if (JS_IsException(val)) {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "k", val, JS_PROP_C_W_E) < 0) {
+        JS_FreeValue(cx, val);
+        goto fail;
+    }
+
+    type = key->alg->type;
+
+    if (key->alg->type == QJS_ALGORITHM_HMAC) {
+        nm = &qjs_webcrypto_alg_name[type][key->hash];
+        val = JS_NewStringLen(cx, (char *) nm->start, nm->length);
+        if (JS_IsException(val)) {
+            goto fail;
+        }
+
+    } else {
+        switch (key->u.s.raw.length) {
+        case 16:
+        case 24:
+        case 32:
+            nm = &qjs_webcrypto_alg_aes_name
+                 [type - QJS_ALGORITHM_AES_GCM][(key->u.s.raw.length - 16) / 8];
+            val = JS_NewStringLen(cx, (char *) nm->start, nm->length);
+            if (JS_IsException(val)) {
+                goto fail;
+            }
+
+            break;
+
+        default:
+            val = JS_UNDEFINED;
+            break;
+        }
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "alg", val, JS_PROP_C_W_E) < 0) {
+        JS_FreeValue(cx, val);
+        goto fail;
+    }
+
+    val = qjs_key_ops(cx, key->usage);
+    if (JS_IsException(val)) {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "key_ops", val, JS_PROP_C_W_E) < 0) {
+        JS_FreeValue(cx, val);
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "oct"),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "ext",
+                                  JS_NewBool(cx, key->extractable),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        goto fail;
+    }
+
+    return jwk;
+
+fail:
+
+    JS_FreeValue(cx, jwk);
+
+    return JS_EXCEPTION;
+
+}
+
+
+static JSValue
+qjs_export_raw_ec(JSContext *cx, qjs_webcrypto_key_t *key)
+{
+    size_t                   size;
+    u_char                   *dst;
+    const EC_KEY             *ec;
+    const EC_GROUP           *group;
+    const EC_POINT           *point;
+    point_conversion_form_t  form;
+
+    njs_assert(key->u.a.pkey != NULL);
+
+    if (key->u.a.privat) {
+        JS_ThrowTypeError(cx, "private key of \"%s\" cannot be exported "
+                          "in \"raw\" format", qjs_algorithm_string(key->alg));
+        return JS_EXCEPTION;
+    }
+
+    ec = njs_pkey_get_ec_key(key->u.a.pkey);
+
+    group = EC_KEY_get0_group(ec);
+    point = EC_KEY_get0_public_key(ec);
+    form = POINT_CONVERSION_UNCOMPRESSED;
+
+    size = EC_POINT_point2oct(group, point, form, NULL, 0, NULL);
+    if (size == 0) {
+        qjs_webcrypto_error(cx, "EC_POINT_point2oct() failed");
+        return JS_EXCEPTION;
+    }
+
+    dst = js_malloc(cx, size);
+    if (dst == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
+    }
+
+    size = EC_POINT_point2oct(group, point, form, dst, size, NULL);
+    if (size == 0) {
+        js_free(cx, dst);
+        qjs_webcrypto_error(cx, "EC_POINT_point2oct() failed");
+        return JS_EXCEPTION;
+    }
+
+    return qjs_new_array_buffer(cx, dst, size);
+}
+
+
+static JSValue
+qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int derive_key)
+{
+    int                        rc;
+    u_char                     *k;
+    size_t                     olen;
+    int64_t                    iterations, length;
+    JSValue                    ret, val, aobject, dobject, dkey_value;
+    unsigned                   usage, mask;
+    njs_str_t                  salt, info;
+    const EVP_MD               *md;
+    EVP_PKEY_CTX               *pctx;
+    qjs_webcrypto_key_t        *key, *dkey;
+    qjs_webcrypto_hash_t       hash;
+    qjs_webcrypto_algorithm_t  *alg, *dalg;
+
+    aobject = argv[0];
+
+    alg = qjs_key_algorithm(cx, aobject);
+    if (alg == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (key == NULL) {
+        JS_ThrowTypeError(cx, "\"baseKey\" is not a CryptoKey object");
+        return JS_EXCEPTION;
+    }
+
+    mask = derive_key ? QJS_KEY_USAGE_DERIVE_KEY : QJS_KEY_USAGE_DERIVE_BITS;
+    if (!(key->usage & mask)) {
+        JS_ThrowTypeError(cx, "provide key does not support \"%s\" operation",
+                          derive_key ? "deriveKey" : "deriveBits");
+        return JS_EXCEPTION;
+    }
+
+    if (key->alg != alg) {
+        JS_ThrowTypeError(cx, "cannot derive %s using \"%s\" with \"%s\" key",
+                          derive_key ? "key" : "bits",
+                          qjs_algorithm_string(key->alg),
+                          qjs_algorithm_string(alg));
+        return JS_EXCEPTION;
+    }
+
+    dobject = argv[2];
+
+    if (derive_key) {
+        dalg = qjs_key_algorithm(cx, dobject);
+        if (dalg == NULL) {
+            return JS_EXCEPTION;
+        }
+
+        ret = JS_GetPropertyStr(cx, dobject, "length");
+        if (JS_IsException(ret)) {
+            return JS_EXCEPTION;
+        }
+
+        if (JS_IsUndefined(ret)) {
+            JS_ThrowTypeError(cx, "derivedKeyAlgorithm.length is not provided");
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        dalg = NULL;
+        ret = JS_DupValue(cx, dobject);
+    }
+
+    if (JS_ToInt64(cx, &length, ret) < 0) {
+        return JS_EXCEPTION;
+    }
+
+    JS_FreeValue(cx, ret);
+
+    dkey = NULL;
+    length /= 8;
+    dkey_value = JS_UNDEFINED;
+
+    if (derive_key) {
+        switch (dalg->type) {
+        case QJS_ALGORITHM_AES_GCM:
+        case QJS_ALGORITHM_AES_CTR:
+        case QJS_ALGORITHM_AES_CBC:
+
+            if (length != 16 && length != 32) {
+                JS_ThrowTypeError(cx, "deriveKey \"%s\" length must be "
+                                  "128 or 256", qjs_algorithm_string(dalg));
+                return JS_EXCEPTION;
+            }
+
+            break;
+
+        default:
+            JS_ThrowTypeError(cx, "not implemented deriveKey: \"%s\"",
+                              qjs_algorithm_string(dalg));
+            return JS_EXCEPTION;
+        }
+
+        ret = qjs_key_usage(cx, argv[4], &usage);
+        if (JS_IsException(ret)) {
+            return JS_EXCEPTION;
+        }
+
+        if (usage & ~dalg->usage) {
+            JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key",
+                              qjs_algorithm_string(alg));
+            return JS_EXCEPTION;
+        }
+
+        dkey_value = qjs_webcrypto_key_make(cx, dalg, usage, 0);
+        if (JS_IsException(dkey_value)) {
+            JS_ThrowOutOfMemory(cx);
+            return JS_EXCEPTION;
+        }
+
+        dkey = JS_GetOpaque(dkey_value, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    }
+
+    k = js_malloc(cx, length);
+    if (k == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
+    }
+
+    switch (alg->type) {
+    case QJS_ALGORITHM_PBKDF2:
+        ret = qjs_algorithm_hash(cx, aobject, &hash);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        val = JS_GetPropertyStr(cx, aobject, "salt");
+        if (JS_IsException(val)) {
+            goto fail;
+        }
+
+        ret = qjs_typed_array_data(cx, val, &salt);
+        JS_FreeValue(cx, val);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        if (salt.length < 16) {
+            JS_ThrowTypeError(cx, "PBKDF2 algorithm.salt must be at least "
+                              "16 bytes long");
+            goto fail;
+        }
+
+        ret = JS_GetPropertyStr(cx, aobject, "iterations");
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        if (JS_IsUndefined(ret)) {
+            JS_ThrowTypeError(cx, "PBKDF2 algorithm.iterations is not provided");
+            goto fail;
+        }
+
+        if (JS_ToInt64(cx, &iterations, ret) < 0) {
+            goto fail;
+        }
+
+        JS_FreeValue(cx, ret);
+
+        md = qjs_algorithm_hash_digest(hash);
+
+        rc = PKCS5_PBKDF2_HMAC((char *) key->u.s.raw.start, key->u.s.raw.length,
+                               salt.start, salt.length, iterations, md, length,
+                               k);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "PKCS5_PBKDF2_HMAC() failed");
+            goto fail;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_HKDF:
+#ifdef EVP_PKEY_HKDF
+        ret = qjs_algorithm_hash(cx, aobject, &hash);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        val = JS_GetPropertyStr(cx, aobject, "salt");
+        if (JS_IsException(val)) {
+            goto fail;
+        }
+
+        ret = qjs_typed_array_data(cx, val, &salt);
+        JS_FreeValue(cx, val);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        val = JS_GetPropertyStr(cx, aobject, "info");
+        if (JS_IsException(val)) {
+            goto fail;
+        }
+
+        ret = qjs_typed_array_data(cx, val, &info);
+        JS_FreeValue(cx, val);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+        if (pctx == NULL) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed");
+            goto fail;
+        }
+
+        rc = EVP_PKEY_derive_init(pctx);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_derive_init() failed");
+            goto free;
+        }
+
+        md = qjs_algorithm_hash_digest(hash);
+
+        rc = EVP_PKEY_CTX_set_hkdf_md(pctx, md);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_hkdf_md() failed");
+            goto free;
+        }
+
+        rc = EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt.start, salt.length);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set1_hkdf_salt() failed");
+            goto free;
+        }
+
+        rc = EVP_PKEY_CTX_set1_hkdf_key(pctx, key->u.s.raw.start,
+                                        key->u.s.raw.length);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set1_hkdf_key() failed");
+            goto free;
+        }
+
+        rc = EVP_PKEY_CTX_add1_hkdf_info(pctx, info.start, info.length);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_add1_hkdf_info() failed");
+            goto free;
+        }
+
+        olen = (size_t) length;
+        rc = EVP_PKEY_derive(pctx, k, &olen);
+        if (rc <= 0 || olen != (size_t) length) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed");
+            goto free;
+        }
+
+free:
+        EVP_PKEY_CTX_free(pctx);
+
+        if (rc <= 0) {
+            goto fail;
+        }
+
+        break;
+#else
+        (void) pctx;
+        (void) olen;
+        (void) &string_info;
+        (void) &info;
+#endif
+
+    case QJS_ALGORITHM_ECDH:
+    default:
+        JS_ThrowTypeError(cx, "not implemented deriveKey algorithm: \"%s\"",
+                          qjs_algorithm_string(alg));
+        goto fail;
+    }
+
+    if (derive_key) {
+        if (dalg->type == QJS_ALGORITHM_HMAC) {
+            ret = qjs_algorithm_hash(cx, dobject, &dkey->hash);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+        }
+
+        dkey->u.s.raw.start = k;
+        dkey->u.s.raw.length = length;
+
+        ret = dkey_value;
+
+    } else {
+
+        ret = qjs_new_array_buffer(cx, k, length);
+    }
+
+    return qjs_webcrypto_result(cx, ret, 0);
+
+fail:
+
+    JS_FreeValue(cx, dkey_value);
+
+    js_free(cx, k);
+
+    return qjs_webcrypto_result(cx, JS_UNDEFINED, -1);
+}
+
+
+static JSValue
+qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    JSValue               ret;
+    u_char                *dst;
+    unsigned              olen;
+    njs_str_t             data;
+    const EVP_MD          *md;
+    qjs_webcrypto_hash_t  hash;
+
+    ret = qjs_algorithm_hash(cx, argv[0], &hash);
+    if (JS_IsException(ret)) {
+        return JS_EXCEPTION;
+    }
+
+    ret = qjs_typed_array_data(cx, argv[1], &data);
+    if (JS_IsException(ret)) {
+        return JS_EXCEPTION;
+    }
+
+    md = qjs_algorithm_hash_digest(hash);
+    olen = EVP_MD_size(md);
+
+    dst = js_malloc(cx, olen);
+    if (dst == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
+    }
+
+    if (EVP_Digest(data.start, data.length, dst, &olen, md, NULL) <= 0) {
+        js_free(cx, dst);
+        qjs_webcrypto_error(cx, "EVP_Digest() failed");
+        return JS_EXCEPTION;
+    }
+
+    ret = qjs_new_array_buffer(cx, dst, olen);
+
+    return ret;
+}
+
+
+static JSValue
+qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    BIO                         *bio;
+    BUF_MEM                     *mem;
+    JSValue                     ret;
+    qjs_webcrypto_key_t         *key;
+    PKCS8_PRIV_KEY_INFO         *pkcs8;
+    qjs_webcrypto_key_format_t  fmt;
+
+    fmt = qjs_key_format(cx, argv[0]);
+
+    key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (key == NULL) {
+        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
+        return JS_EXCEPTION;
+    }
+
+    if (!(fmt & key->alg->fmt)) {
+        JS_ThrowTypeError(cx, "unsupported key fmt \"%s\" for \"%s\" key",
+                          qjs_format_string(fmt),
+                          qjs_algorithm_string(key->alg));
+        return JS_EXCEPTION;
+    }
+
+    if (!key->extractable) {
+        JS_ThrowTypeError(cx, "provided key cannot be extracted");
+        return JS_EXCEPTION;
+    }
+
+    switch (fmt) {
+    case QJS_KEY_FORMAT_JWK:
+        switch (key->alg->type) {
+        case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+        case QJS_ALGORITHM_RSA_PSS:
+        case QJS_ALGORITHM_RSA_OAEP:
+        case QJS_ALGORITHM_ECDSA:
+            ret = qjs_export_jwk_asymmetric(cx, key);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+
+            break;
+
+        case QJS_ALGORITHM_AES_GCM:
+        case QJS_ALGORITHM_AES_CTR:
+        case QJS_ALGORITHM_AES_CBC:
+        case QJS_ALGORITHM_HMAC:
+            ret = qjs_export_jwk_oct(cx, key);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+
+            break;
+
+        default:
+            JS_ThrowTypeError(cx, "provided key of \"%s\" cannot be exported "
+                              "as JWK", qjs_algorithm_string(key->alg));
+            goto fail;
+        }
+
+        break;
+
+    case QJS_KEY_FORMAT_PKCS8:
+        if (!key->u.a.privat) {
+            JS_ThrowTypeError(cx, "public key of \"%s\" cannot be exported "
+                              "as PKCS8", qjs_algorithm_string(key->alg));
+            goto fail;
+        }
+
+        bio = BIO_new(BIO_s_mem());
+        if (bio == NULL) {
+            qjs_webcrypto_error(cx, "BIO_new(BIO_s_mem()) failed");
+            goto fail;
+        }
+
+        njs_assert(key->u.a.pkey != NULL);
+
+        pkcs8 = EVP_PKEY2PKCS8(key->u.a.pkey);
+        if (pkcs8 == NULL) {
+            BIO_free(bio);
+            qjs_webcrypto_error(cx, "EVP_PKEY2PKCS8() failed");
+            goto fail;
+        }
+
+        if (!i2d_PKCS8_PRIV_KEY_INFO_bio(bio, pkcs8)) {
+            BIO_free(bio);
+            PKCS8_PRIV_KEY_INFO_free(pkcs8);
+            qjs_webcrypto_error(cx, "i2d_PKCS8_PRIV_KEY_INFO_bio() failed");
+            goto fail;
+        }
+
+        BIO_get_mem_ptr(bio, &mem);
+
+        ret = JS_NewArrayBufferCopy(cx, (const uint8_t *) mem->data,
+                                    mem->length);
+
+        BIO_free(bio);
+        PKCS8_PRIV_KEY_INFO_free(pkcs8);
+
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        break;
+
+    case QJS_KEY_FORMAT_SPKI:
+        if (key->u.a.privat) {
+            JS_ThrowTypeError(cx, "private key of \"%s\" cannot be exported "
+                              "as SPKI", qjs_algorithm_string(key->alg));
+            goto fail;
+        }
+
+        bio = BIO_new(BIO_s_mem());
+        if (bio == NULL) {
+            qjs_webcrypto_error(cx, "BIO_new(BIO_s_mem()) failed");
+            goto fail;
+        }
+
+        njs_assert(key->u.a.pkey != NULL);
+
+        if (!i2d_PUBKEY_bio(bio, key->u.a.pkey)) {
+            BIO_free(bio);
+            qjs_webcrypto_error(cx, "i2d_PUBKEY_bio() failed");
+            goto fail;
+        }
+
+        BIO_get_mem_ptr(bio, &mem);
+
+        ret = JS_NewArrayBufferCopy(cx, (const uint8_t *) mem->data,
+                                    mem->length);
+
+        BIO_free(bio);
+
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        break;
+
+    case QJS_KEY_FORMAT_RAW:
+    default:
+        if (key->alg->type == QJS_ALGORITHM_ECDSA) {
+            ret = qjs_export_raw_ec(cx, key);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+
+            break;
+        } else {
+            ret = JS_NewArrayBufferCopy(cx, key->u.s.raw.start,
+                                        key->u.s.raw.length);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+        }
+
+    }
+
+    return qjs_webcrypto_result(cx, ret, 0);
+
+fail:
+
+    return qjs_webcrypto_result(cx, JS_UNDEFINED, -1);
+}
+
+
+static JSValue
+qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    int                        n, extractable;
+    JSValue                    ret, key, keypub, options, obj;
+    unsigned                   usage;
+    EVP_PKEY_CTX               *ctx;
+    qjs_webcrypto_key_t        *wkey, *wkeypub;
+    qjs_webcrypto_algorithm_t  *alg;
+
+    ctx = NULL;
+    options = argv[0];
+    key = JS_UNDEFINED;
+    keypub = JS_UNDEFINED;
+
+    alg = qjs_key_algorithm(cx, options);
+    if (alg == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    ret = qjs_key_usage(cx, argv[2], &usage);
+    if (JS_IsException(ret)) {
+        return JS_EXCEPTION;
+    }
+
+    if (usage & ~alg->usage) {
+        JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key",
+                          qjs_algorithm_string(alg));
+        return JS_EXCEPTION;
+    }
+
+    extractable = JS_ToBool(cx, argv[1]);
+    key = qjs_webcrypto_key_make(cx, alg, usage, extractable);
+    if (JS_IsException(key)) {
+        return JS_EXCEPTION;
+    }
+
+    wkey = JS_GetOpaque(key, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+
+    switch (alg->type) {
+    case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+    case QJS_ALGORITHM_RSA_PSS:
+    case QJS_ALGORITHM_RSA_OAEP:
+        ret = qjs_algorithm_hash(cx, options, &wkey->hash);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        ret = JS_GetPropertyStr(cx, options, "modulusLength");
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        if (!JS_IsNumber(ret)) {
+            JS_FreeValue(cx, ret);
+            JS_ThrowTypeError(cx, "\"modulusLength\" is not a number");
+            goto fail;
+        }
+
+        if (JS_ToInt32(cx, &n, ret) < 0) {
+            goto fail;
+        }
+
+        ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
+        if (ctx == NULL) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed");
+            goto fail;
+        }
+
+        if (EVP_PKEY_keygen_init(ctx) <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_keygen_init() failed");
+            goto fail;
+        }
+
+        if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, n) <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_rsa_keygen_bits() "
+                                "failed");
+            goto fail;
+        }
+
+        if (EVP_PKEY_keygen(ctx, &wkey->u.a.pkey) <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_keygen() failed");
+            goto fail;
+        }
+
+        EVP_PKEY_CTX_free(ctx);
+        ctx = NULL;
+
+        wkey->u.a.privat = 1;
+        wkey->usage = (alg->type == QJS_ALGORITHM_RSA_OAEP)
+                        ? QJS_KEY_USAGE_DECRYPT
+                        : QJS_KEY_USAGE_SIGN;
+
+        keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable);
+        if (JS_IsException(keypub)) {
+            goto fail;
+        }
+
+        if (njs_pkey_up_ref(wkey->u.a.pkey) <= 0) {
+            qjs_webcrypto_error(cx, "qjs_pkey_up_ref() failed");
+            goto fail;
+        }
+
+        wkeypub = JS_GetOpaque(keypub, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+
+        wkeypub->u.a.pkey = wkey->u.a.pkey;
+        wkeypub->hash = wkey->hash;
+        wkeypub->usage = (alg->type == QJS_ALGORITHM_RSA_OAEP)
+                          ? QJS_KEY_USAGE_ENCRYPT
+                          : QJS_KEY_USAGE_VERIFY;
+
+        obj = JS_NewObject(cx);
+        if (JS_IsException(obj)) {
+            goto fail;
+        }
+
+        if (JS_SetPropertyStr(cx, obj, "privateKey", key) < 0) {
+            goto fail;
+        }
+
+        key = JS_UNDEFINED;
+
+        if (JS_SetPropertyStr(cx, obj, "publicKey", keypub) < 0) {
+            goto fail;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_ECDSA:
+        ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);
+        if (ctx == NULL) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed");
+            goto fail;
+        }
+
+        if (EVP_PKEY_keygen_init(ctx) <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_keygen_init() failed");
+            goto fail;
+        }
+
+        if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, wkey->u.a.curve) <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_ec_paramgen_curve_nid() "
+                                "failed");
+            goto fail;
+        }
+
+        if (EVP_PKEY_keygen(ctx, &wkey->u.a.pkey) <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_keygen() failed");
+            goto fail;
+        }
+
+        EVP_PKEY_CTX_free(ctx);
+        ctx = NULL;
+
+        wkey->u.a.privat = 1;
+        wkey->usage = QJS_KEY_USAGE_SIGN;
+
+        keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable);
+        if (JS_IsException(keypub)) {
+            goto fail;
+        }
+
+        if (njs_pkey_up_ref(wkey->u.a.pkey) <= 0) {
+            qjs_webcrypto_error(cx, "qjs_pkey_up_ref() failed");
+            goto fail;
+        }
+
+        wkeypub = JS_GetOpaque(keypub, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+
+        wkeypub->u.a.pkey = wkey->u.a.pkey;
+        wkeypub->u.a.curve = wkey->u.a.curve;
+        wkeypub->usage = QJS_KEY_USAGE_VERIFY;
+
+        obj = JS_NewObject(cx);
+        if (JS_IsException(obj)) {
+            goto fail;
+        }
+
+        if (JS_SetPropertyStr(cx, obj, "privateKey", key) < 0) {
+            goto fail;
+        }
+
+        key = JS_UNDEFINED;
+
+        if (JS_SetPropertyStr(cx, obj, "publicKey", keypub) < 0) {
+            goto fail;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_AES_GCM:
+    case QJS_ALGORITHM_AES_CTR:
+    case QJS_ALGORITHM_AES_CBC:
+    case QJS_ALGORITHM_HMAC:
+        if (alg->type == QJS_ALGORITHM_HMAC) {
+            ret = qjs_algorithm_hash(cx, options, &wkey->hash);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+
+            wkey->u.s.raw.length =
+                            EVP_MD_size(qjs_algorithm_hash_digest(wkey->hash));
+
+        } else {
+            ret = JS_GetPropertyStr(cx, options, "length");
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+
+            if (!JS_IsNumber(ret)) {
+                JS_FreeValue(cx, ret);
+                JS_ThrowTypeError(cx, "length is not a number");
+                goto fail;
+            }
+
+            if (JS_ToInt32(cx, &n, ret) < 0) {
+                goto fail;
+            }
+
+            wkey->u.s.raw.length = n / 8;
+
+            if (wkey->u.s.raw.length != 16
+                && wkey->u.s.raw.length != 24
+                && wkey->u.s.raw.length != 32)
+            {
+                JS_ThrowTypeError(cx, "length for \"%s\" key should be "
+                                  "one of 128, 192, 256",
+                                  qjs_algorithm_string(alg));
+                goto fail;
+            }
+        }
+
+        wkey->u.s.raw.start = js_malloc(cx, wkey->u.s.raw.length);
+        if (wkey->u.s.raw.start == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            goto fail;
+        }
+
+        if (RAND_bytes(wkey->u.s.raw.start, wkey->u.s.raw.length) <= 0) {
+            qjs_webcrypto_error(cx, "RAND_bytes() failed");
+            goto fail;
+        }
+
+        obj = key;
+
+        break;
+
+    default:
+        JS_ThrowTypeError(cx, "not implemented generateKey algorithm: \"%s\"",
+                          qjs_algorithm_string(alg));
+        goto fail;
+    }
+
+    return qjs_webcrypto_result(cx, obj, 0);
+
+fail:
+
+    if (ctx != NULL) {
+        EVP_PKEY_CTX_free(ctx);
+    }
+
+    JS_FreeValue(cx, key);
+    JS_FreeValue(cx, keypub);
+
+    return qjs_webcrypto_result(cx, JS_UNDEFINED, -1);
+}
+
+
+static BIGNUM *
+qjs_import_base64url_bignum(JSContext *cx, JSValue value)
+{
+    BIGNUM     *bn;
+    njs_str_t  data, decoded;
+    u_char     buf[512];
+
+    data.start = (u_char *) JS_ToCStringLen(cx, &data.length, value);
+    if (data.start == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return NULL;
+    }
+
+    decoded.length = qjs_base64url_decode_length(cx, &data);
+
+    if (decoded.length > sizeof(buf)) {
+        JS_ThrowRangeError(cx, "JWK key too long: %zu > 512", decoded.length);
+        return NULL;
+    }
+
+    decoded.start = buf;
+
+    qjs_base64url_decode(cx, &data, &decoded);
+
+    bn = BN_bin2bn(decoded.start, decoded.length, NULL);
+    JS_FreeCString(cx, (char *) data.start);
+
+    return bn;
+}
+
+
+static EVP_PKEY *
+qjs_import_jwk_rsa(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
+{
+    RSA                    *rsa;
+    BIGNUM                 *n_bn, *e_bn, *d_bn, *p_bn, *q_bn, *dp_bn, *dq_bn,
+                           *qi_bn;
+    JSValue                ret, val, n, e, d, p, q, dp, dq, qi;
+    unsigned               usage;
+    EVP_PKEY               *pkey;
+    njs_str_t              alg;
+    qjs_webcrypto_entry_t  *w;
+
+    e = JS_UNDEFINED;
+    d = JS_UNDEFINED;
+
+    n = JS_GetPropertyStr(cx, jwk, "n");
+    if (JS_IsException(n)) {
+        goto fail0;
+    }
+
+    e = JS_GetPropertyStr(cx, jwk, "e");
+    if (JS_IsException(e)) {
+        goto fail0;
+    }
+
+    d = JS_GetPropertyStr(cx, jwk, "d");
+    if (JS_IsException(d)) {
+        goto fail0;
+    }
+
+    if (!JS_IsString(n)
+        || !JS_IsString(e)
+        || (!JS_IsUndefined(d) && !JS_IsString(d)))
+    {
+fail0:
+        JS_FreeValue(cx, n);
+        JS_FreeValue(cx, e);
+        JS_FreeValue(cx, d);
+        JS_ThrowTypeError(cx, "Invalid JWK RSA key");
+        return NULL;
+    }
+
+    key->u.a.privat = JS_IsString(d);
+
+    val = JS_GetPropertyStr(cx, jwk, "key_ops");
+    if (!JS_IsException(val) && !JS_IsUndefined(val)) {
+        ret = qjs_key_usage(cx, val, &usage);
+        JS_FreeValue(cx, val);
+        if (JS_IsException(ret)) {
+            goto fail0;
+        }
+
+        if ((key->usage & usage) != key->usage) {
+            JS_ThrowTypeError(cx, "Key operations and usage mismatch");
+            goto fail0;
+        }
+    }
+
+    ret = JS_GetPropertyStr(cx, jwk, "alg");
+    if (!JS_IsException(ret) && !JS_IsUndefined(ret)) {
+        alg.start = (u_char *) JS_ToCStringLen(cx, &alg.length, ret);
+        JS_FreeValue(cx, ret);
+        if (alg.start == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            goto fail0;
+        }
+
+        for (w = &qjs_webcrypto_alg_hash[0]; w->name.length != 0; w++) {
+            if (njs_strstr_eq(&alg, &w->name)) {
+                key->hash = w->value;
+                break;
+            }
+        }
+
+        JS_FreeCString(cx, (char *) alg.start);
+    }
+
+    if (key->extractable) {
+        ret = JS_GetPropertyStr(cx, jwk, "ext");
+        if (!JS_IsException(ret)
+            && !JS_IsUndefined(ret)
+            && !JS_ToBool(cx, ret))
+        {
+            JS_FreeValue(cx, ret);
+            JS_ThrowTypeError(cx, "JWK RSA is not extractable");
+            goto fail0;
+        }
+
+        JS_FreeValue(cx, ret);
+    }
+
+    rsa = RSA_new();
+    if (rsa == NULL) {
+        qjs_webcrypto_error(cx, "RSA_new() failed");
+        goto fail0;
+    }
+
+    p = JS_UNDEFINED;
+    q = JS_UNDEFINED;
+    dp = JS_UNDEFINED;
+    dq = JS_UNDEFINED;
+    qi = JS_UNDEFINED;
+
+    n_bn = qjs_import_base64url_bignum(cx, n);
+    if (n_bn == NULL) {
+        goto fail;
+    }
+
+    e_bn = qjs_import_base64url_bignum(cx, e);
+    if (e_bn == NULL) {
+        BN_free(n_bn);
+        goto fail;
+    }
+
+    if (!njs_rsa_set0_key(rsa, n_bn, e_bn, NULL)) {
+        BN_free(n_bn);
+        BN_free(e_bn);
+        qjs_webcrypto_error(cx, "RSA_set0_key() failed");
+        goto fail;
+    }
+
+    if (!key->u.a.privat) {
+        goto done;
+    }
+
+    p = JS_GetPropertyStr(cx, jwk, "p");
+    if (JS_IsException(p)) {
+        goto fail1;
+    }
+
+    q = JS_GetPropertyStr(cx, jwk, "q");
+    if (JS_IsException(q)) {
+        goto fail1;
+    }
+
+    dp = JS_GetPropertyStr(cx, jwk, "dp");
+    if (JS_IsException(dp)) {
+        goto fail1;
+    }
+
+    dq = JS_GetPropertyStr(cx, jwk, "dq");
+    if (JS_IsException(dq)) {
+        goto fail1;
+    }
+
+    qi = JS_GetPropertyStr(cx, jwk, "qi");
+    if (JS_IsException(qi)) {
+        goto fail1;
+    }
+
+    if (!JS_IsString(d)
+        || !JS_IsString(p)
+        || !JS_IsString(q)
+        || !JS_IsString(dp)
+        || !JS_IsString(dq)
+        || !JS_IsString(qi))
+    {
+fail1:
+        JS_ThrowTypeError(cx, "Invalid JWK RSA key");
+        goto fail;
+    }
+
+    d_bn = qjs_import_base64url_bignum(cx, d);
+    if (d_bn == NULL) {
+        goto fail;
+    }
+
+    if (!njs_rsa_set0_key(rsa, NULL, NULL, d_bn)) {
+        BN_free(d_bn);
+        qjs_webcrypto_error(cx, "RSA_set0_key() failed");
+        goto fail;
+    }
+
+    p_bn = qjs_import_base64url_bignum(cx, p);
+    if (p_bn == NULL) {
+        goto fail;
+    }
+
+    q_bn = qjs_import_base64url_bignum(cx, q);
+    if (q_bn == NULL) {
+        BN_free(p_bn);
+        goto fail;
+    }
+
+    if (!njs_rsa_set0_factors(rsa, p_bn, q_bn)) {
+        BN_free(p_bn);
+        BN_free(q_bn);
+
+        qjs_webcrypto_error(cx, "RSA_set0_factors() failed");
+        goto fail;
+    }
+
+    dp_bn = qjs_import_base64url_bignum(cx, dp);
+    if (dp_bn == NULL) {
+        goto fail;
+    }
+
+    dq_bn = qjs_import_base64url_bignum(cx, dq);
+    if (dq_bn == NULL) {
+        BN_free(dp_bn);
+        goto fail;
+    }
+
+    qi_bn = qjs_import_base64url_bignum(cx, qi);
+    if (qi_bn == NULL) {
+        BN_free(dp_bn);
+        BN_free(dq_bn);
+        goto fail;
+    }
+
+    if (!njs_rsa_set0_ctr_params(rsa, dp_bn, dq_bn, qi_bn)) {
+        BN_free(dp_bn);
+        BN_free(dq_bn);
+        BN_free(qi_bn);
+        qjs_webcrypto_error(cx, "RSA_set0_crt_params() failed");
+        goto fail;
+    }
+
+    JS_FreeValue(cx, p);
+    JS_FreeValue(cx, q);
+    JS_FreeValue(cx, dp);
+    JS_FreeValue(cx, dq);
+    JS_FreeValue(cx, qi);
+
+done:
+
+    JS_FreeValue(cx, n);
+    JS_FreeValue(cx, e);
+    JS_FreeValue(cx, d);
+
+    pkey = EVP_PKEY_new();
+    if (pkey == NULL) {
+        goto fail;
+    }
+
+    if (!EVP_PKEY_set1_RSA(pkey, rsa)) {
+        EVP_PKEY_free(pkey);
+        goto fail;
+    }
+
+    RSA_free(rsa);
+
+    return pkey;
+
+fail:
+
+    JS_FreeValue(cx, n);
+    JS_FreeValue(cx, e);
+    JS_FreeValue(cx, d);
+    JS_FreeValue(cx, p);
+    JS_FreeValue(cx, q);
+    JS_FreeValue(cx, dp);
+    JS_FreeValue(cx, dq);
+    JS_FreeValue(cx, qi);
+
+    RSA_free(rsa);
+
+    return NULL;
+}
+
+
+static EVP_PKEY *
+qjs_import_jwk_ec(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
+{
+    int                    curve;
+    EC_KEY                 *ec;
+    BIGNUM                 *x_bn, *y_bn, *d_bn;
+    JSValue                ret, val, x, y, d;
+    unsigned               usage;
+    EVP_PKEY               *pkey;
+    njs_str_t              name;
+    qjs_webcrypto_entry_t  *e;
+
+    x = JS_UNDEFINED;
+    y = JS_UNDEFINED;
+    d = JS_UNDEFINED;
+
+    x = JS_GetPropertyStr(cx, jwk, "x");
+    if (JS_IsException(x)) {
+        goto fail0;
+    }
+
+    y = JS_GetPropertyStr(cx, jwk, "y");
+    if (JS_IsException(y)) {
+        goto fail0;
+    }
+
+    d = JS_GetPropertyStr(cx, jwk, "d");
+    if (JS_IsException(d)) {
+        goto fail0;
+    }
+
+    if (!JS_IsString(x)
+        || !JS_IsString(y)
+        || (!JS_IsUndefined(d) && !JS_IsString(d)))
+    {
+fail0:
+        JS_FreeValue(cx, x);
+        JS_FreeValue(cx, y);
+        JS_FreeValue(cx, d);
+        JS_ThrowTypeError(cx, "Invalid JWK EC key");
+        return NULL;
+    }
+
+    key->u.a.privat = JS_IsString(d);
+
+    val = JS_GetPropertyStr(cx, jwk, "key_ops");
+    if (!JS_IsException(val) && !JS_IsUndefined(val)) {
+        ret = qjs_key_usage(cx, val, &usage);
+        JS_FreeValue(cx, val);
+        if (JS_IsException(ret)) {
+            goto fail0;
+        }
+
+        if ((key->usage & usage) != key->usage) {
+            JS_ThrowTypeError(cx, "Key operations and usage mismatch");
+            goto fail0;
+        }
+    }
+
+    if (key->extractable) {
+        ret = JS_GetPropertyStr(cx, jwk, "ext");
+        if (!JS_IsException(ret)
+            && !JS_IsUndefined(ret)
+            && !JS_ToBool(cx, ret))
+        {
+            JS_FreeValue(cx, ret);
+            JS_ThrowTypeError(cx, "JWK EC is not extractable");
+            goto fail0;
+        }
+
+        JS_FreeValue(cx, ret);
+    }
+
+    curve = 0;
+
+    ret = JS_GetPropertyStr(cx, jwk, "crv");
+    if (!JS_IsException(ret) && !JS_IsUndefined(ret)) {
+        name.start = (u_char *) JS_ToCStringLen(cx, &name.length, ret);
+        JS_FreeValue(cx, ret);
+        if (name.start == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            goto fail0;
+        }
+
+        for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) {
+            if (njs_strstr_eq(&name, &e->name)) {
+                curve = e->value;
+                break;
+            }
+        }
+
+        JS_FreeCString(cx, (char *) name.start);
+    }
+
+    if (curve != key->u.a.curve) {
+        JS_ThrowTypeError(cx, "JWK EC curve mismatch");
+        goto fail0;
+    }
+
+    ec = EC_KEY_new_by_curve_name(key->u.a.curve);
+    if (ec == NULL) {
+        qjs_webcrypto_error(cx, "EC_KEY_new_by_curve_name() failed");
+        goto fail0;
+    }
+
+    y_bn = NULL;
+    d_bn = NULL;
+
+    x_bn = qjs_import_base64url_bignum(cx, x);
+    if (x_bn == NULL) {
+        goto fail;
+    }
+
+    y_bn = qjs_import_base64url_bignum(cx, y);
+    if (y_bn == NULL) {
+        goto fail;
+    }
+
+    if (!EC_KEY_set_public_key_affine_coordinates(ec, x_bn, y_bn)) {
+        qjs_webcrypto_error(cx, "EC_KEY_set_public_key_affine_coordinates() "
+                            "failed");
+        goto fail;
+    }
+
+    BN_free(x_bn);
+    x_bn = NULL;
+
+    BN_free(y_bn);
+    y_bn = NULL;
+
+    if (key->u.a.privat) {
+        d_bn = qjs_import_base64url_bignum(cx, d);
+        if (d_bn == NULL) {
+            goto fail;
+        }
+
+        if (!EC_KEY_set_private_key(ec, d_bn)) {
+            qjs_webcrypto_error(cx, "EC_KEY_set_private_key() failed");
+            goto fail;
+        }
+
+        BN_free(d_bn);
+        d_bn = NULL;
+    }
+
+    pkey = EVP_PKEY_new();
+    if (pkey == NULL) {
+        goto fail;
+    }
+
+    if (!EVP_PKEY_set1_EC_KEY(pkey, ec)) {
+        qjs_webcrypto_error(cx, "EVP_PKEY_set1_EC_KEY() failed");
+        goto fail_pkey;
+    }
+
+    EC_KEY_free(ec);
+
+    JS_FreeValue(cx, x);
+    JS_FreeValue(cx, y);
+    JS_FreeValue(cx, d);
+
+    return pkey;
+
+fail_pkey:
+
+    EVP_PKEY_free(pkey);
+
+fail:
+
+    EC_KEY_free(ec);
+
+    if (x_bn != NULL) {
+        BN_free(x_bn);
+    }
+
+    if (y_bn != NULL) {
+        BN_free(y_bn);
+    }
+
+    if (d_bn != NULL) {
+        BN_free(d_bn);
+    }
+
+    JS_FreeValue(cx, x);
+    JS_FreeValue(cx, y);
+    JS_FreeValue(cx, d);
+
+    return NULL;
+}
+
+
+static EVP_PKEY *
+qjs_import_raw_ec(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key)
+{
+    EC_KEY          *ec;
+    EVP_PKEY        *pkey;
+    EC_POINT        *pub;
+    const EC_GROUP  *group;
+
+    ec = EC_KEY_new_by_curve_name(key->u.a.curve);
+    if (ec == NULL) {
+        qjs_webcrypto_error(cx, "EC_KEY_new_by_curve_name() failed");
+        return NULL;
+    }
+
+    group = EC_KEY_get0_group(ec);
+
+    pub = EC_POINT_new(group);
+    if (pub == NULL) {
+        EC_KEY_free(ec);
+        qjs_webcrypto_error(cx, "EC_POINT_new() failed");
+        return NULL;
+    }
+
+    if (!EC_POINT_oct2point(group, pub, data->start, data->length, NULL)) {
+        EC_KEY_free(ec);
+        EC_POINT_free(pub);
+        qjs_webcrypto_error(cx, "EC_POINT_oct2point() failed");
+        return NULL;
+    }
+
+    if (!EC_KEY_set_public_key(ec, pub)) {
+        EC_KEY_free(ec);
+        EC_POINT_free(pub);
+        qjs_webcrypto_error(cx, "EC_KEY_set_public_key() failed");
+        return NULL;
+    }
+
+    pkey = EVP_PKEY_new();
+    if (pkey == NULL) {
+        EC_KEY_free(ec);
+        EC_POINT_free(pub);
+        qjs_webcrypto_error(cx, "EVP_PKEY_new() failed");
+        return NULL;
+    }
+
+    if (!EVP_PKEY_set1_EC_KEY(pkey, ec)) {
+        EC_KEY_free(ec);
+        EC_POINT_free(pub);
+        EVP_PKEY_free(pkey);
+        qjs_webcrypto_error(cx, "EVP_PKEY_set1_EC_KEY() failed");
+        return NULL;
+    }
+
+    EC_KEY_free(ec);
+    EC_POINT_free(pub);
+
+    return pkey;
+}
+
+
+static JSValue
+qjs_import_jwk_oct(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
+{
+    size_t                 size;
+    unsigned               usage;
+    JSValue                val, ret;
+    njs_str_t              *a, alg, b64;
+    qjs_webcrypto_alg_t    type;
+    qjs_webcrypto_entry_t  *w;
+
+    static qjs_webcrypto_entry_t hashes[] = {
+        { njs_str("HS1"), QJS_HASH_SHA1 },
+        { njs_str("HS256"), QJS_HASH_SHA256 },
+        { njs_str("HS384"), QJS_HASH_SHA384 },
+        { njs_str("HS512"), QJS_HASH_SHA512 },
+        { njs_null_str, 0 }
+    };
+
+    val = JS_GetPropertyStr(cx, jwk, "k");
+    if (JS_IsException(val) || !JS_IsString(val)) {
+        JS_ThrowTypeError(cx, "Invalid JWK oct key");
+        return JS_EXCEPTION;
+    }
+
+    b64.start = (u_char *) JS_ToCStringLen(cx, &b64.length, val);
+    JS_FreeValue(cx, val);
+    if (b64.start == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
+    }
+
+    size = qjs_base64url_decode_length(cx, &b64);
+
+    key->u.s.raw.length = size;
+    key->u.s.raw.start = js_malloc(cx, size);
+    if (key->u.s.raw.start == NULL) {
+        JS_FreeCString(cx, (char *) b64.start);
+        JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
+    }
+
+    qjs_base64url_decode(cx, &b64, &key->u.s.raw);
+    JS_FreeCString(cx, (char *) b64.start);
+
+    size = 16;
+
+    val = JS_GetPropertyStr(cx, jwk, "alg");
+    if (JS_IsException(val)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_IsString(val)) {
+        alg.start = (u_char *) JS_ToCStringLen(cx, &alg.length, val);
+        JS_FreeValue(cx, val);
+        val = JS_UNDEFINED;
+
+        if (alg.start == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            return JS_EXCEPTION;
+        }
+
+        if (key->alg->type == QJS_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 = &qjs_webcrypto_alg_aes_name[type - QJS_ALGORITHM_AES_GCM][0];
+            for (; a->length != 0; a++) {
+                if (njs_strstr_eq(&alg, a)) {
+                    goto done;
+                }
+
+                size += 8;
+            }
+        }
+
+        JS_ThrowTypeError(cx, "unexpected \"alg\" value \"%s\" for JWK key",
+                          alg.start);
+        JS_FreeCString(cx, (char *) alg.start);
+        return JS_EXCEPTION;
+    }
+
+    JS_FreeValue(cx, val);
+
+done:
+
+    if (key->alg->type != QJS_ALGORITHM_HMAC) {
+        if (key->u.s.raw.length != size) {
+            JS_ThrowTypeError(cx, "key size and \"alg\" value \"%s\" mismatch",
+                              alg.start);
+            JS_FreeCString(cx, (char *) alg.start);
+            return JS_EXCEPTION;
+        }
+    }
+
+    JS_FreeCString(cx, (char *) alg.start);
+
+    val = JS_GetPropertyStr(cx, jwk, "key_ops");
+    if (!JS_IsException(val) && !JS_IsUndefined(val)) {
+        ret = qjs_key_usage(cx, val, &usage);
+        JS_FreeValue(cx, val);
+        if (JS_IsException(ret)) {
+            return JS_EXCEPTION;
+        }
+
+        if ((key->usage & usage) != key->usage) {
+            JS_ThrowTypeError(cx, "Key operations and usage mismatch");
+            return JS_EXCEPTION;
+        }
+    }
+
+    if (key->extractable) {
+        val = JS_GetPropertyStr(cx, jwk, "ext");
+        if (!JS_IsException(val)
+            && !JS_IsUndefined(val)
+            && !JS_ToBool(cx, val))
+        {
+            JS_FreeValue(cx, val);
+            JS_ThrowTypeError(cx, "JWK oct is not extractable");
+            return JS_EXCEPTION;
+        }
+
+        JS_FreeValue(cx, val);
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    int                         nid;
+    BIO                         *bio;
+#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
+    RSA                         *rsa;
+    EC_KEY                      *ec;
+#else
+    char                        gname[80];
+#endif
+    JSValue                     options, key, jwk, val, ret;
+    unsigned                    mask, usage;
+    EVP_PKEY                    *pkey;
+    njs_str_t                   key_data;
+    const u_char                *start;
+#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
+    const EC_GROUP              *group;
+#endif
+    qjs_webcrypto_key_t         *wkey;
+    PKCS8_PRIV_KEY_INFO         *pkcs8;
+    qjs_webcrypto_hash_t        hash;
+    qjs_webcrypto_jwk_kty_t     kty;
+    qjs_webcrypto_algorithm_t   *alg;
+    qjs_webcrypto_key_format_t  fmt;
+
+    pkey = NULL;
+    key_data.start = NULL;
+    key_data.length = 0;
+
+    fmt = qjs_key_format(cx, argv[0]);
+    if (fmt == QJS_KEY_FORMAT_UNKNOWN) {
+        return JS_EXCEPTION;
+    }
+
+    options = argv[2];
+
+    alg = qjs_key_algorithm(cx, options);
+    if (alg == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    if (!(fmt & alg->fmt)) {
+        JS_ThrowTypeError(cx, "unsupported key fmt \"%s\" for \"%s\" key",
+                          qjs_format_string(fmt), qjs_algorithm_string(alg));
+        return JS_EXCEPTION;
+    }
+
+    ret = qjs_key_usage(cx, argv[4], &usage);
+    if (JS_IsException(ret)) {
+        return JS_EXCEPTION;
+    }
+
+    if (usage & ~alg->usage) {
+        JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key",
+                          qjs_algorithm_string(alg));
+        return JS_EXCEPTION;
+    }
+
+    if (fmt != QJS_KEY_FORMAT_JWK) {
+        ret = qjs_typed_array_data(cx, argv[1], &key_data);
+        if (JS_IsException(ret)) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    key = qjs_webcrypto_key_make(cx, alg, usage, JS_ToBool(cx, argv[3]));
+    if (JS_IsException(key)) {
+        return JS_EXCEPTION;
+    }
+
+    wkey = JS_GetOpaque(key, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+
+    /*
+     * set by qjs_webcrypto_key_make():
+     *
+     *  key->u.a.pkey = NULL;
+     *  key->u.s.raw.length = 0;
+     *  key->u.s.raw.start = NULL;
+     *  key->u.a.curve = 0;
+     *  key->u.a.privat = 0;
+     *  key->hash = QJS_HASH_UNSET;
+     */
+
+    switch (fmt) {
+    case QJS_KEY_FORMAT_PKCS8:
+        bio = BIO_new_mem_buf(key_data.start, key_data.length);
+        if (bio == NULL) {
+            JS_ThrowTypeError(cx, "BIO_new_mem_buf() failed");
+            goto fail;
+        }
+
+        pkcs8 = d2i_PKCS8_PRIV_KEY_INFO_bio(bio, NULL);
+        if (pkcs8 == NULL) {
+            BIO_free(bio);
+            JS_ThrowTypeError(cx, "d2i_PKCS8_PRIV_KEY_INFO_bio() failed");
+            goto fail;
+        }
+
+        pkey = EVP_PKCS82PKEY(pkcs8);
+        if (pkey == NULL) {
+            PKCS8_PRIV_KEY_INFO_free(pkcs8);
+            BIO_free(bio);
+            JS_ThrowTypeError(cx, "EVP_PKCS82PKEY() failed");
+            goto fail;
+        }
+
+        PKCS8_PRIV_KEY_INFO_free(pkcs8);
+        BIO_free(bio);
+
+        wkey->u.a.privat = 1;
+
+        break;
+
+    case QJS_KEY_FORMAT_SPKI:
+        start = key_data.start;
+        pkey = d2i_PUBKEY(NULL, &start, key_data.length);
+        if (pkey == NULL) {
+            JS_ThrowTypeError(cx, "d2i_PUBKEY() failed");
+            goto fail;
+        }
+
+        break;
+
+    case QJS_KEY_FORMAT_JWK:
+        jwk = argv[1];
+        if (!JS_IsObject(jwk)) {
+            JS_ThrowTypeError(cx, "invalid JWK key data: object value "
+                              "expected");
+            goto fail;
+        }
+
+        val = JS_GetPropertyStr(cx, jwk, "kty");
+        if (JS_IsException(val)) {
+            goto fail;
+        }
+
+        kty = qjs_jwk_kty(cx, val);
+        JS_FreeValue(cx, val);
+        if (kty == QJS_KEY_JWK_KTY_UNKNOWN) {
+            goto fail;
+        }
+
+        switch (kty) {
+        case QJS_KEY_JWK_KTY_RSA:
+            pkey = qjs_import_jwk_rsa(cx, jwk, wkey);
+            if (pkey == NULL) {
+                goto fail;
+            }
+
+            break;
+
+        case QJS_KEY_JWK_KTY_EC:
+            ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+
+            pkey = qjs_import_jwk_ec(cx, jwk, wkey);
+            if (pkey == NULL) {
+                goto fail;
+            }
+
+            break;
+
+        case QJS_KEY_JWK_KTY_OCT:
+        default:
+            ret = qjs_import_jwk_oct(cx, jwk, wkey);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+        }
+
+        break;
+
+    case QJS_KEY_FORMAT_RAW:
+    default:
+        break;
+    }
+
+    switch (alg->type) {
+    case QJS_ALGORITHM_RSA_OAEP:
+    case QJS_ALGORITHM_RSA_PSS:
+    case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+
+#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
+
+        rsa = EVP_PKEY_get1_RSA(pkey);
+        if (rsa == NULL) {
+            qjs_webcrypto_error(cx, "RSA key is not found");
+            goto fail;
+        }
+
+        RSA_free(rsa);
+
+#else
+        if (!EVP_PKEY_is_a(pkey, "RSA")) {
+            qjs_webcrypto_error(cx, "RSA key is not found");
+            goto fail;
+        }
+#endif
+
+        ret = qjs_algorithm_hash(cx, options, &hash);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        if (wkey->hash != QJS_HASH_UNSET && wkey->hash != hash) {
+            JS_ThrowTypeError(cx, "RSA JWK hash mismatch");
+            goto fail;
+        }
+
+        if (wkey->u.a.privat) {
+            mask = (alg->type == QJS_ALGORITHM_RSA_OAEP)
+                         ? ~(QJS_KEY_USAGE_DECRYPT | QJS_KEY_USAGE_UNWRAP_KEY)
+                         : ~(QJS_KEY_USAGE_SIGN);
+        } else {
+            mask = (alg->type == QJS_ALGORITHM_RSA_OAEP)
+                         ? ~(QJS_KEY_USAGE_ENCRYPT | QJS_KEY_USAGE_WRAP_KEY)
+                         : ~(QJS_KEY_USAGE_VERIFY);
+        }
+
+        if (wkey->usage & mask) {
+            JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key",
+                              qjs_algorithm_string(alg));
+            goto fail;
+        }
+
+        wkey->hash = hash;
+        wkey->u.a.pkey = pkey;
+
+        break;
+
+    case QJS_ALGORITHM_ECDSA:
+    case QJS_ALGORITHM_ECDH:
+        ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        if (fmt == QJS_KEY_FORMAT_RAW) {
+            pkey = qjs_import_raw_ec(cx, &key_data, wkey);
+            if (pkey == NULL) {
+                goto fail;
+            }
+        }
+
+#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
+        ec = EVP_PKEY_get1_EC_KEY(pkey);
+        if (ec == NULL) {
+            qjs_webcrypto_error(cx, "EC key is not found");
+            goto fail;
+        }
+
+        group = EC_KEY_get0_group(ec);
+        nid = EC_GROUP_get_curve_name(group);
+        EC_KEY_free(ec);
+#else
+        if (!EVP_PKEY_is_a(pkey, "EC")) {
+            qjs_webcrypto_error(cx, "EC key is not found");
+            goto fail;
+        }
+
+        if (EVP_PKEY_get_group_name(pkey, gname, sizeof(gname), NULL) != 1) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_get_group_name() failed");
+            goto fail;
+        }
+
+        nid = OBJ_txt2nid(gname);
+#endif
+
+        if (wkey->u.a.curve != nid) {
+            qjs_webcrypto_error(cx, "name curve mismatch");
+            goto fail;
+        }
+
+        mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN : ~QJS_KEY_USAGE_VERIFY;
+
+        if (wkey->usage & mask) {
+            JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key",
+                              qjs_algorithm_string(alg));
+            goto fail;
+        }
+
+        wkey->u.a.pkey = pkey;
+        break;
+
+    case QJS_ALGORITHM_HMAC:
+        if (fmt == QJS_KEY_FORMAT_RAW) {
+            ret = qjs_algorithm_hash(cx, options, &wkey->hash);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+
+            wkey->u.s.raw.start = js_malloc(cx, key_data.length);
+            if (wkey->u.s.raw.start == NULL) {
+                JS_ThrowOutOfMemory(cx);
+                goto fail;
+            }
+
+            wkey->u.s.raw.length = key_data.length;
+            memcpy(wkey->u.s.raw.start, key_data.start, key_data.length);
+
+        } else {
+            /* QJS_KEY_FORMAT_JWK. */
+
+            ret = qjs_algorithm_hash(cx, options, &hash);
+            if (JS_IsException(ret)) {
+                goto fail;
+            }
+
+            if (wkey->hash != QJS_HASH_UNSET && wkey->hash != hash) {
+                JS_ThrowTypeError(cx, "HMAC JWK hash mismatch");
+                goto fail;
+            }
+        }
+
+        break;
+
+    case QJS_ALGORITHM_AES_GCM:
+    case QJS_ALGORITHM_AES_CTR:
+    case QJS_ALGORITHM_AES_CBC:
+        if (fmt == QJS_KEY_FORMAT_RAW) {
+            switch (key_data.length) {
+            case 16:
+            case 24:
+            case 32:
+                break;
+
+            default:
+                JS_ThrowTypeError(cx, "AES Invalid key length");
+                goto fail;
+            }
+
+            wkey->u.s.raw.start = js_malloc(cx, key_data.length);
+            if (wkey->u.s.raw.start == NULL) {
+                JS_ThrowOutOfMemory(cx);
+                goto fail;
+            }
+
+            wkey->u.s.raw.length = key_data.length;
+            memcpy(wkey->u.s.raw.start, key_data.start, key_data.length);
+        }
+
+        break;
+
+    case QJS_ALGORITHM_PBKDF2:
+    case QJS_ALGORITHM_HKDF:
+    default:
+        wkey->u.s.raw.start = js_malloc(cx, key_data.length);
+        if (wkey->u.s.raw.start == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            goto fail;
+        }
+
+        wkey->u.s.raw.length = key_data.length;
+        memcpy(wkey->u.s.raw.start, key_data.start, key_data.length);
+        break;
+    }
+
+    return qjs_webcrypto_result(cx, key, 0);
+
+fail:
+
+    if (pkey != NULL) {
+        EVP_PKEY_free(pkey);
+    }
+
+    JS_FreeValue(cx, key);
+
+    return qjs_webcrypto_result(cx, JS_UNDEFINED, -1);
+}
+
+
+static int
+qjs_set_rsa_padding(JSContext *cx, JSValue options, EVP_PKEY *pkey,
+    EVP_PKEY_CTX *ctx, qjs_webcrypto_alg_t type)
+{
+    int      padding, rc;
+    int64_t  salt_length;
+    JSValue  value;
+
+    if (type == QJS_ALGORITHM_ECDSA) {
+        return 0;
+    }
+
+    padding = (type == QJS_ALGORITHM_RSA_PSS) ? RSA_PKCS1_PSS_PADDING
+                                              : RSA_PKCS1_PADDING;
+    rc = EVP_PKEY_CTX_set_rsa_padding(ctx, padding);
+    if (rc <= 0) {
+        qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_rsa_padding() failed");
+        return -1;
+    }
+
+    if (padding == RSA_PKCS1_PSS_PADDING) {
+        value = JS_GetPropertyStr(cx, options, "saltLength");
+        if (JS_IsException(value)) {
+            return -1;
+        }
+
+        if (JS_IsUndefined(value)) {
+            JS_ThrowTypeError(cx, "RSA-PSS algorithm.saltLength is not "
+                              "provided");
+            return -1;
+        }
+
+        rc = JS_ToInt64(cx, &salt_length, value);
+        JS_FreeValue(cx, value);
+        if (rc < 0) {
+            return -1;
+        }
+
+        rc = EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, salt_length);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx,
+                                "EVP_PKEY_CTX_set_rsa_pss_saltlen() failed");
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+
+static unsigned int
+qjs_ec_rs_size(EVP_PKEY *pkey)
+{
+    int             bits;
+    const EC_KEY    *ec_key;
+    const EC_GROUP  *ec_group;
+
+    ec_key = njs_pkey_get_ec_key(pkey);
+    if (ec_key == NULL) {
+        return 0;
+    }
+
+    ec_group = EC_KEY_get0_group(ec_key);
+    if (ec_group == NULL) {
+        return 0;
+    }
+
+    bits = njs_ec_group_order_bits(ec_group);
+    if (bits == 0) {
+        return 0;
+    }
+
+    return (bits + 7) / 8;
+}
+
+
+static int
+qjs_convert_der_to_p1363(JSContext *cx, EVP_PKEY *pkey, const u_char *der,
+    size_t der_len, u_char **pout, size_t *out_len)
+{
+    u_char        *data;
+    unsigned      n;
+    ECDSA_SIG     *ec_sig;
+    const BIGNUM  *r, *s;
+
+    ec_sig = NULL;
+
+    n = qjs_ec_rs_size(pkey);
+    if (n == 0) {
+        return -1;
+    }
+
+    data = js_malloc(cx, 2 * n);
+    if (data == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return -1;
+    }
+
+    ec_sig = d2i_ECDSA_SIG(NULL, &der, der_len);
+    if (ec_sig == NULL) {
+        goto fail;
+    }
+
+#if (OPENSSL_VERSION_NUMBER >= 0x10100000L)
+    ECDSA_SIG_get0(ec_sig, &r, &s);
+#else
+    r = ec_sig->r;
+    s = ec_sig->s;
+#endif
+
+    if (BN_bn2binpad(r, data, n) <= 0) {
+        goto fail;
+    }
+
+    if (BN_bn2binpad(s, &data[n], n) <= 0) {
+        goto fail;
+    }
+
+    *pout = data;
+    *out_len = 2 * n;
+
+    ECDSA_SIG_free(ec_sig);
+
+    return 0;
+
+fail:
+
+    js_free(cx, data);
+
+    if (ec_sig != NULL) {
+        ECDSA_SIG_free(ec_sig);
+    }
+
+    return -1;
+}
+
+
+static int
+qjs_convert_p1363_to_der(JSContext *cx, EVP_PKEY *pkey, u_char *p1363,
+    size_t p1363_len, u_char **pout, size_t *out_len)
+{
+    int        len;
+    BIGNUM     *r, *s;
+    u_char     *data;
+    unsigned   n;
+    ECDSA_SIG  *ec_sig;
+
+    n = qjs_ec_rs_size(pkey);
+
+    if (n == 0 || p1363_len != 2 * n) {
+        JS_ThrowTypeError(cx, "invalid ECDSA signature length %zu != %u",
+                          p1363_len, 2 * n);
+        return -1;
+    }
+
+    ec_sig = ECDSA_SIG_new();
+    if (ec_sig == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return -1;
+    }
+
+    r = BN_bin2bn(p1363, n, NULL);
+    if (r == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        goto fail;
+    }
+
+    s = BN_bin2bn(&p1363[n], n, NULL);
+    if (s == NULL) {
+        BN_free(r);
+        JS_ThrowOutOfMemory(cx);
+        goto fail;
+    }
+
+    if (ECDSA_SIG_set0(ec_sig, r, s) != 1) {
+        BN_free(r);
+        BN_free(s);
+        JS_ThrowOutOfMemory(cx);
+        goto fail;
+    }
+
+    data = js_malloc(cx, 2 * n + 16);
+    if (data == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        goto fail;
+    }
+
+    *pout = data;
+    len = i2d_ECDSA_SIG(ec_sig, &data);
+
+    if (len < 0) {
+        js_free(cx, data);
+        JS_ThrowTypeError(cx, "i2d_ECDSA_SIG() failed");
+        goto fail;
+    }
+
+    *out_len = len;
+
+    ECDSA_SIG_free(ec_sig);
+
+    return 0;
+
+fail:
+
+    ECDSA_SIG_free(ec_sig);
+
+    return -1;
+}
+
+
+static JSValue
+qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int verify)
+{
+    int                        rc;
+    u_char                     *dst, *p, *p1363;
+    size_t                     olen, outlen;
+    JSValue                    ret, options;
+    unsigned                   mask, m_len;
+    njs_str_t                  data, sig;
+    EVP_MD_CTX                 *mctx;
+    EVP_PKEY_CTX               *pctx;
+    const EVP_MD               *md;
+    qjs_webcrypto_key_t        *key;
+    qjs_webcrypto_hash_t       hash;
+    qjs_webcrypto_algorithm_t  *alg;
+    unsigned char              m[EVP_MAX_MD_SIZE];
+
+    dst = NULL;
+    mctx = NULL;
+    pctx = NULL;
+
+    options = argv[0];
+
+    alg = qjs_key_algorithm(cx, options);
+    if (alg == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (key == NULL) {
+        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
+        return JS_EXCEPTION;
+    }
+
+    mask = verify ? QJS_KEY_USAGE_VERIFY : QJS_KEY_USAGE_SIGN;
+    if (!(key->usage & mask)) {
+        JS_ThrowTypeError(cx, "provide key does not support \"%s\" operation",
+                          verify ? "verify" : "sign");
+        return JS_EXCEPTION;
+    }
+
+    if (key->alg != alg) {
+        JS_ThrowTypeError(cx, "cannot %s using \"%s\" with \"%s\" key",
+                          verify ? "verify" : "sign",
+                          qjs_algorithm_string(key->alg),
+                          qjs_algorithm_string(alg));
+        return JS_EXCEPTION;
+    }
+
+    if (verify) {
+        ret = qjs_typed_array_data(cx, argv[2], &sig);
+        if (JS_IsException(ret)) {
+            return JS_EXCEPTION;
+        }
+
+        ret = qjs_typed_array_data(cx, argv[3], &data);
+        if (JS_IsException(ret)) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        ret = qjs_typed_array_data(cx, argv[2], &data);
+        if (JS_IsException(ret)) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    if (alg->type == QJS_ALGORITHM_ECDSA) {
+        ret = qjs_algorithm_hash(cx, options, &hash);
+        if (JS_IsException(ret)) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        hash = key->hash;
+    }
+
+    md = qjs_algorithm_hash_digest(hash);
+
+    /* Clang complains about uninitialized rc. */
+    rc = 0;
+    outlen = 0;
+
+    switch (alg->type) {
+    case QJS_ALGORITHM_HMAC:
+        m_len = EVP_MD_size(md);
+
+        if (!verify) {
+            dst = js_malloc(cx, m_len);
+            if (dst == NULL) {
+                JS_ThrowOutOfMemory(cx);
+                return JS_EXCEPTION;
+            }
+
+        } else {
+            dst = (u_char *) &m[0];
+        }
+
+        outlen = m_len;
+
+        p = HMAC(md, key->u.s.raw.start, key->u.s.raw.length, data.start,
+                 data.length, dst, &m_len);
+
+        if (p == NULL || m_len != outlen) {
+            qjs_webcrypto_error(cx, "HMAC() failed");
+            goto fail;
+        }
+
+        if (verify) {
+            rc = (sig.length == outlen && memcmp(sig.start, dst, outlen) == 0);
+        }
+
+        break;
+
+    case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+    case QJS_ALGORITHM_RSA_PSS:
+    case QJS_ALGORITHM_ECDSA:
+    default:
+        mctx = njs_evp_md_ctx_new();
+        if (mctx == NULL) {
+            qjs_webcrypto_error(cx, "njs_evp_md_ctx_new() failed");
+            goto fail;
+        }
+
+        rc = EVP_DigestInit_ex(mctx, md, NULL);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_DigestInit_ex() failed");
+            goto fail;
+        }
+
+        rc = EVP_DigestUpdate(mctx, data.start, data.length);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_DigestUpdate() failed");
+            goto fail;
+        }
+
+        rc = EVP_DigestFinal_ex(mctx, m, &m_len);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_DigestFinal_ex() failed");
+            goto fail;
+        }
+
+        olen = EVP_PKEY_size(key->u.a.pkey);
+        dst = js_malloc(cx, olen);
+        if (dst == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            goto fail;
+        }
+
+        pctx = EVP_PKEY_CTX_new(key->u.a.pkey, NULL);
+        if (pctx == NULL) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed");
+            goto fail;
+        }
+
+        if (!verify) {
+            rc = EVP_PKEY_sign_init(pctx);
+            if (rc <= 0) {
+                qjs_webcrypto_error(cx, "EVP_PKEY_sign_init() failed");
+                goto fail;
+            }
+
+        } else {
+            rc = EVP_PKEY_verify_init(pctx);
+            if (rc <= 0) {
+                qjs_webcrypto_error(cx, "EVP_PKEY_verify_init() failed");
+                goto fail;
+            }
+        }
+
+        rc = qjs_set_rsa_padding(cx, options, key->u.a.pkey, pctx, alg->type);
+        if (rc < 0) {
+            goto fail;
+        }
+
+        rc = EVP_PKEY_CTX_set_signature_md(pctx, md);
+        if (rc <= 0) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_signature_md() failed");
+            goto fail;
+        }
+
+        if (!verify) {
+            outlen = olen;
+            rc = EVP_PKEY_sign(pctx, dst, &outlen, m, m_len);
+            if (rc <= 0) {
+                qjs_webcrypto_error(cx, "EVP_PKEY_sign() failed");
+                goto fail;
+            }
+
+            if (alg->type == QJS_ALGORITHM_ECDSA) {
+                rc = qjs_convert_der_to_p1363(cx, key->u.a.pkey, dst, outlen,
+                                              &p1363, &outlen);
+                if (rc < 0) {
+                    goto fail;
+                }
+
+                js_free(cx, dst);
+                dst = p1363;
+            }
+
+        } else {
+            if (alg->type == QJS_ALGORITHM_ECDSA) {
+                rc = qjs_convert_p1363_to_der(cx, key->u.a.pkey, sig.start,
+                                              sig.length, &sig.start,
+                                              &sig.length);
+                if (rc < 0) {
+                    goto fail;
+                }
+            }
+
+            rc = EVP_PKEY_verify(pctx, sig.start, sig.length, m, m_len);
+
+            if (alg->type == QJS_ALGORITHM_ECDSA) {
+                js_free(cx, sig.start);
+            }
+
+            if (rc < 0) {
+                qjs_webcrypto_error(cx, "EVP_PKEY_verify() failed");
+                goto fail;
+            }
+
+            js_free(cx, dst);
+        }
+
+        njs_evp_md_ctx_free(mctx);
+        mctx = NULL;
+
+        EVP_PKEY_CTX_free(pctx);
+        pctx = NULL;
+    }
+
+    if (!verify) {
+        ret = qjs_new_array_buffer(cx, dst, outlen);
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+    } else {
+        ret = JS_NewBool(cx, rc != 0);
+    }
+
+    return qjs_webcrypto_result(cx, ret, 0);
+
+fail:
+
+    if (mctx != NULL) {
+        njs_evp_md_ctx_free(mctx);
+    }
+
+    if (pctx != NULL) {
+        EVP_PKEY_CTX_free(pctx);
+    }
+
+    if (dst != NULL) {
+        js_free(cx, dst);
+    }
+
+    return qjs_webcrypto_result(cx, JS_UNDEFINED, -1);
+}
+
+
+static JSValue
+qjs_webcrypto_key_to_string_tag(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewString(cx, "CryptoKey");
+}
+
+
+static JSValue
+qjs_webcrypto_key_algorithm(JSContext *cx, JSValueConst this_val)
+{
+    JSValue              obj, ret, hash, len, pe;
+    njs_str_t            *name, pe_data;
+    const BIGNUM         *n_bn, *e_bn;
+    const EC_GROUP       *group;
+    qjs_webcrypto_key_t  *key;
+
+    key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (key == NULL) {
+        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
+        return JS_EXCEPTION;
+    }
+
+    name = &qjs_webcrypto_alg[key->alg->type].name;
+
+    obj = JS_NewObject(cx);
+    if (JS_IsException(obj)) {
+        return JS_EXCEPTION;
+    }
+
+    ret = JS_NewStringLen(cx, (const char *) name->start, name->length);
+    if (JS_IsException(ret)) {
+        JS_FreeValue(cx, obj);
+        return JS_EXCEPTION;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, obj, "name", ret, JS_PROP_C_W_E)
+        < 0)
+    {
+        JS_FreeValue(cx, obj);
+        return JS_EXCEPTION;
+    }
+
+    switch (key->alg->type) {
+    case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+    case QJS_ALGORITHM_RSA_PSS:
+    case QJS_ALGORITHM_RSA_OAEP:
+        /* RsaHashedKeyGenParams. */
+
+        njs_assert(key->u.a.pkey != NULL);
+        njs_assert(EVP_PKEY_id(key->u.a.pkey) == EVP_PKEY_RSA);
+
+        njs_rsa_get0_key(njs_pkey_get_rsa_key(key->u.a.pkey), &n_bn, &e_bn,
+                         NULL);
+
+        if (JS_DefinePropertyValueStr(cx, obj, "modulusLength",
+                                      JS_NewInt32(cx, BN_num_bits(n_bn)),
+                                                  JS_PROP_C_W_E)
+            < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        len = JS_NewInt32(cx, BN_num_bytes(e_bn));
+        pe = qjs_new_uint8_array(cx, 1, &len);
+        if (JS_IsException(pe)) {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        ret = qjs_typed_array_data(cx, pe, &pe_data);
+        if (JS_IsException(ret)) {
+            JS_FreeValue(cx, pe);
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        BN_bn2bin(e_bn, pe_data.start);
+
+        if (JS_DefinePropertyValueStr(cx, obj, "publicExponent", pe,
+                                      JS_PROP_C_W_E)
+            < 0)
+        {
+            JS_FreeValue(cx, pe);
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        hash = JS_NewString(cx, qjs_algorithm_hash_name(key->hash));
+        if (JS_IsException(hash)) {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        ret = JS_NewObject(cx);
+        if (JS_IsException(ret)) {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueStr(cx, ret, "name", hash, JS_PROP_C_W_E)
+            < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueStr(cx, obj, "hash", ret, JS_PROP_C_W_E)
+            < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_AES_GCM:
+    case QJS_ALGORITHM_AES_CTR:
+    case QJS_ALGORITHM_AES_CBC:
+        /* AesKeyGenParams. */
+
+        if (JS_DefinePropertyValueStr(cx, obj, "length",
+                                      JS_NewInt32(cx, key->u.s.raw.length * 8),
+                                      JS_PROP_C_W_E)
+            < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_ECDSA:
+    case QJS_ALGORITHM_ECDH:
+        /* EcKeyGenParams. */
+
+        njs_assert(key->u.a.pkey != NULL);
+        njs_assert(EVP_PKEY_id(key->u.a.pkey) == EVP_PKEY_EC);
+
+        group = EC_KEY_get0_group(njs_pkey_get_ec_key(key->u.a.pkey));
+        name = qjs_algorithm_curve_name(EC_GROUP_get_curve_name(group));
+
+        ret = JS_NewStringLen(cx, (const char *) name->start, name->length);
+        if (JS_IsException(ret)) {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueStr(cx, obj, "namedCurve", ret, JS_PROP_C_W_E)
+            < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        break;
+
+    case QJS_ALGORITHM_HMAC:
+    default:
+         /* HmacKeyGenParams */
+
+        hash = JS_NewString(cx, qjs_algorithm_hash_name(key->hash));
+        if (JS_IsException(hash)) {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        ret = JS_NewObject(cx);
+        if (JS_IsException(ret)) {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueStr(cx, ret, "name", hash, JS_PROP_C_W_E)
+            < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueStr(cx, obj, "hash", ret, JS_PROP_C_W_E)
+            < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+        break;
+    }
+
+    return obj;
+}
+
+
+static JSValue
+qjs_webcrypto_key_extractable(JSContext *cx, JSValueConst this_val)
+{
+    qjs_webcrypto_key_t  *key;
+
+    key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (key == NULL) {
+        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
+        return JS_EXCEPTION;
+    }
+
+    return JS_NewBool(cx, key->extractable);
+}
+
+
+static JSValue
+qjs_webcrypto_key_type(JSContext *cx, JSValueConst this_val)
+{
+    qjs_webcrypto_key_t  *key;
+
+    key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (key == NULL) {
+        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
+        return JS_EXCEPTION;
+    }
+
+    if (key->alg->raw) {
+        return JS_NewString(cx, "secret");
+    }
+
+    return JS_NewString(cx, key->u.a.privat ? "private": "public");
+}
+
+
+static JSValue
+qjs_webcrypto_key_usages(JSContext *cx, JSValueConst this_val)
+{
+    qjs_webcrypto_key_t  *key;
+
+    key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (key == NULL) {
+        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
+        return JS_EXCEPTION;
+    }
+
+    return qjs_key_ops(cx, key->usage);
+}
+
+
+static JSValue
+qjs_get_random_values(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    JSValue    buffer, ret;
+    njs_str_t  fill;
+
+    buffer = JS_DupValue(cx, argv[0]);
+
+    ret = qjs_typed_array_data(cx, buffer, &fill);
+    if (JS_IsException(ret)) {
+        JS_FreeValue(cx, buffer);
+        return JS_EXCEPTION;
+    }
+
+    if (fill.length > 65536) {
+        JS_ThrowTypeError(cx, "requested length exceeds 65536 bytes");
+        JS_FreeValue(cx, buffer);
+        return JS_EXCEPTION;
+    }
+
+    if (RAND_bytes(fill.start, fill.length) != 1) {
+        JS_FreeValue(cx, buffer);
+        qjs_webcrypto_error(cx, "RAND_bytes() failed");
+        return JS_EXCEPTION;
+    }
+
+    return buffer;
+}
+
+
+static JSValue
+qjs_webcrypto_key_make(JSContext *cx, qjs_webcrypto_algorithm_t *alg,
+  unsigned usage, int extractable)
+{
+    JSValue              key;
+    qjs_webcrypto_key_t  *k;
+
+    key = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    if (JS_IsException(key)) {
+        return JS_EXCEPTION;
+    }
+
+    k = js_mallocz(cx, sizeof(qjs_webcrypto_key_t));
+    if (k == NULL) {
+        return JS_ThrowOutOfMemory(cx);
+    }
+
+    /*
+     *  k->u.a.pkey = NULL;
+     *  k->u.s.raw.length = 0;
+     *  k->u.s.raw.start = NULL;
+     *  k->u.a.curve = 0;
+     *  k->u.a.privat = 0;
+     *  k->hash = QJS_HASH_UNSET;
+     */
+
+    k->alg = alg;
+    k->usage = usage;
+    k->extractable = extractable;
+
+    JS_SetOpaque(key, k);
+
+    return key;
+}
+
+
+static void
+qjs_webcrypto_key_finalizer(JSRuntime *rt, JSValue val)
+{
+    qjs_webcrypto_key_t  *key;
+
+    key = JS_GetOpaque(val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+
+    if (key != NULL) {
+        if (!key->alg->raw) {
+            if (key->u.a.pkey != NULL) {
+                EVP_PKEY_free(key->u.a.pkey);
+            }
+
+        } else {
+            if (key->u.s.raw.start != NULL) {
+                js_free_rt(rt, key->u.s.raw.start);
+            }
+        }
+
+        js_free_rt(rt, key);
+    }
+}
+
+
+static qjs_webcrypto_key_format_t
+qjs_key_format(JSContext *cx, JSValueConst value)
+{
+    njs_str_t              format;
+    qjs_webcrypto_entry_t  *e;
+
+    format.start = (u_char *) JS_ToCStringLen(cx, &format.length, value);
+    if (format.start == NULL) {
+        return QJS_KEY_FORMAT_UNKNOWN;
+    }
+
+    for (e = &qjs_webcrypto_format[0]; e->name.length != 0; e++) {
+        if (njs_strstr_eq(&format, &e->name)) {
+            JS_FreeCString(cx, (char *) format.start);
+            return e->value;
+        }
+    }
+
+    JS_ThrowTypeError(cx, "unknown key format: \"%s\"", format.start);
+    JS_FreeCString(cx, (char *) format.start);
+
+    return QJS_KEY_FORMAT_UNKNOWN;
+}
+
+
+static qjs_webcrypto_jwk_kty_t
+qjs_jwk_kty(JSContext *cx, JSValueConst value)
+{
+    njs_str_t              kty;
+    qjs_webcrypto_entry_t  *e;
+
+    kty.start = (u_char *) JS_ToCStringLen(cx, &kty.length, value);
+    if (kty.start == NULL) {
+        return QJS_KEY_JWK_KTY_UNKNOWN;
+    }
+
+    for (e = &qjs_webcrypto_jwk_kty[0]; e->name.length != 0; e++) {
+        if (njs_strstr_eq(&kty, &e->name)) {
+            JS_FreeCString(cx, (char *) kty.start);
+            return e->value;
+        }
+    }
+
+    JS_ThrowTypeError(cx, "invalid JWK key type: %s", kty.start);
+    JS_FreeCString(cx, (char *) kty.start);
+
+    return QJS_KEY_JWK_KTY_UNKNOWN;
+}
+
+
+static qjs_webcrypto_algorithm_t *
+qjs_key_algorithm(JSContext *cx, JSValue options)
+{
+    JSValue                    v;
+    njs_str_t                  a;
+    qjs_webcrypto_entry_t      *e;
+    qjs_webcrypto_algorithm_t  *alg;
+
+    if (JS_IsObject(options)) {
+        v = JS_GetPropertyStr(cx, options, "name");
+        if (JS_IsException(v)) {
+            return NULL;
+        }
+
+    } else {
+        v = JS_DupValue(cx, options);
+    }
+
+    a.start = (u_char *) JS_ToCStringLen(cx, &a.length, v);
+    JS_FreeValue(cx, v);
+    if (a.start == NULL) {
+        return NULL;
+    }
+
+    for (e = &qjs_webcrypto_alg[0]; e->name.length != 0; e++) {
+        if (njs_strstr_case_eq(&a, &e->name)) {
+            alg = (qjs_webcrypto_algorithm_t *) e->value;
+            if (alg->usage & QJS_KEY_USAGE_UNSUPPORTED) {
+                JS_ThrowTypeError(cx, "unsupported algorithm: \"%.*s\"",
+                                  (int) a.length, a.start);
+                JS_FreeCString(cx, (char *) a.start);
+                return NULL;
+            }
+
+            JS_FreeCString(cx, (char *) a.start);
+            return alg;
+        }
+    }
+
+    JS_ThrowTypeError(cx, "unknown algorithm name: \"%.*s\"", (int) a.length,
+                      a.start);
+    JS_FreeCString(cx, (char *) a.start);
+
+    return NULL;
+}
+
+
+static JSValue
+qjs_algorithm_curve(JSContext *cx, JSValue options, int *curve)
+{
+    JSValue                v;
+    njs_str_t              name;
+    qjs_webcrypto_entry_t  *e;
+
+    if (JS_IsObject(options)) {
+        v = JS_GetPropertyStr(cx, options, "namedCurve");
+        if (JS_IsException(v)) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        v = JS_DupValue(cx, options);
+    }
+
+    name.start = (u_char *) JS_ToCStringLen(cx, &name.length, v);
+    JS_FreeValue(cx, v);
+    if (name.start == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) {
+        if (njs_strstr_eq(&name, &e->name)) {
+            JS_FreeCString(cx, (char *) name.start);
+            *curve = e->value;
+            return JS_UNDEFINED;
+        }
+    }
+
+    JS_ThrowTypeError(cx, "unknown namedCurve: \"%.*s\"", (int) name.length,
+                      name.start);
+    JS_FreeCString(cx, (char *) name.start);
+
+    return JS_EXCEPTION;
+}
+
+
+static JSValue
+qjs_algorithm_hash(JSContext *cx, JSValue options, qjs_webcrypto_hash_t *hash)
+{
+    JSValue                v;
+    njs_str_t              name;
+    qjs_webcrypto_entry_t  *e;
+
+    if (JS_IsObject(options)) {
+        v = JS_GetPropertyStr(cx, options, "hash");
+        if (JS_IsException(v)) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        v = JS_DupValue(cx, options);
+    }
+
+    name.start = (u_char *) JS_ToCStringLen(cx, &name.length, v);
+    JS_FreeValue(cx, v);
+    if (name.start == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    for (e = &qjs_webcrypto_hash[0]; e->name.length != 0; e++) {
+        if (njs_strstr_eq(&name, &e->name)) {
+            JS_FreeCString(cx, (char *) name.start);
+            *hash = e->value;
+            return JS_UNDEFINED;
+        }
+    }
+
+    JS_ThrowTypeError(cx, "unknown hash name: \"%.*s\"", (int) name.length,
+                      name.start);
+    JS_FreeCString(cx, (char *) name.start);
+
+    return JS_EXCEPTION;
+}
+
+
+static const EVP_MD *
+qjs_algorithm_hash_digest(qjs_webcrypto_hash_t hash)
+{
+    switch (hash) {
+    case QJS_HASH_SHA256:
+        return EVP_sha256();
+
+    case QJS_HASH_SHA384:
+        return EVP_sha384();
+
+    case QJS_HASH_SHA512:
+        return EVP_sha512();
+
+    case QJS_HASH_SHA1:
+    default:
+        break;
+    }
+
+    return EVP_sha1();
+}
+
+
+static njs_str_t *
+qjs_algorithm_curve_name(int curve)
+{
+    qjs_webcrypto_entry_t  *e;
+
+    for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) {
+        if (e->value == (uintptr_t) curve) {
+            return &e->name;
+        }
+    }
+
+    return &e->name;
+}
+
+
+static const char *
+qjs_format_string(qjs_webcrypto_key_format_t fmt)
+{
+    qjs_webcrypto_entry_t  *e;
+
+    for (e = &qjs_webcrypto_format[0]; e->name.length != 0; e++) {
+        if (fmt == e->value) {
+            break;
+        }
+    }
+
+    return (const char *) e->name.start;
+}
+
+
+static const char *
+qjs_algorithm_string(qjs_webcrypto_algorithm_t *algorithm)
+{
+    qjs_webcrypto_entry_t      *e;
+    qjs_webcrypto_algorithm_t  *alg;
+
+    for (e = &qjs_webcrypto_alg[0]; e->name.length != 0; e++) {
+        alg = (qjs_webcrypto_algorithm_t *) e->value;
+        if (alg->type == algorithm->type) {
+            break;
+        }
+    }
+
+    return (const char *) e->name.start;
+}
+
+
+static const char *
+qjs_algorithm_hash_name(qjs_webcrypto_hash_t hash)
+{
+    qjs_webcrypto_entry_t  *e;
+
+    for (e = &qjs_webcrypto_hash[0]; e->name.length != 0; e++) {
+        if (e->value == hash) {
+            break;
+        }
+    }
+
+    return (const char *) e->name.start;
+}
+
+
+static JSValue
+qjs_key_usage(JSContext *cx, JSValue value, unsigned *mask)
+{
+    int64_t                length;
+    JSValue                v;
+    uint32_t               i;
+    njs_str_t              s;
+    qjs_webcrypto_entry_t  *e;
+
+    if (!JS_IsArray(cx, value)) {
+        JS_ThrowTypeError(cx, "\"keyUsages\" argument must be an Array");
+        return JS_EXCEPTION;
+    }
+
+    v = JS_GetPropertyStr(cx, value, "length");
+    if (JS_IsException(v)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_ToInt64(cx, &length, v) < 0) {
+        JS_FreeValue(cx, v);
+        return JS_EXCEPTION;
+    }
+
+    JS_FreeValue(cx, v);
+
+    *mask = 0;
+
+    for (i = 0; i < length; i++) {
+        v = JS_GetPropertyUint32(cx, value, i);
+        if (JS_IsException(v)) {
+            return JS_EXCEPTION;
+        }
+
+        s.start = (u_char *) JS_ToCStringLen(cx, &s.length, v);
+        JS_FreeValue(cx, v);
+        if (s.start == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            return JS_EXCEPTION;
+        }
+
+        for (e = &qjs_webcrypto_usage[0]; e->name.length != 0; e++) {
+            if (njs_strstr_eq(&s, &e->name)) {
+                *mask |= e->value;
+                break;
+            }
+        }
+
+        JS_FreeCString(cx, (char *) s.start);
+    }
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+qjs_key_ops(JSContext *cx, unsigned mask)
+{
+    uint32_t               i;
+    JSValue                ops, value;
+    qjs_webcrypto_entry_t  *e;
+
+    ops = JS_NewArray(cx);
+    if (JS_IsException(ops)) {
+        return JS_EXCEPTION;
+    }
+
+    i = 0;
+
+    for (e = &qjs_webcrypto_usage[0]; e->name.length != 0; e++) {
+        if (mask & e->value) {
+            value = JS_NewStringLen(cx, (const char *) e->name.start,
+                                    e->name.length);
+            if (JS_IsException(value)) {
+                JS_FreeValue(cx, ops);
+                return JS_EXCEPTION;
+            }
+
+            if (JS_SetPropertyUint32(cx, ops, i++, value) < 0) {
+                JS_FreeValue(cx, ops);
+                JS_FreeValue(cx, value);
+                return JS_EXCEPTION;
+            }
+        }
+    }
+
+    return ops;
+}
+
+
+static u_char *
+qjs_cpystrn(u_char *dst, u_char *src, size_t n)
+{
+    if (n == 0) {
+        return dst;
+    }
+
+    while (--n) {
+        *dst = *src;
+
+        if (*dst == '\0') {
+            return dst;
+        }
+
+        dst++;
+        src++;
+    }
+
+    *dst = '\0';
+
+    return dst;
+}
+
+
+static JSValue
+qjs_webcrypto_promise_trampoline(JSContext *cx, int argc, JSValueConst *argv)
+{
+    return JS_Call(cx, argv[0], JS_UNDEFINED, 1, &argv[1]);
+}
+
+
+static JSValue
+qjs_webcrypto_result(JSContext *cx, JSValue result, int rc)
+{
+    JS_BOOL  is_error;
+    JSValue  promise, callbacks[2], arguments[2];
+
+    promise = JS_NewPromiseCapability(cx, callbacks);
+    if (JS_IsException(promise)) {
+        JS_FreeValue(cx, result);
+        return JS_EXCEPTION;
+    }
+
+    is_error = !!(rc != 0);
+
+    JS_FreeValue(cx, callbacks[!is_error]);
+    arguments[0] = callbacks[is_error];
+    arguments[1] = is_error ? JS_GetException(cx) : result;
+
+    if (JS_EnqueueJob(cx, qjs_webcrypto_promise_trampoline, 2, arguments) < 0) {
+        JS_FreeValue(cx, promise);
+        JS_FreeValue(cx, callbacks[is_error]);
+        JS_FreeValue(cx, arguments[1]);
+        return JS_EXCEPTION;
+    }
+
+    JS_FreeValue(cx, arguments[0]);
+    JS_FreeValue(cx, arguments[1]);
+
+    return promise;
+}
+
+
+static void
+qjs_webcrypto_error(JSContext *cx, const char *fmt, ...)
+{
+    int            flags;
+    u_char         *p, *last;
+    va_list        args;
+    const char     *data;
+    unsigned long  n;
+    u_char         errstr[NJS_MAX_ERROR_STR];
+
+    last = &errstr[NJS_MAX_ERROR_STR];
+
+    va_start(args, fmt);
+    p = njs_vsprintf(errstr, last - 1, fmt, args);
+    va_end(args);
+
+    if (ERR_peek_error()) {
+        p = qjs_cpystrn(p, (u_char *) " (SSL:", last - p);
+
+        for ( ;; ) {
+
+            n = ERR_peek_error_data(&data, &flags);
+
+            if (n == 0) {
+                break;
+            }
+
+            /* ERR_error_string_n() requires at least one byte */
+
+            if (p >= last - 1) {
+                goto next;
+            }
+
+            *p++ = ' ';
+
+            ERR_error_string_n(n, (char *) p, last - p);
+
+            while (p < last && *p) {
+                p++;
+            }
+
+            if (p < last && *data && (flags & ERR_TXT_STRING)) {
+                *p++ = ':';
+                p = qjs_cpystrn(p, (u_char *) data, last - p);
+            }
+
+        next:
+
+            (void) ERR_get_error();
+        }
+
+        if (p < last) {
+            *p++ = ')';
+        }
+    }
+
+    JS_ThrowTypeError(cx, "%*s", (int) (p - errstr), errstr);
+}
+
+
+static int
+qjs_webcrypto_module_init(JSContext *cx, JSModuleDef *m)
+{
+    int      rc;
+    JSValue  proto;
+
+    proto = JS_NewObject(cx);
+    JS_SetPropertyFunctionList(cx, proto, qjs_webcrypto_export,
+                               njs_nitems(qjs_webcrypto_export));
+
+    rc = JS_SetModuleExport(cx, m, "default", proto);
+    if (rc != 0) {
+        return -1;
+    }
+
+    return JS_SetModuleExportList(cx, m, qjs_webcrypto_export,
+                                  njs_nitems(qjs_webcrypto_export));
+}
+
+
+static JSModuleDef *
+qjs_webcrypto_init(JSContext *cx, const char *name)
+{
+    int          rc;
+    JSValue      crypto, proto, global_obj;
+    JSModuleDef  *m;
+
+    (void) qjs_webcrypto_alg_name;
+    (void) qjs_algorithm_hash_name;
+
+    if (!JS_IsRegisteredClass(JS_GetRuntime(cx),
+                              QJS_CORE_CLASS_ID_WEBCRYPTO_KEY))
+    {
+        if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_WEBCRYPTO_KEY,
+                        &qjs_webcrypto_key_class))
+        {
+            return NULL;
+        }
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, qjs_webcrypto_key_proto,
+                                   njs_nitems(qjs_webcrypto_key_proto));
+
+        JS_SetClassProto(cx, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY, proto);
+    }
+
+    global_obj = JS_GetGlobalObject(cx);
+
+    crypto = JS_NewObject(cx);
+    JS_SetPropertyFunctionList(cx, crypto, qjs_webcrypto_export,
+                               njs_nitems(qjs_webcrypto_export));
+
+    rc = JS_SetPropertyStr(cx, global_obj, "crypto", crypto);
+    if (rc == -1) {
+        return NULL;
+    }
+
+    JS_FreeValue(cx, global_obj);
+
+    m = JS_NewCModule(cx, name, qjs_webcrypto_module_init);
+    if (m == NULL) {
+        return NULL;
+    }
+
+    JS_AddModuleExport(cx, m, "default");
+    rc = JS_AddModuleExportList(cx, m, qjs_webcrypto_export,
+                                njs_nitems(qjs_webcrypto_export));
+    if (rc != 0) {
+        return NULL;
+    }
+
+    return m;
+}
diff --git a/nginx/config b/nginx/config
index 8e920477..2edf7a3d 100644
--- a/nginx/config
+++ b/nginx/config
@@ -117,6 +117,10 @@ if [ $NJS_OPENSSL != NO ]; then
     have=NJS_HAVE_OPENSSL . auto/have
     NJS_SRCS="$NJS_SRCS $ngx_addon_dir/../external/njs_webcrypto_module.c"
 
+    if [ "$NJS_HAVE_QUICKJS" = "YES" ];  then
+        NJS_SRCS="$NJS_SRCS $ngx_addon_dir/../external/qjs_webcrypto_module.c"
+    fi
+
     echo " enabled webcrypto module"
 fi
 
diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 9b6d8ea1..04d06fb2 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -1141,6 +1141,9 @@ qjs_module_t *njs_http_qjs_addon_modules[] = {
      * Shared addons should be in the same order and the same positions
      * in all nginx modules.
      */
+#ifdef NJS_HAVE_OPENSSL
+    &qjs_webcrypto_module,
+#endif
 #ifdef NJS_HAVE_ZLIB
     &qjs_zlib_module,
 #endif
diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h
index 11dba9ed..833c1490 100644
--- a/nginx/ngx_js.h
+++ b/nginx/ngx_js.h
@@ -377,6 +377,7 @@ ngx_int_t ngx_qjs_string(ngx_engine_t *e, JSValueConst val, ngx_str_t *str);
 #define ngx_qjs_external_ctx(cx, e)                                          \
     ((ngx_js_external_ctx_pt) ngx_qjs_meta(cx, 11))(e)
 
+extern qjs_module_t  qjs_webcrypto_module;
 extern qjs_module_t  qjs_zlib_module;
 extern qjs_module_t  ngx_qjs_ngx_module;
 extern qjs_module_t  ngx_qjs_ngx_shared_dict_module;
diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c
index 98427aae..5f4c6a04 100644
--- a/nginx/ngx_stream_js_module.c
+++ b/nginx/ngx_stream_js_module.c
@@ -840,6 +840,9 @@ qjs_module_t *njs_stream_qjs_addon_modules[] = {
      * Shared addons should be in the same order and the same positions
      * in all nginx modules.
      */
+#ifdef NJS_HAVE_OPENSSL
+    &qjs_webcrypto_module,
+#endif
 #ifdef NJS_HAVE_ZLIB
     &qjs_zlib_module,
 #endif
diff --git a/nginx/t/js_webcrypto.t b/nginx/t/js_webcrypto.t
new file mode 100644
index 00000000..f43f9d6d
--- /dev/null
+++ b/nginx/t/js_webcrypto.t
@@ -0,0 +1,85 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, WebCrypto module.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http/)
+	->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /random_values_test {
+            js_content test.random_values_test;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function count1(v) {
+        return v.toString(2).match(/1/g).length;
+    }
+
+    /*
+     * Statistic test
+     * bits1 is a random variable with Binomial distribution
+     * Expected value is N / 2
+     * Standard deviation is sqrt(N / 4)
+     */
+
+    function random_values_test(r) {
+        let buf = new Uint32Array(32);
+        crypto.getRandomValues(buf);
+        let bits1 = buf.reduce((a, v)=> a + count1(v), 0);
+        let nbits = buf.length * 32;
+        let mean = nbits / 2;
+        let stdd = Math.sqrt(nbits / 4);
+
+        r.return(200, bits1 > (mean - 10 * stdd) && bits1 < (mean + 10 * stdd));
+    }
+
+    export default {random_values_test};
+
+EOF
+
+$t->try_run('no njs')->plan(1);
+
+###############################################################################
+
+like(http_get('/random_values_test'), qr/true/, 'random_values_test');
+
+###############################################################################
diff --git a/nginx/t/stream_js_webcrypto.t b/nginx/t/stream_js_webcrypto.t
new file mode 100644
index 00000000..d7b34396
--- /dev/null
+++ b/nginx/t/stream_js_webcrypto.t
@@ -0,0 +1,83 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, WebCrypto module.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+	->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    js_set $test test.random_values_test;
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $test;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function count1(v) {
+        return v.toString(2).match(/1/g).length;
+    }
+
+    /*
+     * Statistic test
+     * bits1 is a random variable with Binomial distribution
+     * Expected value is N / 2
+     * Standard deviation is sqrt(N / 4)
+     */
+
+    function random_values_test(s) {
+        let buf = new Uint32Array(32);
+        crypto.getRandomValues(buf);
+        let bits1 = buf.reduce((a, v)=> a + count1(v), 0);
+        let nbits = buf.length * 32;
+        let mean = nbits / 2;
+        let stdd = Math.sqrt(nbits / 4);
+
+        return bits1 > (mean - 10 * stdd) && bits1 < (mean + 10 * stdd);
+    }
+
+    export default {random_values_test};
+EOF
+
+$t->try_run('no stream js_var')->plan(1);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->io('###'), 'true', 'random values test');
+
+###############################################################################
diff --git a/src/qjs.c b/src/qjs.c
index 8c5b0b6d..56c6a3ba 100644
--- a/src/qjs.c
+++ b/src/qjs.c
@@ -1027,6 +1027,20 @@ qjs_typed_array_data(JSContext *ctx, JSValueConst value, njs_str_t *data)
 }
 
 
+static void
+js_array_buffer_free(JSRuntime *rt, void *opaque, void *ptr)
+{
+    js_free_rt(rt, ptr);
+}
+
+
+JSValue
+qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len)
+{
+    return JS_NewArrayBuffer(cx, src, len, js_array_buffer_free, NULL, 0);
+}
+
+
 JSValue
 qjs_string_create_chb(JSContext *cx, njs_chb_t *chain)
 {
diff --git a/src/qjs.h b/src/qjs.h
index 71dd297e..2d3c289c 100644
--- a/src/qjs.h
+++ b/src/qjs.h
@@ -18,6 +18,7 @@
 #include <njs_utf8.h>
 #include <njs_chb.h>
 #include <njs_utils.h>
+#include <njs_assert.h>
 
 #if defined(__GNUC__) && (__GNUC__ >= 8)
 #pragma GCC diagnostic push
@@ -32,6 +33,9 @@
 #include <pthread.h>
 
 
+#define QJS_CORE_CLASS_ID_OFFSET    64
+#define QJS_CORE_CLASS_ID_BUFFER    (QJS_CORE_CLASS_ID_OFFSET)
+#define QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR (QJS_CORE_CLASS_ID_OFFSET + 1)
 #define QJS_CORE_CLASS_ID_OFFSET    64
 #define QJS_CORE_CLASS_ID_BUFFER    (QJS_CORE_CLASS_ID_OFFSET)
 #define QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR (QJS_CORE_CLASS_ID_OFFSET + 1)
@@ -40,7 +44,8 @@
 #define QJS_CORE_CLASS_ID_FS_STATS  (QJS_CORE_CLASS_ID_OFFSET + 4)
 #define QJS_CORE_CLASS_ID_FS_DIRENT (QJS_CORE_CLASS_ID_OFFSET + 5)
 #define QJS_CORE_CLASS_ID_FS_FILEHANDLE (QJS_CORE_CLASS_ID_OFFSET + 6)
-#define QJS_CORE_CLASS_ID_LAST      (QJS_CORE_CLASS_ID_OFFSET + 7)
+#define QJS_CORE_CLASS_ID_WEBCRYPTO_KEY (QJS_CORE_CLASS_ID_OFFSET + 7)
+#define QJS_CORE_CLASS_ID_LAST      (QJS_CORE_CLASS_ID_OFFSET + 8)
 
 
 typedef JSModuleDef *(*qjs_addon_init_pt)(JSContext *ctx, const char *name);
@@ -55,6 +60,7 @@ JSContext *qjs_new_context(JSRuntime *rt, qjs_module_t **addons);
 
 
 JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv);
+JSValue qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len);
 JSValue qjs_buffer_alloc(JSContext *ctx, size_t size);
 JSValue qjs_buffer_create(JSContext *ctx, u_char *start, size_t size);
 JSValue qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain);
@@ -75,6 +81,22 @@ typedef struct {
 const qjs_buffer_encoding_t *qjs_buffer_encoding(JSContext *ctx,
     JSValueConst value, JS_BOOL thrw);
 
+int qjs_base64_encode(JSContext *ctx, const njs_str_t *src,
+    njs_str_t *dst);
+size_t qjs_base64_encode_length(JSContext *ctx, const njs_str_t *src);
+int qjs_base64_decode(JSContext *ctx, const njs_str_t *src,
+    njs_str_t *dst);
+size_t qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src);
+int qjs_base64url_encode(JSContext *ctx, const njs_str_t *src,
+    njs_str_t *dst);
+int qjs_base64url_decode(JSContext *ctx, const njs_str_t *src,
+    njs_str_t *dst);
+size_t qjs_base64url_decode_length(JSContext *ctx, const njs_str_t *src);
+int qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst);
+size_t qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src);
+int qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst);
+size_t qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src);
+
 JSValue qjs_process_object(JSContext *ctx, int argc, const char **argv);
 
 typedef struct {
diff --git a/src/qjs_buffer.c b/src/qjs_buffer.c
index 9840377f..8fc296ad 100644
--- a/src/qjs_buffer.c
+++ b/src/qjs_buffer.c
@@ -75,21 +75,6 @@ static JSValue qjs_buffer_from_object(JSContext *ctx, JSValueConst obj);
 static JSValue qjs_buffer_compare_array(JSContext *ctx, JSValue val1,
     JSValue val2, JSValueConst target_start, JSValueConst target_end,
     JSValueConst source_start, JSValueConst source_end);
-static int qjs_base64_encode(JSContext *ctx, const njs_str_t *src,
-    njs_str_t *dst);
-static size_t qjs_base64_encode_length(JSContext *ctx, const njs_str_t *src);
-static int qjs_base64_decode(JSContext *ctx, const njs_str_t *src,
-    njs_str_t *dst);
-static size_t qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src);
-static int qjs_base64url_encode(JSContext *ctx, const njs_str_t *src,
-    njs_str_t *dst);
-static int qjs_base64url_decode(JSContext *ctx, const njs_str_t *src,
-    njs_str_t *dst);
-static size_t qjs_base64url_decode_length(JSContext *ctx, const njs_str_t *src);
-static int qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst);
-static size_t qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src);
-static int qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst);
-static size_t qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src);
 static JSModuleDef *qjs_buffer_init(JSContext *ctx, const char *name);
 
 
@@ -2175,7 +2160,7 @@ qjs_base64_encode_core(njs_str_t *dst, const njs_str_t *src,
 }
 
 
-static int
+int
 qjs_base64_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 {
     qjs_base64_encode_core(dst, src, qjs_basis64_enc, 1);
@@ -2184,7 +2169,7 @@ qjs_base64_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 }
 
 
-static size_t
+size_t
 qjs_base64_encode_length(JSContext *ctx, const njs_str_t *src)
 {
     return qjs_base64_encoded_length(src->length);
@@ -2245,7 +2230,7 @@ qjs_base64_decode_length_core(const njs_str_t *src, const u_char *basis)
 }
 
 
-static int
+int
 qjs_base64_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 {
     qjs_base64_decode_core(dst, src, qjs_basis64);
@@ -2254,14 +2239,14 @@ qjs_base64_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 }
 
 
-static size_t
+size_t
 qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src)
 {
     return qjs_base64_decode_length_core(src, qjs_basis64);
 }
 
 
-static int
+int
 qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 {
     qjs_base64_encode_core(dst, src, qjs_basis64url_enc, 0);
@@ -2270,7 +2255,7 @@ qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 }
 
 
-static int
+int
 qjs_base64url_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 {
     qjs_base64_decode_core(dst, src, qjs_basis64url);
@@ -2279,7 +2264,7 @@ qjs_base64url_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 }
 
 
-static size_t
+size_t
 qjs_base64url_decode_length(JSContext *ctx, const njs_str_t *src)
 {
     return qjs_base64_decode_length_core(src, qjs_basis64url);
@@ -2309,7 +2294,7 @@ qjs_char_to_hex(u_char c)
 }
 
 
-static int
+int
 qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 {
     u_char        *p;
@@ -2344,7 +2329,7 @@ qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 }
 
 
-static size_t
+size_t
 qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src)
 {
     const u_char  *p, *end;
@@ -2362,7 +2347,7 @@ qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src)
 }
 
 
-static int
+int
 qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 {
     u_char        *p, c;
@@ -2386,7 +2371,7 @@ qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
 }
 
 
-static size_t
+size_t
 qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src)
 {
     return src->length * 2;
diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c
index 2e9a5379..81fee436 100644
--- a/src/test/njs_unit_test.c
+++ b/src/test/njs_unit_test.c
@@ -20840,35 +20840,6 @@ static njs_unit_test_t  njs_querystring_module_test[] =
 };
 
 
-static njs_unit_test_t  njs_webcrypto_test[] =
-{
-    /* Statistic test
-     * bits1 is a random variable with Binomial distribution
-     * Expected value is N / 2
-     * Standard deviation is sqrt(N / 4)
-     */
-    { njs_str("function count1(v) {return v.toString(2).match(/1/g).length;}"
-              "let buf = new Uint32Array(32);"
-              "crypto.getRandomValues(buf);"
-              "let bits1 = buf.reduce((a, v)=> a + count1(v), 0);"
-              "let nbits = buf.length * 32;"
-              "let mean = nbits / 2;"
-              "let stddev = Math.sqrt(nbits / 4);"
-              "let condition = bits1 > (mean - 10 * stddev) && bits1 < (mean + 10 * stddev);"
-              "condition ? true : [buf, nbits, bits1, mean, stddev]"),
-      njs_str("true") },
-
-    { njs_str("let buf = new Uint32Array(4);"
-              "buf === crypto.getRandomValues(buf)"),
-      njs_str("true") },
-
-    { njs_str("crypto.subtle;"
-              "var d = Object.getOwnPropertyDescriptor(crypto, 'subtle');"
-              "d.enumerable && !d.configurable && d.writable"),
-      njs_str("true") },
-};
-
-
 #define NJS_XML_DOC "const xml = require('xml');" \
                     "let data = `<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`;" \
                     "let doc = xml.parse(data);"
@@ -23918,17 +23889,6 @@ static njs_test_suite_t  njs_suites[] =
       njs_nitems(njs_disabled_denormals_test),
       njs_disabled_denormals_tests },
 
-    {
-#if (NJS_HAVE_OPENSSL && !NJS_HAVE_MEMORY_SANITIZER)
-        njs_str("webcrypto"),
-#else
-        njs_str(""),
-#endif
-      { .externals = 1, .repeat = 1, .unsafe = 1 },
-      njs_webcrypto_test,
-      njs_nitems(njs_webcrypto_test),
-      njs_unit_test },
-
     {
 #if (NJS_HAVE_LIBXML2 && !NJS_HAVE_MEMORY_SANITIZER)
         njs_str("xml"),
diff --git a/test/webcrypto/sign.t.mjs b/test/webcrypto/sign.t.mjs
index 4fa78716..c2d52a6f 100644
--- a/test/webcrypto/sign.t.mjs
+++ b/test/webcrypto/sign.t.mjs
@@ -51,10 +51,12 @@ async function test(params) {
     let sig = await crypto.subtle.sign(params.sign_alg, skey,
                                        encoder.encode(params.text))
                     .catch (e => {
-                        if (e.toString().startsWith("Error: EVP_PKEY_CTX_set_signature_md() failed")) {
+                        if (e.message.startsWith("EVP_PKEY_CTX_set_signature_md() failed")) {
                             /* Red Hat Enterprise Linux: SHA-1 is disabled */
                             return "SKIPPED";
                         }
+
+                        throw e;
                     });
 
     if (sig == "SKIPPED") {
diff --git a/test/webcrypto/verify.t.mjs b/test/webcrypto/verify.t.mjs
index 70f608f3..5f77ad47 100644
--- a/test/webcrypto/verify.t.mjs
+++ b/test/webcrypto/verify.t.mjs
@@ -13,10 +13,12 @@ async function test(params) {
                                        key, params.signature,
                                        params.text)
                     .catch (e => {
-                        if (e.toString().startsWith("Error: EVP_PKEY_CTX_set_signature_md() failed")) {
+                        if (e.message.startsWith("EVP_PKEY_CTX_set_signature_md() failed")) {
                             /* Red Hat Enterprise Linux: SHA-1 is disabled */
                             return "SKIPPED";
                         }
+
+                        throw e;
                     });
 
     if (r == "SKIPPED") {


More information about the nginx-devel mailing list