Introduced WebCrypto API according to W3C spec.

The following methods were implemented:
    crypto.getRandomValues()
    crypto.subtle.importKey()
        format: raw, pkcs8, spki
        algorithm: AES-CBC, AES-CTR, AES-GCM,
            ECDSA, HKDF, HMAC, PBKDF2,
            RSASSA-PKCS1-v1_5, RSA-OAEP, RSA-PSS
    crypto.subtle.decrypt()
    crypto.subtle.encrypt()
        algorithm: AES-CBC, AES-CTR, AES-GCM,
            RSA-OAEP
    crypto.subtle.deriveBits()
    crypto.subtle.deriveKey()
        algorithm: HKDF, PBKDF2
    crypto.subtle.digest()
        algorithm: SHA-1, SHA-256, SHA-384, SHA-512
    crypto.subtle.sign()
    crypto.subtle.verify()
        algorithm: ECDSA, HMAC, RSASSA-PKCS1-v1_5, RSA-PSS
diff --git a/auto/make b/auto/make
index b66864e..e5ba7bf 100644
--- a/auto/make
+++ b/auto/make
@@ -75,7 +75,7 @@
 
 $NJS_BUILD_DIR/njs: \\
 	$NJS_BUILD_DIR/libnjs.a \\
-	src/njs_shell.c
+	src/njs_shell.c external/njs_webcrypto.h external/njs_webcrypto.c
 	\$(NJS_LINK) -o $NJS_BUILD_DIR/njs \$(NJS_CFLAGS) \\
 		$NJS_LIB_AUX_CFLAGS \$(NJS_LIB_INCS) -Injs \\
 		src/njs_shell.c \\
@@ -159,7 +159,8 @@
 
 cat << END >> $NJS_MAKEFILE
 
-$NJS_BUILD_DIR/$njs_externals_obj: $njs_src
+$NJS_BUILD_DIR/$njs_externals_obj: \\
+    $njs_src external/njs_webcrypto.h external/njs_webcrypto.c
 	\$(NJS_CC) -c \$(NJS_CFLAGS) $NJS_LIB_AUX_CFLAGS \\
 		\$(NJS_LIB_INCS) -Injs \\
 		-o $NJS_BUILD_DIR/$njs_externals_obj \\
diff --git a/auto/openssl b/auto/openssl
new file mode 100644
index 0000000..1140c6f
--- /dev/null
+++ b/auto/openssl
@@ -0,0 +1,56 @@
+
+# Copyright (C) Dmitry Volyntsev
+# Copyright (C) NGINX, Inc.
+
+
+NJS_OPENSSL_LIB=
+NJS_HAVE_OPENSSL=NO
+
+
+njs_found=no
+
+
+njs_feature="OpenSSL library"
+njs_feature_name=NJS_HAVE_OPENSSL
+njs_feature_run=yes
+njs_feature_incs=
+njs_feature_libs="-lcrypto"
+njs_feature_test="#include <openssl/evp.h>
+
+                  int main() {
+                      OpenSSL_add_all_algorithms();
+                      return 0;
+                 }"
+. auto/feature
+
+
+if [ $njs_found = yes ]; then
+    njs_feature="OpenSSL HKDF"
+    njs_feature_name=NJS_HAVE_OPENSSL_HKDF
+    njs_feature_test="#include <openssl/evp.h>
+                      #include <openssl/kdf.h>
+
+                      int main(void) {
+                          EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+
+                          EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha256());
+                          EVP_PKEY_CTX_free(pctx);
+
+                          return 0;
+                      }"
+    . auto/feature
+
+    njs_feature="OpenSSL EVP_MD_CTX_new()"
+    njs_feature_name=NJS_HAVE_OPENSSL_EVP_MD_CTX_NEW
+    njs_feature_test="#include <openssl/evp.h>
+
+                      int main(void) {
+                          EVP_MD_CTX *ctx = EVP_MD_CTX_new();
+                          EVP_MD_CTX_free(ctx);
+                          return 0;
+                      }"
+    . auto/feature
+
+    NJS_HAVE_OPENSSL=YES
+    NJS_OPENSSL_LIB="$njs_feature_libs"
+fi
diff --git a/auto/sources b/auto/sources
index 4e5e65c..9544e09 100644
--- a/auto/sources
+++ b/auto/sources
@@ -2,6 +2,7 @@
    src/njs_diyfp.c \
    src/njs_dtoa.c \
    src/njs_dtoa_fixed.c \
+   src/njs_str.c \
    src/njs_strtod.c \
    src/njs_murmur_hash.c \
    src/njs_djb_hash.c \
diff --git a/auto/summary b/auto/summary
index 9399ed9..90bac3d 100644
--- a/auto/summary
+++ b/auto/summary
@@ -15,6 +15,10 @@
   echo " + using readline library: $NJS_READLINE_LIB"
 fi
 
+if [ $NJS_HAVE_OPENSSL = YES ]; then
+  echo " + using OpenSSL library: $NJS_OPENSSL_LIB"
+fi
+
 echo
 echo " njs build dir: $NJS_BUILD_DIR"
 
diff --git a/configure b/configure
index f3e845d..9e84823 100755
--- a/configure
+++ b/configure
@@ -26,12 +26,13 @@
 . auto/explicit_bzero
 . auto/pcre
 . auto/readline
+. auto/openssl
 . auto/sources
 
 NJS_LIB_AUX_CFLAGS="$NJS_PCRE_CFLAGS"
 
 NJS_LIBS="$NJS_LIBRT"
-NJS_LIB_AUX_LIBS="$NJS_PCRE_LIB"
+NJS_LIB_AUX_LIBS="$NJS_PCRE_LIB $NJS_OPENSSL_LIB"
 
 . auto/make
 
diff --git a/external/njs_webcrypto.c b/external/njs_webcrypto.c
new file mode 100644
index 0000000..00f9e63
--- /dev/null
+++ b/external/njs_webcrypto.c
@@ -0,0 +1,2666 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#include <njs_main.h>
+#include "njs_webcrypto.h"
+
+#include <openssl/bn.h>
+#include <openssl/bio.h>
+#include <openssl/x509.h>
+#include <openssl/evp.h>
+#include <openssl/aes.h>
+#include <openssl/rsa.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <openssl/crypto.h>
+
+#if NJS_HAVE_OPENSSL_HKDF
+#include <openssl/kdf.h>
+#endif
+
+#if NJS_HAVE_OPENSSL_EVP_MD_CTX_NEW
+#define njs_evp_md_ctx_new()  EVP_MD_CTX_new();
+#define njs_evp_md_ctx_free(_ctx)  EVP_MD_CTX_free(_ctx);
+#else
+#define njs_evp_md_ctx_new()  EVP_MD_CTX_create();
+#define njs_evp_md_ctx_free(_ctx)  EVP_MD_CTX_destroy(_ctx);
+#endif
+
+
+typedef enum {
+    NJS_KEY_FORMAT_RAW          = 1 << 1,
+    NJS_KEY_FORMAT_PKCS8        = 1 << 2,
+    NJS_KEY_FORMAT_SPKI         = 1 << 3,
+    NJS_KEY_FORMAT_JWK          = 1 << 4,
+    NJS_KEY_FORMAT_UNKNOWN      = 1 << 5,
+} njs_webcrypto_key_format_t;
+
+
+typedef enum {
+    NJS_KEY_USAGE_DECRYPT       = 1 << 1,
+    NJS_KEY_USAGE_DERIVE_BITS   = 1 << 2,
+    NJS_KEY_USAGE_DERIVE_KEY    = 1 << 3,
+    NJS_KEY_USAGE_ENCRYPT       = 1 << 4,
+    NJS_KEY_USAGE_GENERATE_KEY  = 1 << 5,
+    NJS_KEY_USAGE_SIGN          = 1 << 6,
+    NJS_KEY_USAGE_VERIFY        = 1 << 7,
+    NJS_KEY_USAGE_WRAP_KEY      = 1 << 8,
+    NJS_KEY_USAGE_UNSUPPORTED   = 1 << 9,
+    NJS_KEY_USAGE_UNWRAP_KEY    = 1 << 10,
+} njs_webcrypto_key_usage_t;
+
+
+typedef enum {
+    NJS_ALGORITHM_RSA_OAEP,
+    NJS_ALGORITHM_AES_GCM,
+    NJS_ALGORITHM_AES_CTR,
+    NJS_ALGORITHM_AES_CBC,
+    NJS_ALGORITHM_RSASSA_PKCS1_v1_5,
+    NJS_ALGORITHM_RSA_PSS,
+    NJS_ALGORITHM_ECDSA,
+    NJS_ALGORITHM_ECDH,
+    NJS_ALGORITHM_PBKDF2,
+    NJS_ALGORITHM_HKDF,
+    NJS_ALGORITHM_HMAC,
+} njs_webcrypto_alg_t;
+
+
+typedef enum {
+    NJS_HASH_SHA1,
+    NJS_HASH_SHA256,
+    NJS_HASH_SHA384,
+    NJS_HASH_SHA512,
+} njs_webcrypto_hash_t;
+
+
+typedef enum {
+    NJS_CURVE_P256,
+    NJS_CURVE_P384,
+    NJS_CURVE_P521,
+} njs_webcrypto_curve_t;
+
+
+typedef struct {
+    njs_str_t                  name;
+    uintptr_t                  value;
+} njs_webcrypto_entry_t;
+
+
+typedef struct {
+    njs_webcrypto_alg_t        type;
+    unsigned                   usage;
+    unsigned                   fmt;
+} njs_webcrypto_algorithm_t;
+
+
+typedef struct {
+    njs_webcrypto_algorithm_t  *alg;
+    unsigned                   usage;
+    njs_webcrypto_hash_t       hash;
+    njs_webcrypto_curve_t      curve;
+
+    EVP_PKEY                   *pkey;
+    njs_str_t                  raw;
+} njs_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 njs_int_t njs_ext_cipher(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_cipher_pkey(njs_vm_t *vm, njs_str_t *data,
+    njs_webcrypto_key_t *key, njs_index_t encrypt);
+static njs_int_t njs_cipher_aes_gcm(njs_vm_t *vm, njs_str_t *data,
+    njs_webcrypto_key_t *key, njs_value_t *options, njs_bool_t encrypt);
+static njs_int_t njs_cipher_aes_ctr(njs_vm_t *vm, njs_str_t *data,
+    njs_webcrypto_key_t *key, njs_value_t *options, njs_bool_t encrypt);
+static njs_int_t njs_cipher_aes_cbc(njs_vm_t *vm, njs_str_t *data,
+    njs_webcrypto_key_t *key, njs_value_t *options, njs_bool_t encrypt);
+static njs_int_t njs_ext_derive(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t derive_key);
+static njs_int_t njs_ext_digest(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_export_key(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_import_key(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_sign(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t verify);
+static njs_int_t njs_ext_unwrap_key(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_wrap_key(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_get_random_values(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+
+static void njs_webcrypto_cleanup_pkey(void *data);
+static njs_webcrypto_key_format_t njs_key_format(njs_vm_t *vm,
+    njs_value_t *value, njs_str_t *format);
+static njs_int_t njs_key_usage(njs_vm_t *vm, njs_value_t *value,
+    unsigned *mask);
+static njs_webcrypto_algorithm_t *njs_key_algorithm(njs_vm_t *vm,
+    njs_value_t *value);
+static njs_str_t *njs_algorithm_string(njs_webcrypto_algorithm_t *algorithm);
+static njs_int_t njs_algorithm_hash(njs_vm_t *vm, njs_value_t *value,
+    njs_webcrypto_hash_t *hash);
+static const EVP_MD *njs_algorithm_hash_digest(njs_webcrypto_hash_t hash);
+static njs_int_t njs_algorithm_curve(njs_vm_t *vm, njs_value_t *value,
+    njs_webcrypto_curve_t *curve);
+
+static njs_int_t njs_webcrypto_result(njs_vm_t *vm, njs_value_t *result,
+    njs_int_t rc);
+static void njs_webcrypto_error(njs_vm_t *vm, const char *fmt, ...);
+
+static njs_webcrypto_entry_t njs_webcrypto_alg[] = {
+
+#define njs_webcrypto_algorithm(type, usage_mask, fmt_mask)                  \
+    (uintptr_t) & (njs_webcrypto_algorithm_t) { type, usage_mask, fmt_mask }
+
+    {
+      njs_str("RSA-OAEP"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_RSA_OAEP,
+                              NJS_KEY_USAGE_ENCRYPT |
+                              NJS_KEY_USAGE_DECRYPT |
+                              NJS_KEY_USAGE_WRAP_KEY |
+                              NJS_KEY_USAGE_UNWRAP_KEY |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_PKCS8 |
+                              NJS_KEY_FORMAT_SPKI)
+    },
+
+    {
+      njs_str("AES-GCM"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_AES_GCM,
+                              NJS_KEY_USAGE_ENCRYPT |
+                              NJS_KEY_USAGE_DECRYPT |
+                              NJS_KEY_USAGE_WRAP_KEY |
+                              NJS_KEY_USAGE_UNWRAP_KEY |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_RAW)
+    },
+
+    {
+      njs_str("AES-CTR"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_AES_CTR,
+                              NJS_KEY_USAGE_ENCRYPT |
+                              NJS_KEY_USAGE_DECRYPT |
+                              NJS_KEY_USAGE_WRAP_KEY |
+                              NJS_KEY_USAGE_UNWRAP_KEY |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_RAW)
+    },
+
+    {
+      njs_str("AES-CBC"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_AES_CBC,
+                              NJS_KEY_USAGE_ENCRYPT |
+                              NJS_KEY_USAGE_DECRYPT |
+                              NJS_KEY_USAGE_WRAP_KEY |
+                              NJS_KEY_USAGE_UNWRAP_KEY |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_RAW)
+    },
+
+    {
+      njs_str("RSASSA-PKCS1-v1_5"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_RSASSA_PKCS1_v1_5,
+                              NJS_KEY_USAGE_SIGN |
+                              NJS_KEY_USAGE_VERIFY |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_PKCS8 |
+                              NJS_KEY_FORMAT_SPKI)
+    },
+
+    {
+      njs_str("RSA-PSS"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_RSA_PSS,
+                              NJS_KEY_USAGE_SIGN |
+                              NJS_KEY_USAGE_VERIFY |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_PKCS8 |
+                              NJS_KEY_FORMAT_SPKI)
+    },
+
+    {
+      njs_str("ECDSA"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_ECDSA,
+                              NJS_KEY_USAGE_SIGN |
+                              NJS_KEY_USAGE_VERIFY |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_PKCS8 |
+                              NJS_KEY_FORMAT_SPKI)
+    },
+
+    {
+      njs_str("ECDH"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_ECDH,
+                              NJS_KEY_USAGE_DERIVE_KEY |
+                              NJS_KEY_USAGE_DERIVE_BITS |
+                              NJS_KEY_USAGE_GENERATE_KEY |
+                              NJS_KEY_USAGE_UNSUPPORTED,
+                              NJS_KEY_FORMAT_UNKNOWN)
+    },
+
+    {
+      njs_str("PBKDF2"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_PBKDF2,
+                              NJS_KEY_USAGE_DERIVE_KEY |
+                              NJS_KEY_USAGE_DERIVE_BITS,
+                              NJS_KEY_FORMAT_RAW)
+    },
+
+    {
+      njs_str("HKDF"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_HKDF,
+                              NJS_KEY_USAGE_DERIVE_KEY |
+                              NJS_KEY_USAGE_DERIVE_BITS,
+                              NJS_KEY_FORMAT_RAW)
+    },
+
+    {
+      njs_str("HMAC"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_HMAC,
+                              NJS_KEY_USAGE_GENERATE_KEY |
+                              NJS_KEY_USAGE_SIGN |
+                              NJS_KEY_USAGE_VERIFY,
+                              NJS_KEY_FORMAT_RAW)
+    },
+
+    {
+        njs_null_str,
+        0
+    }
+};
+
+
+static njs_webcrypto_entry_t njs_webcrypto_hash[] = {
+    { njs_str("SHA-256"), NJS_HASH_SHA256 },
+    { njs_str("SHA-384"), NJS_HASH_SHA384 },
+    { njs_str("SHA-512"), NJS_HASH_SHA512 },
+    { njs_str("SHA-1"), NJS_HASH_SHA1 },
+    { njs_null_str, 0 }
+};
+
+
+static njs_webcrypto_entry_t njs_webcrypto_curve[] = {
+    { njs_str("P-256"), NJS_CURVE_P256 },
+    { njs_str("P-384"), NJS_CURVE_P384 },
+    { njs_str("P-521"), NJS_CURVE_P521 },
+    { njs_null_str, 0 }
+};
+
+
+static njs_webcrypto_entry_t njs_webcrypto_usage[] = {
+    { njs_str("decrypt"), NJS_KEY_USAGE_DECRYPT },
+    { njs_str("deriveBits"), NJS_KEY_USAGE_DERIVE_BITS },
+    { njs_str("deriveKey"), NJS_KEY_USAGE_DERIVE_KEY },
+    { njs_str("encrypt"), NJS_KEY_USAGE_ENCRYPT },
+    { njs_str("sign"), NJS_KEY_USAGE_SIGN },
+    { njs_str("unwrapKey"), NJS_KEY_USAGE_UNWRAP_KEY },
+    { njs_str("verify"), NJS_KEY_USAGE_VERIFY },
+    { njs_str("wrapKey"), NJS_KEY_USAGE_WRAP_KEY },
+    { njs_null_str, 0 }
+};
+
+
+static njs_external_t  njs_ext_webcrypto_crypto_key[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "CryptoKey",
+        }
+    },
+};
+
+
+static njs_external_t  njs_ext_subtle_webcrypto[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "SubtleCrypto",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("decrypt"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_cipher,
+            .magic8 = 0,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("deriveBits"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_derive,
+            .magic8 = 0,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("deriveKey"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_derive,
+            .magic8 = 1,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("digest"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_digest,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("encrypt"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_cipher,
+            .magic8 = 1,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("exportKey"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_export_key,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("generateKey"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_generate_key,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("importKey"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_import_key,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("sign"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_sign,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("unwrapKey"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_unwrap_key,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("verify"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_sign,
+            .magic8 = 1,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("wrapKey"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_wrap_key,
+        }
+    },
+
+};
+
+static njs_external_t  njs_ext_webcrypto[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "Crypto",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("getRandomValues"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_ext_get_random_values,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_OBJECT,
+        .name.string = njs_str("subtle"),
+        .enumerable = 1,
+        .writable = 1,
+        .u.object = {
+            .enumerable = 1,
+            .properties = njs_ext_subtle_webcrypto,
+            .nproperties = njs_nitems(njs_ext_subtle_webcrypto),
+        }
+    },
+
+};
+
+
+static njs_int_t    njs_webcrypto_crypto_key_proto_id;
+
+
+static njs_int_t
+njs_ext_cipher(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t encrypt)
+{
+    unsigned                   mask;
+    njs_int_t                  ret;
+    njs_str_t                  data;
+    njs_value_t                *options;
+    njs_webcrypto_key_t        *key;
+    njs_webcrypto_algorithm_t  *alg;
+
+    options = njs_arg(args, nargs, 1);
+    alg = njs_key_algorithm(vm, options);
+    if (njs_slow_path(alg == NULL)) {
+        goto fail;
+    }
+
+    key = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id,
+                          njs_arg(args, nargs, 2));
+    if (njs_slow_path(key == NULL)) {
+        njs_type_error(vm, "\"key\" is not a CryptoKey object");
+        goto fail;
+    }
+
+    mask = encrypt ? NJS_KEY_USAGE_ENCRYPT : NJS_KEY_USAGE_DECRYPT;
+    if (njs_slow_path(!(key->usage & mask))) {
+        njs_type_error(vm, "provide key does not support %s operation",
+                       encrypt ? "encrypt" : "decrypt");
+        goto fail;
+    }
+
+    if (njs_slow_path(key->alg != alg)) {
+        njs_type_error(vm, "cannot %s using \"%V\" with \"%V\" key",
+                       encrypt ? "encrypt" : "decrypt",
+                       njs_algorithm_string(key->alg),
+                       njs_algorithm_string(alg));
+        goto fail;
+    }
+
+    ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 3));
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    switch (alg->type) {
+    case NJS_ALGORITHM_RSA_OAEP:
+        ret = njs_cipher_pkey(vm, &data, key, encrypt);
+        break;
+
+    case NJS_ALGORITHM_AES_GCM:
+        ret = njs_cipher_aes_gcm(vm, &data, key, options, encrypt);
+        break;
+
+    case NJS_ALGORITHM_AES_CTR:
+        ret = njs_cipher_aes_ctr(vm, &data, key, options, encrypt);
+        break;
+
+    case NJS_ALGORITHM_AES_CBC:
+    default:
+        ret = njs_cipher_aes_cbc(vm, &data, key, options, encrypt);
+    }
+
+    return njs_webcrypto_result(vm, njs_vm_retval(vm), ret);
+
+fail:
+
+    return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static njs_int_t
+njs_cipher_pkey(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
+    njs_index_t encrypt)
+{
+    u_char                  *dst;
+    size_t                  outlen;
+    njs_int_t               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->pkey, NULL);
+    if (njs_slow_path(ctx == NULL)) {
+        njs_webcrypto_error(vm, "EVP_PKEY_CTX_new() failed");
+        return NJS_ERROR;
+    }
+
+    if (encrypt) {
+        init = EVP_PKEY_encrypt_init;
+        cipher = EVP_PKEY_encrypt;
+
+    } else {
+        init = EVP_PKEY_decrypt_init;
+        cipher = EVP_PKEY_decrypt;
+    }
+
+    ret = init(ctx);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_PKEY_%scrypt_init() failed",
+                            encrypt ? "en" : "de");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    md = njs_algorithm_hash_digest(key->hash);
+
+    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
+    EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md);
+    EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md);
+
+    ret = cipher(ctx, NULL, &outlen, data->start, data->length);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_PKEY_%scrypt() failed",
+                            encrypt ? "en" : "de");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    dst = njs_mp_alloc(njs_vm_memory_pool(vm), outlen);
+    if (njs_slow_path(dst == NULL)) {
+        njs_memory_error(vm);
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = cipher(ctx, dst, &outlen, data->start, data->length);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_PKEY_%scrypt() failed",
+                            encrypt ? "en" : "de");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = njs_vm_value_array_buffer_set(vm, njs_vm_retval(vm), dst, outlen);
+
+fail:
+
+    EVP_PKEY_CTX_free(ctx);
+
+    return ret;
+}
+
+
+static njs_int_t
+njs_cipher_aes_gcm(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
+    njs_value_t *options, njs_bool_t encrypt)
+{
+    int               len, outlen, dstlen;
+    u_char            *dst, *p;
+    int64_t           taglen;
+    njs_str_t         iv, aad;
+    njs_int_t         ret;
+    njs_value_t       value;
+    EVP_CIPHER_CTX    *ctx;
+    const EVP_CIPHER  *cipher;
+
+    static const njs_value_t  string_iv = njs_string("iv");
+    static const njs_value_t  string_ad = njs_string("additionalData");
+    static const njs_value_t  string_tl = njs_string("tagLength");
+
+    switch (key->raw.length) {
+    case 16:
+        cipher = EVP_aes_128_gcm();
+        break;
+
+    case 32:
+        cipher = EVP_aes_256_gcm();
+        break;
+
+    default:
+        njs_type_error(vm, "AES-GCM Invalid key length");
+        return NJS_ERROR;
+    }
+
+    ret = njs_value_property(vm, options, njs_value_arg(&string_iv), &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        if (ret == NJS_DECLINED) {
+            njs_type_error(vm, "AES-GCM algorithm.iv is not provided");
+        }
+
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_value_to_bytes(vm, &iv, &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    taglen = 128;
+
+    ret = njs_value_property(vm, options, njs_value_arg(&string_tl), &value);
+    if (njs_slow_path(ret == NJS_ERROR)) {
+        return NJS_ERROR;
+    }
+
+    if (njs_is_defined(&value)) {
+        ret = njs_value_to_integer(vm, &value, &taglen);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    if (njs_slow_path(taglen != 32
+                      && taglen != 64
+                      && taglen != 96
+                      && taglen != 104
+                      && taglen != 112
+                      && taglen != 120
+                      && taglen != 128))
+    {
+        njs_type_error(vm, "AES-GCM Invalid tagLength");
+        return NJS_ERROR;
+    }
+
+    taglen /= 8;
+
+    if (njs_slow_path(!encrypt && (data->length < (size_t) taglen))) {
+        njs_type_error(vm, "AES-GCM data is too short");
+        return NJS_ERROR;
+    }
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (njs_slow_path(ctx == NULL)) {
+        njs_webcrypto_error(vm, "EVP_CIPHER_CTX_new() failed");
+        return NJS_ERROR;
+    }
+
+    ret = EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encrypt);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%sInit_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.length, NULL);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_CIPHER_CTX_ctrl() failed");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = EVP_CipherInit_ex(ctx, NULL, NULL, key->raw.start, iv.start,
+                            encrypt);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%sInit_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    if (!encrypt) {
+        ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, taglen,
+                                  &data->start[data->length - taglen]);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_CIPHER_CTX_ctrl() failed");
+            ret = NJS_ERROR;
+            goto fail;
+        }
+    }
+
+    ret = njs_value_property(vm, options, njs_value_arg(&string_ad), &value);
+    if (njs_slow_path(ret == NJS_ERROR)) {
+        return NJS_ERROR;
+    }
+
+    aad.length = 0;
+
+    if (njs_is_defined(&value)) {
+        ret = njs_vm_value_to_bytes(vm, &aad, &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    if (aad.length != 0) {
+        ret = EVP_CipherUpdate(ctx, NULL, &outlen, aad.start, aad.length);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_%sUpdate() failed",
+                                encrypt ? "Encrypt" : "Decrypt");
+            ret = NJS_ERROR;
+            goto fail;
+        }
+    }
+
+    dstlen = data->length + EVP_CIPHER_CTX_block_size(ctx) + taglen;
+    dst = njs_mp_alloc(njs_vm_memory_pool(vm), dstlen);
+    if (njs_slow_path(dst == NULL)) {
+        njs_memory_error(vm);
+        return NJS_ERROR;
+    }
+
+    ret = EVP_CipherUpdate(ctx, dst, &outlen, data->start,
+                           data->length - (encrypt ? 0 : taglen));
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%sUpdate() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    p = &dst[outlen];
+    len = EVP_CIPHER_CTX_block_size(ctx);
+
+    ret = EVP_CipherFinal_ex(ctx, p, &len);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%sFinal_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    outlen += len;
+    p += len;
+
+    if (encrypt) {
+        ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, taglen, p);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_CIPHER_CTX_ctrl() failed");
+            ret = NJS_ERROR;
+            goto fail;
+        }
+
+        outlen += taglen;
+    }
+
+    ret = njs_vm_value_array_buffer_set(vm, njs_vm_retval(vm), dst, outlen);
+
+fail:
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return ret;
+}
+
+
+static njs_int_t
+njs_cipher_aes_ctr128(njs_vm_t *vm, const EVP_CIPHER *cipher, u_char *key,
+    u_char *data, size_t dlen, u_char *counter, u_char *dst, int *olen,
+    njs_bool_t encrypt)
+{
+    int             len, outlen;
+    njs_int_t       ret;
+    EVP_CIPHER_CTX  *ctx;
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (njs_slow_path(ctx == NULL)) {
+        njs_webcrypto_error(vm, "EVP_CIPHER_CTX_new() failed");
+        return NJS_ERROR;
+    }
+
+    ret = EVP_CipherInit_ex(ctx, cipher, NULL, key, counter, encrypt);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%sInit_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = EVP_CipherUpdate(ctx, dst, &outlen, data, dlen);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%sUpdate() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = EVP_CipherFinal_ex(ctx, &dst[outlen], &len);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%sFinal_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    outlen += len;
+    *olen = outlen;
+
+    ret = NJS_OK;
+
+fail:
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return ret;
+}
+
+
+njs_inline njs_uint_t
+njs_ceil_div(njs_uint_t dend, njs_uint_t dsor)
+{
+    return (dsor == 0) ? 0 : 1 + (dend - 1) / dsor;
+}
+
+
+njs_inline BIGNUM *
+njs_bn_counter128(njs_str_t *ctr, njs_uint_t bits)
+{
+    njs_uint_t  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 = njs_ceil_div(bits, 8);
+
+    memcpy(buf, &ctr->start[ctr->length - bytes], bytes);
+
+    buf[0] &= ~(0xFF << remainder);
+
+    return BN_bin2bn(buf, bytes, NULL);
+}
+
+
+njs_inline void
+njs_counter128_reset(u_char *src, u_char *dst, njs_uint_t bits)
+{
+    size_t      index;
+    njs_uint_t  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 njs_int_t
+njs_cipher_aes_ctr(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
+    njs_value_t *options, njs_bool_t encrypt)
+{
+    int               len, len2;
+    u_char            *dst;
+    int64_t           length;
+    BIGNUM            *total, *blocks, *left, *ctr;
+    njs_int_t         ret;
+    njs_str_t         iv;
+    njs_uint_t        size1;
+    njs_value_t       value;
+    const EVP_CIPHER  *cipher;
+    u_char            iv2[16];
+
+    static const njs_value_t  string_counter = njs_string("counter");
+    static const njs_value_t  string_length = njs_string("length");
+
+    switch (key->raw.length) {
+    case 16:
+        cipher = EVP_aes_128_ctr();
+        break;
+
+    case 32:
+        cipher = EVP_aes_256_ctr();
+        break;
+
+    default:
+        njs_type_error(vm, "AES-CTR Invalid key length");
+        return NJS_ERROR;
+    }
+
+    ret = njs_value_property(vm, options, njs_value_arg(&string_counter),
+                             &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        if (ret == NJS_DECLINED) {
+            njs_type_error(vm, "AES-CTR algorithm.counter is not provided");
+        }
+
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_value_to_bytes(vm, &iv, &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    if (njs_slow_path(iv.length != 16)) {
+        njs_type_error(vm, "AES-CTR algorithm.counter must be 16 bytes long");
+        return NJS_ERROR;
+    }
+
+    ret = njs_value_property(vm, options, njs_value_arg(&string_length),
+                             &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        if (ret == NJS_DECLINED) {
+            njs_type_error(vm, "AES-CTR algorithm.length is not provided");
+        }
+
+        return NJS_ERROR;
+    }
+
+    ret = njs_value_to_integer(vm, &value, &length);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    if (njs_slow_path(length == 0 || length > 128)) {
+        njs_type_error(vm, "AES-CTR algorithm.length "
+                       "must be between 1 and 128");
+        return NJS_ERROR;
+    }
+
+    ctr = NULL;
+    blocks = NULL;
+    left = NULL;
+
+    total = BN_new();
+    if (njs_slow_path(total == NULL)) {
+        njs_webcrypto_error(vm, "BN_new() failed");
+        return NJS_ERROR;
+    }
+
+    ret = BN_lshift(total, BN_value_one(), length);
+    if (njs_slow_path(ret != 1)) {
+        njs_webcrypto_error(vm, "BN_lshift() failed");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ctr = njs_bn_counter128(&iv, length);
+    if (njs_slow_path(ctr == NULL)) {
+        njs_webcrypto_error(vm, "BN_bin2bn() failed");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    blocks = BN_new();
+    if (njs_slow_path(blocks == NULL)) {
+        njs_webcrypto_error(vm, "BN_new() failed");
+        return NJS_ERROR;
+    }
+
+    ret = BN_set_word(blocks, njs_ceil_div(data->length, AES_BLOCK_SIZE));
+    if (njs_slow_path(ret != 1)) {
+        njs_webcrypto_error(vm, "BN_set_word() failed");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = BN_cmp(blocks, total);
+    if (njs_slow_path(ret > 0)) {
+        njs_type_error(vm, "AES-CTR repeated counter");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    left = BN_new();
+    if (njs_slow_path(left == NULL)) {
+        njs_webcrypto_error(vm, "BN_new() failed");
+        return NJS_ERROR;
+    }
+
+    ret = BN_sub(left, total, ctr);
+    if (njs_slow_path(ret != 1)) {
+        njs_webcrypto_error(vm, "BN_sub() failed");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    dst = njs_mp_alloc(njs_vm_memory_pool(vm),
+                       data->length + EVP_MAX_BLOCK_LENGTH);
+    if (njs_slow_path(dst == NULL)) {
+        njs_memory_error(vm);
+        return NJS_ERROR;
+    }
+
+    ret = BN_cmp(left, blocks);
+    if (ret >= 0) {
+
+        /*
+         * Doing a single run if a counter is not wrapped-around
+         * during the ciphering.
+         * */
+
+        ret = njs_cipher_aes_ctr128(vm, cipher, key->raw.start,
+                                    data->start, data->length, iv.start, dst,
+                                    &len, encrypt);
+        if (njs_slow_path(ret != NJS_OK)) {
+            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;
+
+    ret = njs_cipher_aes_ctr128(vm, cipher, key->raw.start, data->start, size1,
+                                iv.start, dst, &len, encrypt);
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    njs_counter128_reset(iv.start, (u_char *) iv2, length);
+
+    ret = njs_cipher_aes_ctr128(vm, cipher, key->raw.start, &data->start[size1],
+                                data->length - size1, iv2, &dst[size1], &len2,
+                                encrypt);
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    len += len2;
+
+done:
+
+    ret = njs_vm_value_array_buffer_set(vm, njs_vm_retval(vm), 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 njs_int_t
+njs_cipher_aes_cbc(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
+    njs_value_t *options, njs_bool_t encrypt)
+{
+    int               olen_max, olen, olen2;
+    u_char            *dst;
+    unsigned          remainder;
+    njs_str_t         iv;
+    njs_int_t         ret;
+    njs_value_t       value;
+    EVP_CIPHER_CTX    *ctx;
+    const EVP_CIPHER  *cipher;
+
+    static const njs_value_t  string_iv = njs_string("iv");
+
+    switch (key->raw.length) {
+    case 16:
+        cipher = EVP_aes_128_cbc();
+        break;
+
+    case 32:
+        cipher = EVP_aes_256_cbc();
+        break;
+
+    default:
+        njs_type_error(vm, "AES-CBC Invalid key length");
+        return NJS_ERROR;
+    }
+
+    ret = njs_value_property(vm, options, njs_value_arg(&string_iv), &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        if (ret == NJS_DECLINED) {
+            njs_type_error(vm, "AES-CBC algorithm.iv is not provided");
+        }
+
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_value_to_bytes(vm, &iv, &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    if (njs_slow_path(iv.length != 16)) {
+        njs_type_error(vm, "AES-CBC algorithm.iv must be 16 bytes long");
+        return NJS_ERROR;
+    }
+
+    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 (njs_slow_path(ctx == NULL)) {
+        njs_webcrypto_error(vm, "EVP_CIPHER_CTX_new() failed");
+        return NJS_ERROR;
+    }
+
+    ret = EVP_CipherInit_ex(ctx, cipher, NULL, key->raw.start, iv.start,
+                            encrypt);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%SInit_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    dst = njs_mp_alloc(njs_vm_memory_pool(vm), olen_max);
+    if (njs_slow_path(dst == NULL)) {
+        njs_memory_error(vm);
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = EVP_CipherUpdate(ctx, dst, &olen, data->start, data->length);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%SUpdate() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    ret = EVP_CipherFinal_ex(ctx, &dst[olen], &olen2);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_%sFinal_ex() failed",
+                            encrypt ? "Encrypt" : "Decrypt");
+        ret = NJS_ERROR;
+        goto fail;
+    }
+
+    olen += olen2;
+
+    ret = njs_vm_value_array_buffer_set(vm, njs_vm_retval(vm), dst, olen);
+
+fail:
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return ret;
+}
+
+
+static njs_int_t
+njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t derive_key)
+{
+    u_char                     *k;
+    size_t                     olen;
+    int64_t                    iterations, length;
+    EVP_PKEY                   *pkey;
+    unsigned                   usage, mask;
+    njs_int_t                  ret;
+    njs_str_t                  salt, info;
+    njs_value_t                value, *aobject, *dobject;
+    const EVP_MD               *md;
+    EVP_PKEY_CTX               *pctx;
+    njs_mp_cleanup_t           *cln;
+    njs_webcrypto_key_t        *key, *dkey;
+    njs_webcrypto_hash_t       hash;
+    njs_webcrypto_algorithm_t  *alg, *dalg;
+
+    static const njs_value_t  string_info = njs_string("info");
+    static const njs_value_t  string_salt = njs_string("salt");
+    static const njs_value_t  string_length = njs_string("length");
+    static const njs_value_t  string_iterations = njs_string("iterations");
+
+    aobject = njs_arg(args, nargs, 1);
+    alg = njs_key_algorithm(vm, aobject);
+    if (njs_slow_path(alg == NULL)) {
+        goto fail;
+    }
+
+    key = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id,
+                          njs_arg(args, nargs, 2));
+    if (njs_slow_path(key == NULL)) {
+        njs_type_error(vm, "\"baseKey\" is not a CryptoKey object");
+        goto fail;
+    }
+
+    mask = derive_key ? NJS_KEY_USAGE_DERIVE_KEY : NJS_KEY_USAGE_DERIVE_BITS;
+    if (njs_slow_path(!(key->usage & mask))) {
+        njs_type_error(vm, "provide key does not support \"%s\" operation",
+                       derive_key ? "deriveKey" : "deriveBits");
+        goto fail;
+    }
+
+    if (njs_slow_path(key->alg != alg)) {
+        njs_type_error(vm, "cannot derive %s using \"%V\" with \"%V\" key",
+                       derive_key ? "key" : "bits",
+                       njs_algorithm_string(key->alg),
+                       njs_algorithm_string(alg));
+        goto fail;
+    }
+
+    dobject = njs_arg(args, nargs, 3);
+
+    if (derive_key) {
+        dalg = njs_key_algorithm(vm, dobject);
+        if (njs_slow_path(dalg == NULL)) {
+            goto fail;
+        }
+
+        ret = njs_value_property(vm, dobject, njs_value_arg(&string_length),
+                                 &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+                if (ret == NJS_DECLINED) {
+                    njs_type_error(vm, "derivedKeyAlgorithm.length "
+                                   "is not provided");
+                    goto fail;
+                }
+        }
+
+    } else {
+        dalg = NULL;
+        njs_value_assign(&value, dobject);
+    }
+
+    ret = njs_value_to_integer(vm, &value, &length);
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    dkey = NULL;
+    length /= 8;
+
+    if (derive_key) {
+        switch (dalg->type) {
+        case NJS_ALGORITHM_AES_GCM:
+        case NJS_ALGORITHM_AES_CTR:
+        case NJS_ALGORITHM_AES_CBC:
+
+            if (length != 16 && length != 32) {
+                njs_type_error(vm, "deriveKey \"%V\" length must be 128 or 256",
+                               njs_algorithm_string(dalg));
+                goto fail;
+            }
+
+            break;
+
+        default:
+            njs_internal_error(vm, "not implemented deriveKey: \"%V\"",
+                               njs_algorithm_string(dalg));
+            goto fail;
+        }
+
+        ret = njs_key_usage(vm, njs_arg(args, nargs, 5), &usage);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        if (njs_slow_path(usage & ~dalg->usage)) {
+            njs_type_error(vm, "unsupported key usage for \"%V\" key",
+                           njs_algorithm_string(alg));
+            goto fail;
+        }
+
+        dkey = njs_mp_zalloc(njs_vm_memory_pool(vm),
+                             sizeof(njs_webcrypto_key_t));
+        if (njs_slow_path(dkey == NULL)) {
+            njs_memory_error(vm);
+            goto fail;
+        }
+
+        dkey->alg = dalg;
+        dkey->usage = usage;
+    }
+
+    k = njs_mp_zalloc(njs_vm_memory_pool(vm), length);
+    if (njs_slow_path(k == NULL)) {
+        njs_memory_error(vm);
+        goto fail;
+    }
+
+    switch (alg->type) {
+    case NJS_ALGORITHM_PBKDF2:
+        ret = njs_algorithm_hash(vm, aobject, &hash);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto fail;
+        }
+
+        ret = njs_value_property(vm, aobject, njs_value_arg(&string_salt),
+                                 &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            if (ret == NJS_DECLINED) {
+                njs_type_error(vm, "PBKDF2 algorithm.salt is not provided");
+            }
+
+            goto fail;
+        }
+
+        ret = njs_vm_value_to_bytes(vm, &salt, &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        if (njs_slow_path(salt.length < 16)) {
+            njs_type_error(vm, "PBKDF2 algorithm.salt must be "
+                           "at least 16 bytes long");
+            goto fail;
+        }
+
+        ret = njs_value_property(vm, aobject, njs_value_arg(&string_iterations),
+                                 &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            if (ret == NJS_DECLINED) {
+                njs_type_error(vm, "PBKDF2 algorithm.iterations "
+                               "is not provided");
+            }
+
+            goto fail;
+        }
+
+        ret = njs_value_to_integer(vm, &value, &iterations);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        md = njs_algorithm_hash_digest(hash);
+
+        ret = PKCS5_PBKDF2_HMAC((char *) key->raw.start, key->raw.length,
+                                salt.start, salt.length, iterations, md,
+                                length, k);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "PKCS5_PBKDF2_HMAC() failed");
+            goto fail;
+        }
+        break;
+
+    case NJS_ALGORITHM_HKDF:
+#ifdef NJS_HAVE_OPENSSL_HKDF
+        ret = njs_algorithm_hash(vm, aobject, &hash);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto fail;
+        }
+
+        ret = njs_value_property(vm, aobject, njs_value_arg(&string_salt),
+                                 &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            if (ret == NJS_DECLINED) {
+                njs_type_error(vm, "HKDF algorithm.salt is not provided");
+            }
+
+            goto fail;
+        }
+
+        ret = njs_vm_value_to_bytes(vm, &salt, &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        ret = njs_value_property(vm, aobject, njs_value_arg(&string_info),
+                                 &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            if (ret == NJS_DECLINED) {
+                njs_type_error(vm, "HKDF algorithm.info is not provided");
+            }
+
+            goto fail;
+        }
+
+        ret = njs_vm_value_to_bytes(vm, &info, &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+        if (njs_slow_path(pctx == NULL)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_CTX_new_id() failed");
+            goto fail;
+        }
+
+        ret = EVP_PKEY_derive_init(pctx);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_derive_init() failed");
+            goto free;
+        }
+
+        md = njs_algorithm_hash_digest(hash);
+
+        ret = EVP_PKEY_CTX_set_hkdf_md(pctx, md);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_CTX_set_hkdf_md() failed");
+            goto free;
+        }
+
+        ret = EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt.start, salt.length);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_CTX_set1_hkdf_salt() failed");
+            goto free;
+        }
+
+        ret = EVP_PKEY_CTX_set1_hkdf_key(pctx, key->raw.start, key->raw.length);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_CTX_set1_hkdf_key() failed");
+            goto free;
+        }
+
+        ret = EVP_PKEY_CTX_add1_hkdf_info(pctx, info.start, info.length);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_CTX_add1_hkdf_info() failed");
+            goto free;
+        }
+
+        olen = (size_t) length;
+        ret = EVP_PKEY_derive(pctx, k, &olen);
+        if (njs_slow_path(ret <= 0 || olen != (size_t) length)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_derive() failed");
+            goto free;
+        }
+
+free:
+
+        EVP_PKEY_CTX_free(pctx);
+
+        if (njs_slow_path(ret <= 0)) {
+            goto fail;
+        }
+
+        break;
+#else
+        (void) pctx;
+        (void) olen;
+        (void) &string_info;
+        (void) &info;
+#endif
+
+    case NJS_ALGORITHM_ECDH:
+    default:
+        njs_internal_error(vm, "not implemented deriveKey "
+                           "algorithm: \"%V\"", njs_algorithm_string(alg));
+        goto fail;
+    }
+
+    if (derive_key) {
+        if (dalg->type == NJS_ALGORITHM_HMAC) {
+            ret = njs_algorithm_hash(vm, dobject, &dkey->hash);
+            if (njs_slow_path(ret == NJS_ERROR)) {
+                goto fail;
+            }
+
+            pkey = EVP_PKEY_new_mac_key(EVP_PKEY_HMAC, NULL, k, length);
+            if (njs_slow_path(pkey == NULL)) {
+                njs_webcrypto_error(vm, "EVP_PKEY_new_mac_key() failed");
+                goto fail;
+            }
+
+            cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0);
+            if (cln == NULL) {
+                njs_memory_error(vm);
+                goto fail;
+            }
+
+            cln->handler = njs_webcrypto_cleanup_pkey;
+            cln->data = key;
+
+            dkey->pkey = pkey;
+
+        } else {
+            dkey->raw.start = k;
+            dkey->raw.length = length;
+        }
+
+        ret = njs_vm_external_create(vm, &value,
+                                     njs_webcrypto_crypto_key_proto_id,
+                                     dkey, 0);
+    } else {
+        ret = njs_vm_value_array_buffer_set(vm, &value, k, length);
+    }
+
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    return njs_webcrypto_result(vm, &value, NJS_OK);
+
+fail:
+
+    return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static njs_int_t
+njs_ext_digest(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    unsigned              olen;
+    u_char                *dst;
+    njs_str_t             data;
+    njs_int_t             ret;
+    njs_value_t           value;
+    const EVP_MD          *md;
+    njs_webcrypto_hash_t  hash;
+
+    ret = njs_algorithm_hash(vm, njs_arg(args, nargs, 1), &hash);
+    if (njs_slow_path(ret == NJS_ERROR)) {
+        goto fail;
+    }
+
+    ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 2));
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    md = njs_algorithm_hash_digest(hash);
+    olen = EVP_MD_size(md);
+
+    dst = njs_mp_zalloc(njs_vm_memory_pool(vm), olen);
+    if (njs_slow_path(dst == NULL)) {
+        njs_memory_error(vm);
+        goto fail;
+    }
+
+    ret = EVP_Digest(data.start, data.length, dst, &olen, md, NULL);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_Digest() failed");
+        goto fail;
+    }
+
+    ret = njs_vm_value_array_buffer_set(vm, &value, dst, olen);
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    return njs_webcrypto_result(vm, &value, NJS_OK);
+
+fail:
+
+    return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static njs_int_t
+njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    njs_internal_error(vm, "\"exportKey\" not implemented");
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    njs_internal_error(vm, "\"generateKey\" not implemented");
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    int                         nid;
+    BIO                         *bio;
+    RSA                         *rsa;
+    EC_KEY                      *ec;
+    unsigned                    usage;
+    EVP_PKEY                    *pkey;
+    njs_int_t                   ret;
+    njs_str_t                   key_data, format;
+    njs_value_t                 value, *options;
+    const u_char                *start;
+    const EC_GROUP              *group;
+    njs_mp_cleanup_t            *cln;
+    njs_webcrypto_key_t         *key;
+    PKCS8_PRIV_KEY_INFO         *pkcs8;
+    njs_webcrypto_algorithm_t   *alg;
+    njs_webcrypto_key_format_t  fmt;
+
+    static const int curves[] = {
+        NID_X9_62_prime256v1,
+        NID_secp384r1,
+        NID_secp521r1,
+    };
+
+    pkey = NULL;
+
+    fmt = njs_key_format(vm, njs_arg(args, nargs, 1), &format);
+    if (njs_slow_path(fmt == NJS_KEY_FORMAT_UNKNOWN)) {
+        njs_type_error(vm, "unknown key format: \"%V\"", &format);
+        goto fail;
+    }
+
+    options = njs_arg(args, nargs, 3);
+    alg = njs_key_algorithm(vm, options);
+    if (njs_slow_path(alg == NULL)) {
+        goto fail;
+    }
+
+    if (njs_slow_path(!(fmt & alg->fmt))) {
+        njs_type_error(vm, "unsupported key fmt for \"%V\" key",
+                       njs_algorithm_string(alg));
+        goto fail;
+    }
+
+    ret = njs_key_usage(vm, njs_arg(args, nargs, 5), &usage);
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    if (njs_slow_path(usage & ~alg->usage)) {
+        njs_type_error(vm, "unsupported key usage for \"%V\" key",
+                       njs_algorithm_string(alg));
+        goto fail;
+    }
+
+    ret = njs_vm_value_to_bytes(vm, &key_data, njs_arg(args, nargs, 2));
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    start = key_data.start;
+
+    switch (fmt) {
+    case NJS_KEY_FORMAT_PKCS8:
+        bio = BIO_new_mem_buf(start, key_data.length);
+        if (njs_slow_path(bio == NULL)) {
+            njs_webcrypto_error(vm, "BIO_new_mem_buf() failed");
+            goto fail;
+        }
+
+        pkcs8 = d2i_PKCS8_PRIV_KEY_INFO_bio(bio, NULL);
+        if (njs_slow_path(pkcs8 == NULL)) {
+            BIO_free(bio);
+            njs_webcrypto_error(vm, "d2i_PKCS8_PRIV_KEY_INFO_bio() failed");
+            goto fail;
+        }
+
+        pkey = EVP_PKCS82PKEY(pkcs8);
+        if (njs_slow_path(pkey == NULL)) {
+            PKCS8_PRIV_KEY_INFO_free(pkcs8);
+            BIO_free(bio);
+            njs_webcrypto_error(vm, "EVP_PKCS82PKEY() failed");
+            goto fail;
+        }
+
+        PKCS8_PRIV_KEY_INFO_free(pkcs8);
+        BIO_free(bio);
+
+        break;
+
+    case NJS_KEY_FORMAT_SPKI:
+        pkey = d2i_PUBKEY(NULL, &start, key_data.length);
+        if (njs_slow_path(pkey == NULL)) {
+            njs_webcrypto_error(vm, "d2i_PUBKEY() failed");
+            goto fail;
+        }
+
+        break;
+
+    case NJS_KEY_FORMAT_RAW:
+        break;
+
+    default:
+        njs_internal_error(vm, "not implemented key format: \"%V\"", &format);
+        goto fail;
+    }
+
+    key = njs_mp_zalloc(njs_vm_memory_pool(vm), sizeof(njs_webcrypto_key_t));
+    if (njs_slow_path(key == NULL)) {
+        njs_memory_error(vm);
+        goto fail;
+    }
+
+    key->alg = alg;
+    key->usage = usage;
+
+    switch (alg->type) {
+    case NJS_ALGORITHM_RSA_OAEP:
+    case NJS_ALGORITHM_RSA_PSS:
+    case NJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+        rsa = EVP_PKEY_get1_RSA(pkey);
+        if (njs_slow_path(rsa == NULL)) {
+            njs_webcrypto_error(vm, "RSA key is not found");
+            goto fail;
+        }
+
+        RSA_free(rsa);
+
+        ret = njs_algorithm_hash(vm, options, &key->hash);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto fail;
+        }
+
+        key->pkey = pkey;
+
+        break;
+
+    case NJS_ALGORITHM_ECDSA:
+    case NJS_ALGORITHM_ECDH:
+        ec = EVP_PKEY_get1_EC_KEY(pkey);
+        if (njs_slow_path(ec == NULL)) {
+            njs_webcrypto_error(vm, "EC key is not found");
+            goto fail;
+        }
+
+        group = EC_KEY_get0_group(ec);
+        nid = EC_GROUP_get_curve_name(group);
+        EC_KEY_free(ec);
+
+        ret = njs_algorithm_curve(vm, options, &key->curve);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto fail;
+        }
+
+        if (njs_slow_path(curves[key->curve] != nid)) {
+            njs_webcrypto_error(vm, "name curve mismatch");
+            goto fail;
+        }
+
+        key->pkey = pkey;
+
+        break;
+
+    case NJS_ALGORITHM_HMAC:
+        pkey = EVP_PKEY_new_mac_key(EVP_PKEY_HMAC, NULL, key_data.start,
+                                    key_data.length);
+        if (njs_slow_path(pkey == NULL)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_new_mac_key() failed");
+            goto fail;
+        }
+
+        ret = njs_algorithm_hash(vm, options, &key->hash);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto fail;
+        }
+
+        key->pkey = pkey;
+
+        break;
+
+    case NJS_ALGORITHM_AES_GCM:
+    case NJS_ALGORITHM_AES_CTR:
+    case NJS_ALGORITHM_AES_CBC:
+    case NJS_ALGORITHM_PBKDF2:
+    case NJS_ALGORITHM_HKDF:
+        key->raw = key_data;
+    default:
+        break;
+    }
+
+    if (pkey != NULL) {
+        cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0);
+        if (cln == NULL) {
+            njs_memory_error(vm);
+            goto fail;
+        }
+
+        cln->handler = njs_webcrypto_cleanup_pkey;
+        cln->data = key;
+        pkey = NULL;
+    }
+
+    ret = njs_vm_external_create(vm, &value, njs_webcrypto_crypto_key_proto_id,
+                                 key, 0);
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto fail;
+    }
+
+    return njs_webcrypto_result(vm, &value, NJS_OK);
+
+fail:
+
+    if (pkey != NULL) {
+        EVP_PKEY_free(pkey);
+    }
+
+    return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static njs_int_t
+njs_set_rsa_padding(njs_vm_t *vm, njs_value_t *options, EVP_PKEY *pkey,
+    EVP_PKEY_CTX *ctx, njs_webcrypto_alg_t type)
+{
+    int          padding;
+    int64_t      salt_length;
+    njs_int_t    ret;
+    njs_value_t  value;
+
+    static const njs_value_t  string_saltl = njs_string("saltLength");
+
+    if (type == NJS_ALGORITHM_ECDSA) {
+        return NJS_OK;
+    }
+
+    padding = (type == NJS_ALGORITHM_RSA_PSS) ? RSA_PKCS1_PSS_PADDING
+                                              : RSA_PKCS1_PADDING;
+    ret = EVP_PKEY_CTX_set_rsa_padding(ctx, padding);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_PKEY_CTX_set_rsa_padding() failed");
+        return NJS_ERROR;
+    }
+
+    if (padding == RSA_PKCS1_PSS_PADDING) {
+        ret = njs_value_property(vm, options, njs_value_arg(&string_saltl),
+                                 &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            if (ret == NJS_DECLINED) {
+                njs_type_error(vm, "RSA-PSS algorithm.saltLength "
+                               "is not provided");
+            }
+
+            return NJS_ERROR;
+        }
+
+        ret = njs_value_to_integer(vm, &value, &salt_length);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+
+        ret = EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, salt_length);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm,
+                                "EVP_PKEY_CTX_set_rsa_pss_saltlen() failed");
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t verify)
+{
+    u_char                     *dst;
+    size_t                     olen, outlen;
+    unsigned                   mask, m_len;
+    njs_int_t                  ret;
+    njs_str_t                  data, sig;
+    EVP_MD_CTX                 *mctx;
+    njs_value_t                value, *options;
+    EVP_PKEY_CTX               *pctx;
+    const EVP_MD               *md;
+    njs_webcrypto_key_t        *key;
+    njs_webcrypto_hash_t       hash;
+    njs_webcrypto_algorithm_t  *alg;
+    unsigned char              m[EVP_MAX_MD_SIZE];
+
+    mctx = NULL;
+    pctx = NULL;
+
+    options = njs_arg(args, nargs, 1);
+    alg = njs_key_algorithm(vm, options);
+    if (njs_slow_path(alg == NULL)) {
+        goto fail;
+    }
+
+    key = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id,
+                          njs_arg(args, nargs, 2));
+    if (njs_slow_path(key == NULL)) {
+        njs_type_error(vm, "\"key\" is not a CryptoKey object");
+        goto fail;
+    }
+
+    mask = verify ? NJS_KEY_USAGE_VERIFY : NJS_KEY_USAGE_SIGN;
+    if (njs_slow_path(!(key->usage & mask))) {
+        njs_type_error(vm, "provide key does not support \"sign\" operation");
+        goto fail;
+    }
+
+    if (njs_slow_path(key->alg != alg)) {
+        njs_type_error(vm, "cannot %s using \"%V\" with \"%V\" key",
+                       verify ? "verify" : "sign",
+                       njs_algorithm_string(key->alg),
+                       njs_algorithm_string(alg));
+        goto fail;
+    }
+
+    if (verify) {
+        ret = njs_vm_value_to_bytes(vm, &sig, njs_arg(args, nargs, 3));
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 4));
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+    } else {
+        ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 3));
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+    }
+
+    mctx = njs_evp_md_ctx_new();
+    if (njs_slow_path(mctx == NULL)) {
+        njs_webcrypto_error(vm, "njs_evp_md_ctx_new() failed");
+        goto fail;
+    }
+
+    if (alg->type == NJS_ALGORITHM_ECDSA) {
+        ret = njs_algorithm_hash(vm, options, &hash);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto fail;
+        }
+
+    } else {
+        hash = key->hash;
+    }
+
+    md = njs_algorithm_hash_digest(hash);
+
+    ret = EVP_DigestSignInit(mctx, NULL, md, NULL, key->pkey);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_DigestSignInit() failed");
+        goto fail;
+    }
+
+    ret = EVP_DigestSignUpdate(mctx, data.start, data.length);
+    if (njs_slow_path(ret <= 0)) {
+        njs_webcrypto_error(vm, "EVP_DigestSignUpdate() failed");
+        goto fail;
+    }
+
+    outlen = 0;
+
+    switch (alg->type) {
+    case NJS_ALGORITHM_HMAC:
+        olen = EVP_MD_size(md);
+
+        if (!verify) {
+            dst = njs_mp_zalloc(njs_vm_memory_pool(vm), olen);
+            if (njs_slow_path(dst == NULL)) {
+                njs_memory_error(vm);
+                goto fail;
+            }
+
+        } else {
+            dst = (u_char *) &m[0];
+        }
+
+        ret = EVP_DigestSignFinal(mctx, dst, &outlen);
+        if (njs_slow_path(ret <= 0 || olen != outlen)) {
+            njs_webcrypto_error(vm, "EVP_DigestSignFinal() failed");
+            goto fail;
+        }
+
+        if (verify) {
+            ret = (sig.length == outlen && memcmp(sig.start, dst, outlen) == 0);
+        }
+
+        break;
+
+    case NJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+    case NJS_ALGORITHM_RSA_PSS:
+    case NJS_ALGORITHM_ECDSA:
+    default:
+        ret = EVP_DigestFinal_ex(mctx, m, &m_len);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_DigestFinal_ex() failed");
+            goto fail;
+        }
+
+        olen = EVP_PKEY_size(key->pkey);
+        dst = njs_mp_zalloc(njs_vm_memory_pool(vm), olen);
+        if (njs_slow_path(dst == NULL)) {
+            njs_memory_error(vm);
+            goto fail;
+        }
+
+        pctx = EVP_PKEY_CTX_new(key->pkey, NULL);
+        if (njs_slow_path(pctx == NULL)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_CTX_new() failed");
+            goto fail;
+        }
+
+        if (!verify) {
+            ret = EVP_PKEY_sign_init(pctx);
+            if (njs_slow_path(ret <= 0)) {
+                njs_webcrypto_error(vm, "EVP_PKEY_sign_init() failed");
+                goto fail;
+            }
+
+        } else {
+            ret = EVP_PKEY_verify_init(pctx);
+            if (njs_slow_path(ret <= 0)) {
+                njs_webcrypto_error(vm, "EVP_PKEY_verify_init() failed");
+                goto fail;
+            }
+        }
+
+        ret = njs_set_rsa_padding(vm, options, key->pkey, pctx, alg->type);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        ret = EVP_PKEY_CTX_set_signature_md(pctx, md);
+        if (njs_slow_path(ret <= 0)) {
+            njs_webcrypto_error(vm, "EVP_PKEY_CTX_set_signature_md() failed");
+            goto fail;
+        }
+
+        if (!verify) {
+            outlen = olen;
+            ret = EVP_PKEY_sign(pctx, dst, &outlen, m, m_len);
+            if (njs_slow_path(ret <= 0)) {
+                njs_webcrypto_error(vm, "EVP_PKEY_sign() failed");
+                goto fail;
+            }
+
+        } else {
+            ret = EVP_PKEY_verify(pctx, sig.start, sig.length, m, m_len);
+            if (njs_slow_path(ret < 0)) {
+                njs_webcrypto_error(vm, "EVP_PKEY_verify() failed");
+                goto fail;
+            }
+        }
+
+        EVP_PKEY_CTX_free(pctx);
+
+        break;
+    }
+
+    if (!verify) {
+        ret = njs_vm_value_array_buffer_set(vm, &value, dst, outlen);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+    } else {
+        njs_set_boolean(&value, ret != 0);
+    }
+
+    njs_evp_md_ctx_free(mctx);
+
+    return njs_webcrypto_result(vm, &value, NJS_OK);
+
+fail:
+
+    if (mctx != NULL) {
+        njs_evp_md_ctx_free(mctx);
+    }
+
+    if (pctx != NULL) {
+        EVP_PKEY_CTX_free(pctx);
+    }
+
+    return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static njs_int_t
+njs_ext_unwrap_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    njs_internal_error(vm, "\"unwrapKey\" not implemented");
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_ext_wrap_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    njs_internal_error(vm, "\"wrapKey\" not implemented");
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_ext_get_random_values(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    njs_int_t  ret;
+    njs_str_t  fill;
+
+    ret = njs_vm_value_to_bytes(vm, &fill, njs_arg(args, nargs, 1));
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    if (njs_slow_path(fill.length > 65536)) {
+        njs_type_error(vm, "requested length exceeds 65536 bytes");
+        return NJS_ERROR;
+    }
+
+    if (RAND_bytes(fill.start, fill.length) != 1) {
+        njs_webcrypto_error(vm, "RAND_bytes() failed");
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
+
+
+static void
+njs_webcrypto_cleanup_pkey(void *data)
+{
+    njs_webcrypto_key_t  *key = data;
+
+    if (key->pkey != NULL) {
+        EVP_PKEY_free(key->pkey);
+    }
+}
+
+
+static njs_webcrypto_key_format_t
+njs_key_format(njs_vm_t *vm, njs_value_t *value, njs_str_t *format)
+{
+    njs_int_t   ret;
+    njs_uint_t  fmt;
+
+    static const struct {
+        njs_str_t   name;
+        njs_uint_t  value;
+    } formats[] = {
+        { njs_str("raw"), NJS_KEY_FORMAT_RAW },
+        { njs_str("pkcs8"), NJS_KEY_FORMAT_PKCS8 },
+        { njs_str("spki"), NJS_KEY_FORMAT_SPKI },
+        { njs_str("jwk"), NJS_KEY_FORMAT_JWK },
+    };
+
+    ret = njs_value_to_string(vm, value, value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    njs_string_get(value, format);
+
+    fmt = 0;
+
+    while (fmt < sizeof(formats) / sizeof(formats[0])) {
+        if (njs_strstr_eq(format, &formats[fmt].name)) {
+            return formats[fmt].value;
+        }
+
+        fmt++;
+    }
+
+    return NJS_KEY_FORMAT_UNKNOWN;
+}
+
+
+static njs_int_t
+njs_key_usage_array_handler(njs_vm_t *vm, njs_iterator_args_t *args,
+    njs_value_t *value, int64_t index)
+{
+    unsigned               *mask;
+    njs_str_t              u;
+    njs_int_t              ret;
+    njs_value_t            usage;
+    njs_webcrypto_entry_t  *e;
+
+    njs_value_assign(&usage, value);
+
+    ret = njs_value_to_string(vm, &usage, &usage);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    njs_string_get(&usage, &u);
+
+    for (e = &njs_webcrypto_usage[0]; e->name.length != 0; e++) {
+        if (njs_strstr_eq(&u, &e->name)) {
+            mask = args->data;
+            *mask |= e->value;
+            return NJS_OK;
+        }
+    }
+
+    njs_type_error(vm, "unknown key usage: \"%V\"", &u);
+
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_key_usage(njs_vm_t *vm, njs_value_t *value, unsigned *mask)
+{
+    int64_t              length;
+    njs_int_t            ret;
+    njs_iterator_args_t  args;
+
+    ret = njs_object_length(vm, value, &length);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    *mask = 0;
+
+    args.value = value;
+    args.from = 0;
+    args.to = length;
+    args.data = mask;
+
+    return njs_object_iterate(vm, &args, njs_key_usage_array_handler);
+}
+
+
+static njs_webcrypto_algorithm_t *
+njs_key_algorithm(njs_vm_t *vm, njs_value_t *options)
+{
+    njs_int_t                  ret;
+    njs_str_t                  a;
+    njs_value_t                name;
+    njs_webcrypto_entry_t      *e;
+    njs_webcrypto_algorithm_t  *alg;
+
+    static const njs_value_t  string_name = njs_string("name");
+
+    if (njs_is_object(options)) {
+        ret = njs_value_property(vm, options, njs_value_arg(&string_name),
+                                 &name);
+        if (njs_slow_path(ret != NJS_OK)) {
+            if (ret == NJS_DECLINED) {
+                njs_type_error(vm, "algorithm name is not provided");
+            }
+
+            return NULL;
+        }
+
+    } else {
+        njs_value_assign(&name, options);
+    }
+
+    ret = njs_value_to_string(vm, &name, &name);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NULL;
+    }
+
+    njs_string_get(&name, &a);
+
+    for (e = &njs_webcrypto_alg[0]; e->name.length != 0; e++) {
+        if (njs_strstr_case_eq(&a, &e->name)) {
+            alg = (njs_webcrypto_algorithm_t *) e->value;
+            if (alg->usage & NJS_KEY_USAGE_UNSUPPORTED) {
+                njs_type_error(vm, "unsupported algorithm: \"%V\"", &a);
+                return NULL;
+            }
+
+            return alg;
+        }
+    }
+
+    njs_type_error(vm, "unknown algorithm name: \"%V\"", &a);
+
+    return NULL;
+}
+
+
+static njs_str_t *
+njs_algorithm_string(njs_webcrypto_algorithm_t *algorithm)
+{
+    njs_webcrypto_entry_t      *e;
+    njs_webcrypto_algorithm_t  *alg;
+
+    for (e = &njs_webcrypto_alg[0]; e->name.length != 0; e++) {
+        alg = (njs_webcrypto_algorithm_t *) e->value;
+        if (alg->type == algorithm->type) {
+            break;
+        }
+    }
+
+    return &e->name;
+}
+
+
+static njs_int_t
+njs_algorithm_hash(njs_vm_t *vm, njs_value_t *options,
+    njs_webcrypto_hash_t *hash)
+{
+    njs_int_t              ret;
+    njs_str_t              name;
+    njs_value_t            value;
+    njs_webcrypto_entry_t  *e;
+
+    static const njs_value_t  string_hash = njs_string("hash");
+
+    if (njs_is_object(options)) {
+        ret = njs_value_property(vm, options, njs_value_arg(&string_hash),
+                                 &value);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            return NJS_ERROR;
+        }
+
+    } else {
+        njs_value_assign(&value, options);
+    }
+
+    ret = njs_value_to_string(vm, &value, &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    njs_string_get(&value, &name);
+
+    for (e = &njs_webcrypto_hash[0]; e->name.length != 0; e++) {
+        if (njs_strstr_eq(&name, &e->name)) {
+            *hash = e->value;
+            return NJS_OK;
+        }
+    }
+
+    njs_type_error(vm, "unknown hash name: \"%V\"", &name);
+
+    return NJS_ERROR;
+}
+
+
+static const EVP_MD *
+njs_algorithm_hash_digest(njs_webcrypto_hash_t hash)
+{
+    switch (hash) {
+    case NJS_HASH_SHA256:
+        return EVP_sha256();
+
+    case NJS_HASH_SHA384:
+        return EVP_sha384();
+
+    case NJS_HASH_SHA512:
+        return EVP_sha512();
+
+    case NJS_HASH_SHA1:
+    default:
+        break;
+    }
+
+    return EVP_sha1();
+}
+
+
+static njs_int_t
+njs_algorithm_curve(njs_vm_t *vm, njs_value_t *options,
+    njs_webcrypto_curve_t *curve)
+{
+    njs_int_t              ret;
+    njs_str_t              name;
+    njs_value_t            value;
+    njs_webcrypto_entry_t  *e;
+
+    static const njs_value_t  string_curve = njs_string("namedCurve");
+
+    ret = njs_value_property(vm, options, njs_value_arg(&string_curve),
+                             &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return ret;
+    }
+
+    ret = njs_value_to_string(vm, &value, &value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    njs_string_get(&value, &name);
+
+    for (e = &njs_webcrypto_curve[0]; e->name.length != 0; e++) {
+        if (njs_strstr_eq(&name, &e->name)) {
+            *curve = e->value;
+            return NJS_OK;
+        }
+    }
+
+    njs_type_error(vm, "unknown namedCurve: \"%V\"", &name);
+
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_promise_trampoline(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused)
+{
+    njs_function_t  *callback;
+
+    callback = njs_value_function(njs_argument(args, 1));
+
+    if (callback != NULL) {
+        return njs_vm_call(vm, callback, njs_argument(args, 2), 1);
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_webcrypto_result(njs_vm_t *vm, njs_value_t *result, njs_int_t rc)
+{
+    njs_int_t       ret;
+    njs_value_t     retval, arguments[2];
+    njs_function_t  *callback;
+    njs_vm_event_t  vm_event;
+
+    ret = njs_vm_promise_create(vm, &retval, njs_value_arg(&arguments));
+    if (ret != NJS_OK) {
+        goto error;
+    }
+
+    callback = njs_vm_function_alloc(vm, njs_promise_trampoline);
+    if (callback == NULL) {
+        goto error;
+    }
+
+    vm_event = njs_vm_add_event(vm, callback, 1, NULL, NULL);
+    if (vm_event == NULL) {
+        goto error;
+    }
+
+    njs_value_assign(&arguments[0], &arguments[(rc != NJS_OK)]);
+    njs_value_assign(&arguments[1], result);
+
+    ret = njs_vm_post_event(vm, vm_event, njs_value_arg(&arguments), 2);
+    if (ret == NJS_ERROR) {
+        goto error;
+    }
+
+    njs_vm_retval_set(vm, njs_value_arg(&retval));
+
+    return NJS_OK;
+
+error:
+
+    njs_vm_error(vm, "internal error");
+
+    return NJS_ERROR;
+}
+
+
+static u_char *
+njs_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 void
+njs_webcrypto_error(njs_vm_t *vm, 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 = njs_cpystrn(p, (u_char *) " (SSL:", last - p);
+
+        for ( ;; ) {
+
+            n = ERR_peek_error_line_data(NULL, NULL, &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 = njs_cpystrn(p, (u_char *) data, last - p);
+            }
+
+        next:
+
+            (void) ERR_get_error();
+        }
+
+        if (p < last) {
+            *p++ = ')';
+        }
+    }
+
+    njs_vm_value_error_set(vm, njs_vm_retval(vm), "%*s", p - errstr, errstr);
+}
+
+
+njs_int_t
+njs_external_webcrypto_init(njs_vm_t *vm)
+{
+    njs_int_t           ret, proto_id;
+    njs_str_t           name;
+    njs_opaque_value_t  value;
+
+    OpenSSL_add_all_algorithms();
+
+    njs_webcrypto_crypto_key_proto_id =
+        njs_vm_external_prototype(vm, njs_ext_webcrypto_crypto_key,
+                                  njs_nitems(njs_ext_webcrypto_crypto_key));
+    if (njs_slow_path(njs_webcrypto_crypto_key_proto_id < 0)) {
+        return NJS_ERROR;
+    }
+
+    proto_id = njs_vm_external_prototype(vm, njs_ext_webcrypto,
+                                         njs_nitems(njs_ext_webcrypto));
+    if (njs_slow_path(proto_id < 0)) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_external_create(vm, njs_value_arg(&value), proto_id, NULL, 1);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    name.length = njs_length("crypto");
+    name.start = (u_char *) "crypto";
+
+    ret = njs_vm_bind(vm, &name, njs_value_arg(&value), 1);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
diff --git a/external/njs_webcrypto.h b/external/njs_webcrypto.h
new file mode 100644
index 0000000..3331b57
--- /dev/null
+++ b/external/njs_webcrypto.h
@@ -0,0 +1,15 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#ifndef _NJS_EXTERNAL_WEBCRYPTO_H_INCLUDED_
+#define _NJS_EXTERNAL_WEBCRYPTO_H_INCLUDED_
+
+
+njs_int_t njs_external_webcrypto_init(njs_vm_t *vm);
+
+
+#endif /* _NJS_EXTERNAL_WEBCRYPTO_H_INCLUDED_ */
diff --git a/nginx/config b/nginx/config
index 29f6dca..7ebdcfe 100644
--- a/nginx/config
+++ b/nginx/config
@@ -1,8 +1,11 @@
 ngx_addon_name="ngx_js_module"
 
-NJS_DEPS="$ngx_addon_dir/ngx_js.h"
+NJS_DEPS="$ngx_addon_dir/ngx_js.h \
+    $ngx_addon_dir/ngx_js_fetch.h \
+    $ngx_addon_dir/../external/njs_webcrypto.h"
 NJS_SRCS="$ngx_addon_dir/ngx_js.c \
-    $ngx_addon_dir/ngx_js_fetch.c"
+    $ngx_addon_dir/ngx_js_fetch.c \
+    $ngx_addon_dir/../external/njs_webcrypto.c"
 
 if [ $HTTP != NO ]; then
     ngx_module_type=HTTP_AUX_FILTER
@@ -10,7 +13,7 @@
     ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build"
     ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS"
     ngx_module_srcs="$ngx_addon_dir/ngx_http_js_module.c $NJS_SRCS"
-    ngx_module_libs="PCRE $ngx_addon_dir/../build/libnjs.a -lm"
+    ngx_module_libs="PCRE OPENSSL $ngx_addon_dir/../build/libnjs.a -lm"
 
     . auto/module
 
@@ -25,7 +28,7 @@
     ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build"
     ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS"
     ngx_module_srcs="$ngx_addon_dir/ngx_stream_js_module.c $NJS_SRCS"
-    ngx_module_libs="PCRE $ngx_addon_dir/../build/libnjs.a -lm"
+    ngx_module_libs="PCRE OPENSSL $ngx_addon_dir/../build/libnjs.a -lm"
 
     . auto/module
 fi
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c
index ae7955e..012adb4 100644
--- a/nginx/ngx_js.c
+++ b/nginx/ngx_js.c
@@ -10,6 +10,7 @@
 #include <ngx_core.h>
 #include "ngx_js.h"
 #include "ngx_js_fetch.h"
+#include "../external/njs_webcrypto.h"
 
 
 static njs_external_t  ngx_js_ext_core[] = {
@@ -176,6 +177,12 @@
         return NGX_ERROR;
     }
 
+    ret = njs_external_webcrypto_init(vm);
+    if (ret != NJS_OK) {
+        ngx_log_error(NGX_LOG_EMERG, log, 0, "failed to add webcrypto object");
+        return NGX_ERROR;
+    }
+
     proto_id = njs_vm_external_prototype(vm, ngx_js_ext_core,
                                          njs_nitems(ngx_js_ext_core));
     if (proto_id < 0) {
diff --git a/src/njs_shell.c b/src/njs_shell.c
index bfa9a37..5abb171 100644
--- a/src/njs_shell.c
+++ b/src/njs_shell.c
@@ -23,6 +23,11 @@
 
 #endif
 
+#if (NJS_HAVE_OPENSSL)
+#include "../external/njs_webcrypto.h"
+#include "../external/njs_webcrypto.c"
+#endif
+
 
 typedef struct {
     uint8_t                 disassemble;
@@ -718,6 +723,13 @@
         return NJS_ERROR;
     }
 
+#if (NJS_HAVE_OPENSSL)
+    ret = njs_external_webcrypto_init(vm);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+#endif
+
     return NJS_OK;
 }
 
diff --git a/src/njs_str.c b/src/njs_str.c
new file mode 100644
index 0000000..fdad373
--- /dev/null
+++ b/src/njs_str.c
@@ -0,0 +1,37 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#include <njs_main.h>
+
+
+njs_int_t
+njs_strncasecmp(u_char *s1, u_char *s2, size_t n)
+{
+    njs_uint_t  c1, c2;
+
+    while (n) {
+        c1 = (njs_uint_t) *s1++;
+        c2 = (njs_uint_t) *s2++;
+
+        c1 = (c1 >= 'A' && c1 <= 'Z') ? (c1 | 0x20) : c1;
+        c2 = (c2 >= 'A' && c2 <= 'Z') ? (c2 | 0x20) : c2;
+
+        if (c1 == c2) {
+
+            if (c1) {
+                n--;
+                continue;
+            }
+
+            return 0;
+        }
+
+        return c1 - c2;
+    }
+
+    return 0;
+}
diff --git a/src/njs_str.h b/src/njs_str.h
index 5bc013d..ae4dd08 100644
--- a/src/njs_str.h
+++ b/src/njs_str.h
@@ -134,4 +134,13 @@
      && (memcmp((s1)->start, (s2)->start, (s1)->length) == 0))
 
 
+#define                                                                       \
+njs_strstr_case_eq(s1, s2)                                                    \
+    (((s1)->length == (s2)->length)                                           \
+     && (njs_strncasecmp((s1)->start, (s2)->start, (s1)->length) == 0))
+
+
+NJS_EXPORT njs_int_t njs_strncasecmp(u_char *s1, u_char *s2, size_t n);
+
+
 #endif /* _NJS_STR_H_INCLUDED_ */
diff --git a/src/test/njs_externals_test.c b/src/test/njs_externals_test.c
index c52e65b..3285b16 100644
--- a/src/test/njs_externals_test.c
+++ b/src/test/njs_externals_test.c
@@ -8,6 +8,11 @@
 
 #include "njs_externals_test.h"
 
+#if (NJS_HAVE_OPENSSL)
+#include "../external/njs_webcrypto.h"
+#include "../external/njs_webcrypto.c"
+#endif
+
 
 typedef struct {
     njs_lvlhsh_t          hash;
@@ -833,6 +838,15 @@
 njs_int_t
 njs_externals_shared_init(njs_vm_t *vm)
 {
+#if (NJS_HAVE_OPENSSL)
+    njs_int_t  ret;
+
+    ret = njs_external_webcrypto_init(vm);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+#endif
+
     return njs_externals_init_internal(vm, njs_test_requests, 1, 1);
 }
 
diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c
index ae97e76..4309c50 100644
--- a/src/test/njs_unit_test.c
+++ b/src/test/njs_unit_test.c
@@ -20621,6 +20621,26 @@
 };
 
 
+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") },
+};
+
+
 static njs_unit_test_t  njs_module_test[] =
 {
     { njs_str("function f(){return 2}; var f; f()"),
@@ -20854,13 +20874,21 @@
     { njs_str("$r2.uri == 'αβγ' && $r2.uri === 'αβγ'"),
       njs_str("true") },
 
-    { njs_str("Object.keys(this).sort()"),
 #if (NJS_TEST262)
-      njs_str("$262,$r,$r2,$r3,$shared,global,njs,process") },
+#define N262 "$262,"
 #else
-      njs_str("$r,$r2,$r3,$shared,global,njs,process") },
+#define N262 ""
 #endif
 
+#if (NJS_HAVE_OPENSSL)
+#define NCRYPTO "crypto,"
+#else
+#define NCRYPTO ""
+#endif
+
+    { njs_str("Object.keys(this).sort()"),
+      njs_str(N262 "$r,$r2,$r3,$shared," NCRYPTO "global,njs,process") },
+
     { njs_str("Object.getOwnPropertySymbols($r2)[0] == Symbol.toStringTag"),
       njs_str("true") },
 
@@ -23246,6 +23274,17 @@
       njs_nitems(njs_disabled_denormals_test),
       njs_disabled_denormals_tests },
 
+    {
+#if (NJS_HAVE_OPENSSL)
+        njs_str("webcrypto"),
+#else
+        njs_str(""),
+#endif
+      { .externals = 1, .repeat = 1, .unsafe = 1 },
+      njs_webcrypto_test,
+      njs_nitems(njs_webcrypto_test),
+      njs_unit_test },
+
     { njs_str("module"),
       { .repeat = 1, .module = 1, .unsafe = 1 },
       njs_module_test,
diff --git a/test/njs_expect_test.exp b/test/njs_expect_test.exp
index cc3bebe..9b2287a 100644
--- a/test/njs_expect_test.exp
+++ b/test/njs_expect_test.exp
@@ -1113,6 +1113,38 @@
 njs_run {"./test/js/promise_race_throw.js"} \
 "rejected:one"
 
+# Webcrypto
+
+njs_run {"./test/webcrypto/rsa_decoding.js" "--match-exception-text"} \
+"RSA-OAEP decoding SUCCESS"
+
+njs_run {"./test/webcrypto/rsa.js" "--match-exception-text"} \
+"RSA-OAEP encoding/decoding SUCCESS"
+
+njs_run {"./test/webcrypto/aes_decoding.js" "--match-exception-text"} \
+"AES decoding SUCCESS"
+
+njs_run {"./test/webcrypto/aes.js" "--match-exception-text"} \
+"AES encoding/decoding SUCCESS"
+
+njs_run {"./test/webcrypto/derive.js" "--match-exception-text"} \
+"derive SUCCESS"
+
+njs_run {"./test/webcrypto/digest.js" "--match-exception-text"} \
+"SHA digest SUCCESS"
+
+njs_run {"./test/webcrypto/sign.js" "--match-exception-text"} \
+"HMAC sign SUCCESS
+RSASSA-PKCS1-v1_5 sign SUCCESS
+RSA-PSS sign SUCCESS
+ECDSA sign SUCCESS"
+
+njs_run {"./test/webcrypto/verify.js" "--match-exception-text"} \
+"HMAC verify SUCCESS
+RSASSA-PKCS1-v1_5 verify SUCCESS
+RSA-PSS verify SUCCESS
+ECDSA verify SUCCESS"
+
 # Async/Await
 
 njs_run {"./test/js/async_await_inline.js"} \
diff --git a/test/ts/test.ts b/test/ts/test.ts
index 4ec9db0..a30c588 100644
--- a/test/ts/test.ts
+++ b/test/ts/test.ts
@@ -1,6 +1,6 @@
 import fs from 'fs';
 import qs from 'querystring';
-import crypto from 'crypto';
+import cr from 'crypto';
 
 function http_module(r: NginxHTTPRequest) {
     var bs: NjsByteString;
@@ -122,11 +122,30 @@
     var b:Buffer;
     var s:string;
 
-    h = crypto.createHash("sha1");
+    h = cr.createHash("sha1");
     h = h.update(str).update(Buffer.from([0]));
     b = h.digest();
 
-    s = crypto.createHash("sha256").digest("hex");
+    s = cr.createHash("sha256").digest("hex");
+}
+
+async function crypto_object(keyData: ArrayBuffer, data: ArrayBuffer) {
+    let iv = crypto.getRandomValues(new Uint8Array(16));
+
+    let ekey = await crypto.subtle.importKey("pkcs8", keyData,
+                                             {name: 'RSA-OAEP', hash: "SHA-256"},
+                                             false, ['decrypt']);
+
+    let skey = await crypto.subtle.importKey("raw", keyData, 'AES-CBC',
+                                             false, ['encrypt']);
+
+    data = await crypto.subtle.decrypt({name: 'RSA-OAEP'}, ekey, data);
+    data = await crypto.subtle.encrypt({name: 'AES-CBC', iv:iv}, skey, data);
+
+    let sig = await crypto.subtle.sign({name: 'RSA-PSS', saltLength:32}, skey, data);
+
+    let r:boolean;
+    r = await crypto.subtle.verify({name: 'RSA-PSS', saltLength:32}, skey, sig, data);
 }
 
 function buffer(b: Buffer) {
diff --git a/test/webcrypto/README.rst b/test/webcrypto/README.rst
new file mode 100644
index 0000000..a06d1b4
--- /dev/null
+++ b/test/webcrypto/README.rst
@@ -0,0 +1,136 @@
+===============
+WebCrypto tests
+===============
+
+Intro
+=====
+
+Tests in this folder are expected to be compatible with node.js
+
+Tested versions
+---------------
+
+node: v16.4.0
+openssl: OpenSSL 1.1.1f  31 Mar 2020
+
+Keys generation
+===============
+
+Generating RSA PKCS8/SPKI key files
+-----------------------------------
+
+.. code-block:: shell
+
+  openssl genrsa -out rsa.pem 1024
+  openssl pkcs8 -inform PEM -in rsa.pem -nocrypt -topk8 -outform PEM -out rsa.pkcs8
+  openssl rsa -in rsa.pkcs8 -pubout > rsa.spki
+
+Generating EC PKCS8/SPKI key files
+----------------------------------
+
+.. code-block:: shell
+
+  openssl ecparam -name prime256v1 -genkey -noout -out ec.pem
+  openssl pkcs8 -inform PEM -in ec.pem -nocrypt -topk8 -outform PEM -out ec.pkcs8
+  openssl ec -in ec.pkcs8 -pubout > ec.spki
+
+Encoding
+========
+
+Encoding data using RSA-OAEP
+----------------------------
+
+.. code-block:: shell
+
+    echo -n "WAKAWAKA" > text.txt
+    openssl rsautl -inkey key.spki -pubin -in text.txt -out - -oaep -encrypt | \
+        base64 > text.base64.rsa-oaep.enc
+
+Decoding ciphertext using RSA-OAEP
+----------------------------------
+
+.. code-block:: shell
+
+    base64 -d text.base64.rsa-oaep.enc | openssl rsautl -inkey key.pkcs8 -in - -out - -oaep -decrypt
+    WAKAWAKA
+
+Encoding data using AES-GCM
+---------------------------
+
+.. code-block:: shell
+
+   echo -n "AES-GCM-SECRET-TEXT" > text.txt
+   node ./test/webcrypto/aes_gcm_enc.js '{"in":"text.txt"}' > text.base64.aes-gcm128.enc
+
+   echo -n "AES-GCM-96-TAG-LENGTH-SECRET-TEXT" > text.txt
+   node ./test/webcrypto/aes_gcm_enc.js '{"in":"text.txt","tagLength":96}' > text.base64.aes-gcm128-96.enc
+
+Encoding data using AES-CTR
+---------------------------
+
+.. code-block:: shell
+
+    echo -n "AES-CTR-SECRET-TEXT" | \
+        openssl enc -aes-128-ctr -K 00112233001122330011223300112233 -iv 44556677445566774455667744556677 | \
+        base64 > text.base64.aes-ctr128.enc
+
+Encoding data using AES-CBC
+---------------------------
+
+.. code-block:: shell
+
+    echo -n "AES-CBC-SECRET-TEXT" | \
+        openssl enc -aes-128-cbc -K 00112233001122330011223300112233 -iv 44556677445566774455667744556677 | \
+        base64 > text.base64.aes-cbc128.enc
+
+Signing
+=======
+
+Signing data using HMAC
+-----------------------
+
+.. code-block:: shell
+
+    echo -n "SigneD-TExt" > text.txt
+    openssl dgst -sha256 -mac hmac -macopt hexkey:aabbcc -binary text.txt | \
+        base64 > test/webcrypto/text.base64.sha256.hmac.sig
+
+Signing data using RSASSA-PKCS1-v1_5
+------------------------------------
+
+.. code-block:: shell
+
+    echo -n "SigneD-TExt" > text.txt
+    openssl dgst -sha256 -sigopt rsa_padding_mode:pkcs1 -sign test/webcrypto/rsa.pkcs8 text.txt | \
+        base64 > test/webcrypto/text.base64.sha256.pkcs1.sig
+    base64 -d test/webcrypto/text.base64.sha256.pkcs1.sig > text.sha256.pkcs1.sig
+    openssl dgst -sha256 -sigopt rsa_padding_mode:pkcs1 -verify test/webcrypto/rsa.spki \
+        -signature text.sha256.pkcs1.sig text.txt
+    Verified OK
+
+Signing data using RSA-PSS
+--------------------------
+
+.. code-block:: shell
+
+    echo -n "SigneD-TExt" > text.txt
+    openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 -sign test/webcrypto/rsa.pkcs8 text.txt | \
+        base64 > test/webcrypto/text.base64.sha256.rsa-pss.32.sig
+    base64 -d test/webcrypto/text.base64.sha256.rsa-pss.32.sig > text.sha256.rsa-pss.32.sig
+    openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 \
+        -verify test/webcrypto/rsa.spki -signature text.sha256.rsa-pss.sig text.txt
+    Verified OK
+
+Signing data using ECDSA
+------------------------
+
+.. code-block:: shell
+
+    echo -n "SigneD-TExt" > text.txt
+    openssl dgst -sha256 -binary text.txt > text.sha256
+    openssl pkeyutl -sign -in text.sha256 -inkey test/webcrypto/ec.pkcs8 | \
+        base64 > test/webcrypto/text.base64.sha256.ecdsa.sig
+    base64 -d test/webcrypto/text.base64.sha256.ecdsa.sig > text.sha256.ecdsa.sig
+    openssl pkeyutl -verify -in text.sha256 -pubin -inkey test/webcrypto/ec.spki  -sigfile text.sha256.ecdsa.sig
+    Signature Verified Successfully
+
diff --git a/test/webcrypto/aes.js b/test/webcrypto/aes.js
new file mode 100644
index 0000000..1b8219f
--- /dev/null
+++ b/test/webcrypto/aes.js
@@ -0,0 +1,123 @@
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+    function validate(t, r, i) {
+        if (r.status == "fulfilled" && !t[i].exception) {
+            return r.value === "SUCCESS";
+        }
+
+        if (r.status == "rejected" && t[i].exception) {
+            if (process.argv[2] === '--match-exception-text') {
+                /* is not compatible with node.js format */
+                return r.reason.toString().startsWith(t[i].exception);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    for (let k = 0; k < tlist.length; k++) {
+        let ts = tlist[k];
+        let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+        let r = results.map((r, i) => validate(ts.tests, r, i));
+
+        console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+        r.forEach((v, i) => {
+            if (!v) {
+                console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n    with reason: ${results[i].reason}`);
+            }
+        })
+    }
+}
+
+function p(args, default_opts) {
+    let params = Object.assign({}, default_opts, args);
+
+    params.key = Buffer.from(params.key, "hex");
+    params.data = Buffer.from(params.data, "hex");
+    params.iv = Buffer.from(params.iv, "hex");
+    params.counter = Buffer.from(params.counter, "hex");
+
+    switch (params.name) {
+    case "AES-GCM":
+        if (params.additionalData) {
+            params.additionalData = Buffer.from(params.additionalData, "hex");
+        }
+
+        break;
+    }
+
+    return params;
+}
+
+async function test(params) {
+    let dkey = await crypto.subtle.importKey("raw", params.key,
+                                       {name: params.name},
+                                       false, ["decrypt"]);
+
+    let ekey = await crypto.subtle.importKey("raw", params.key,
+                                       {name: params.name},
+                                       false, ["encrypt"]);
+
+    let enc = await crypto.subtle.encrypt(params, ekey, params.data);
+    let plaintext = await crypto.subtle.decrypt(params, dkey, enc);
+    plaintext = Buffer.from(plaintext);
+
+    if (params.data.compare(plaintext) != 0) {
+        throw Error(`${params.name} encoding/decoding failed length ${data.length}`);
+    }
+
+    return 'SUCCESS';
+}
+
+let aes_tsuite = {
+    name: "AES encoding/decoding",
+    opts: {
+        iv: "44556677445566774455667744556677",
+        key: "00112233001122330011223300112233",
+        counter: "44556677445566774455667744556677",
+        length: 64
+    },
+
+    tests: [
+        { name: "AES-gcm", data: "aa" },
+        { name: "aes-gcm", data: "aabbcc" },
+        { name: "AES-GCM", data: "aabbcc", additionalData: "deafbeef"},
+        { name: "AES-GCM", data: "aabbccdd".repeat(4) },
+        { name: "AES-GCM", data: "aa", iv: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
+        { name: "AES-GCM", data: "aabbcc", tagLength: 96 },
+        { name: "AES-GCM", data: "aabbcc", tagLength: 112 },
+        { name: "AES-GCM", data: "aabbcc", tagLength: 113, exception: "TypeError: AES-GCM Invalid tagLength" },
+        { name: "AES-GCM", data: "aabbccdd".repeat(4096) },
+
+        { name: "AES-CTR", data: "aa" },
+        { name: "AES-CTR", data: "aabbcc" },
+        { name: "AES-CTR", data: "aabbccdd".repeat(4) },
+        { name: "AES-CTR", data: "aabbccdd".repeat(4096) },
+        { name: "AES-CTR", data: "aa", counter: "ffffffffffffffffffffffffffffffff" },
+        { name: "AES-CTR", data: "aa", counter: "ffffffff",
+          exception: "TypeError: AES-CTR algorithm.counter must be 16 bytes long" },
+        { name: "AES-CTR", data: "aabbcc", counter: "ffffffffffffffffffffffffffffffff" },
+        { name: "AES-CTR", data: "aabbccdd".repeat(5), counter: "ffffffffffffffffffffffffffffffff" },
+        { name: "AES-CTR", data: "aabbccdd".repeat(4096), counter: "fffffffffffffffffffffffffffffff0" },
+        { name: "AES-CTR", data: "aabbccdd".repeat(4096), counter: "ffffffffffffffffffffffffffffffff" },
+        { name: "AES-CTR", data: "aabbccdd".repeat(4096), counter: "ffffffffffffffffffffffffffffffff", length: 7,
+          exception: "TypeError: AES-CTR repeated counter" },
+        { name: "AES-CTR", data: "aabbccdd".repeat(4096), counter: "ffffffffffffffffffffffffffffffff", length: 11 },
+        { name: "AES-CTR", data: "aabbccdd".repeat(4096), length: 20 },
+        { name: "AES-CTR", data: "aabbccdd".repeat(4096), length: 24 },
+        { name: "AES-CTR", data: "aabbccdd", length: 129,
+          exception: "TypeError: AES-CTR algorithm.length must be between 1 and 128" },
+
+        { name: "AES-CBC", data: "aa" },
+        { name: "AES-CBC", data: "aabbccdd".repeat(4) },
+        { name: "AES-CBC", data: "aabbccdd".repeat(4096) },
+        { name: "AES-CBC", data: "aabbccdd".repeat(5), iv: "ffffffffffffffffffffffffffffffff" },
+]};
+
+run([aes_tsuite], test, p);
diff --git a/test/webcrypto/aes_decoding.js b/test/webcrypto/aes_decoding.js
new file mode 100644
index 0000000..3b4dfe7
--- /dev/null
+++ b/test/webcrypto/aes_decoding.js
@@ -0,0 +1,116 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+    function validate(t, r, i) {
+        if (r.status == "fulfilled" && !t[i].exception) {
+            return r.value === "SUCCESS";
+        }
+
+        if (r.status == "rejected" && t[i].exception) {
+            if (process.argv[2] === '--match-exception-text') {
+                /* is not compatible with node.js format */
+                return r.reason.toString().startsWith(t[i].exception);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    for (let k = 0; k < tlist.length; k++) {
+        let ts = tlist[k];
+        let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+        let r = results.map((r, i) => validate(ts.tests, r, i));
+
+        console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+        r.forEach((v, i) => {
+            if (!v) {
+                console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n    with reason: ${results[i].reason}`);
+            }
+        })
+    }
+}
+
+function base64decode(b64) {
+    const joined = b64.toString().split('\n').join('');
+    return Buffer.from(joined, 'base64');
+}
+
+function p(args, default_opts) {
+    let params = Object.assign({}, default_opts, args);
+
+    params.key = Buffer.from(params.key, "hex");
+    params.iv = Buffer.from(params.iv, "hex");
+    params.counter = Buffer.from(params.counter, "hex");
+
+    switch (params.name) {
+    case "AES-GCM":
+        if (params.additionalData) {
+            params.additionalData = Buffer.from(params.additionalData, "hex");
+        }
+
+        break;
+    }
+
+    return params;
+}
+
+async function test(params) {
+    let enc = base64decode(fs.readFileSync(`test/webcrypto/${params.file}`));
+    let key = await crypto.subtle.importKey("raw", params.key,
+                                            {name: params.name},
+                                            false, ["decrypt"]);
+
+    let plaintext = await crypto.subtle.decrypt(params, key, enc);
+    plaintext = new TextDecoder().decode(plaintext);
+
+    if (params.expected != plaintext) {
+        throw Error(`${params.name} decoding failed expected: "${params.expected}" vs "${plaintext}"`);
+    }
+
+    return 'SUCCESS';
+}
+
+let aes_tsuite = {
+    name: "AES decoding",
+    opts: {
+        key: "00112233001122330011223300112233",
+        iv: "44556677445566774455667744556677",
+        counter: "44556677445566774455667744556677",
+        length: 64
+    },
+
+    tests: [
+        { name: "AES-GCM", file: "text.base64.aes-gcm128.enc",
+          expected: "AES-GCM-SECRET-TEXT" },
+        { name: "AES-GCM", file: "text.base64.aes-gcm128-96.enc",
+          exception: "Error: EVP_DecryptFinal_ex() failed" },
+        { name: "AES-GCM", file: "text.base64.aes-gcm128-96.enc", tagLength: 96,
+          expected: "AES-GCM-96-TAG-LENGTH-SECRET-TEXT" },
+        { name: "AES-GCM", file: "text.base64.aes-gcm128-extra.enc", additionalData: "deadbeef",
+          expected: "AES-GCM-ADDITIONAL-DATA-SECRET-TEXT" },
+        { name: "AES-GCM", file: "text.base64.aes-gcm256.enc",
+          key: "0011223300112233001122330011223300112233001122330011223300112233",
+          expected: "AES-GCM-256-SECRET-TEXT" },
+        { name: "AES-GCM", file: "text.base64.aes-gcm256.enc",
+          key: "00112233001122330011223300112233001122330011223300112233001122",
+          exception: "TypeError: AES-GCM Invalid key length" },
+        { name: "AES-CTR", file: "text.base64.aes-ctr128.enc",
+          expected: "AES-CTR-SECRET-TEXT" },
+        { name: "AES-CTR", file: "text.base64.aes-ctr256.enc",
+          key: "0011223300112233001122330011223300112233001122330011223300112233",
+          expected: "AES-CTR-256-SECRET-TEXT" },
+        { name: "AES-CBC", file: "text.base64.aes-cbc128.enc",
+          expected: "AES-CBC-SECRET-TEXT" },
+        { name: "AES-CBC", file: "text.base64.aes-cbc256.enc",
+          key: "0011223300112233001122330011223300112233001122330011223300112233",
+          expected: "AES-CBC-256-SECRET-TEXT" },
+]};
+
+run([aes_tsuite], test, p);
diff --git a/test/webcrypto/aes_gcm_enc.js b/test/webcrypto/aes_gcm_enc.js
new file mode 100644
index 0000000..f286d83
--- /dev/null
+++ b/test/webcrypto/aes_gcm_enc.js
@@ -0,0 +1,51 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+function parse_options(argv) {
+    let opts = JSON.parse(argv[2] ? argv[2] : "{}");
+
+    if (!opts.key) {
+        opts.key = Buffer.from("00112233001122330011223300112233", "hex");
+
+    } else {
+        opts.key = Buffer.from(opts.key, "hex");
+    }
+
+    if (!opts.iv) {
+        opts.iv = Buffer.from("44556677445566774455667744556677", "hex");
+
+    } else {
+        opts.iv = Buffer.from(opts.iv, "hex");
+    }
+
+    if (opts.additionalData) {
+        opts.additionalData = Buffer.from(opts.additionalData, "hex");
+    }
+
+    if (!opts['in']) {
+        throw Error("opts.in is expected");
+    }
+
+    return opts;
+}
+
+(async function main() {
+    let opts = parse_options(process.argv);
+    let stdin = fs.readFileSync(`test/webcrypto/${opts['in']}`);
+    let key = await crypto.subtle.importKey("raw", opts.key,
+                                            {name: "AES-GCM"},
+                                            false, ["encrypt"]);
+
+    let params = Object.assign(opts);
+    params.name = "AES-GCM";
+
+    let enc = await crypto.subtle.encrypt(params, key, stdin);
+
+    console.log(Buffer.from(enc).toString("base64"));
+})()
+.catch(e => {
+    console.log(`exception:${e.stack}`);
+})
diff --git a/test/webcrypto/derive.js b/test/webcrypto/derive.js
new file mode 100644
index 0000000..e2f6917
--- /dev/null
+++ b/test/webcrypto/derive.js
@@ -0,0 +1,149 @@
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+    function validate(t, r, i) {
+        if (r.status == "fulfilled" && !t[i].exception) {
+            return r.value === "SUCCESS";
+        }
+
+        if (r.status == "rejected" && t[i].exception) {
+            if (process.argv[2] === '--match-exception-text') {
+                /* is not compatible with node.js format */
+                return r.reason.toString().startsWith(t[i].exception);
+            }
+
+            return true;
+        }
+
+        if (r.status == "rejected" && t[i].optional) {
+            return r.reason.toString().startsWith("InternalError: not implemented");
+        }
+
+        return false;
+    }
+
+    for (let k = 0; k < tlist.length; k++) {
+        let ts = tlist[k];
+        let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+        let r = results.map((r, i) => validate(ts.tests, r, i));
+
+        console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+        r.forEach((v, i) => {
+            if (!v) {
+                console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n    with reason: ${results[i].reason}`);
+            }
+        })
+    }
+}
+
+function merge(to, from) {
+    let r = Object.assign({}, to);
+    Object.keys(from).forEach(v => {
+        if (typeof r[v] == 'object' && typeof from[v] == 'object') {
+            r[v] = merge(r[v], from[v]);
+
+        } else if (typeof from[v] == 'object') {
+            r[v] = Object.assign({}, from[v]);
+
+        } else {
+            r[v] = from[v];
+        }
+    })
+
+    return r;
+};
+
+function p(args, default_opts) {
+    let params = Object.assign({}, default_opts);
+    params = merge(params, args);
+
+    params.algorithm.salt = Buffer.from(params.algorithm.salt, "hex");
+    params.algorithm.info = Buffer.from(params.algorithm.info, "hex");
+    params.derivedAlgorithm.iv = Buffer.from(params.derivedAlgorithm.iv, "hex");
+
+    return params;
+}
+
+async function test(params) {
+    let r;
+    let encoder = new TextEncoder();
+    let keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(params.pass),
+                                                    params.algorithm.name,
+                                                    false, [ "deriveBits", "deriveKey" ]);
+    if (params.derive === "key") {
+        let key = await crypto.subtle.deriveKey(params.algorithm, keyMaterial,
+                                                params.derivedAlgorithm,
+                                                true, [ "encrypt", "decrypt" ]);
+
+        r = await crypto.subtle.encrypt(params.derivedAlgorithm, key,
+                                        encoder.encode(params.text));
+    } else {
+
+        r = await crypto.subtle.deriveBits(params.algorithm, keyMaterial, params.length);
+    }
+
+    r = Buffer.from(r).toString("hex");
+
+    if (params.expected != r) {
+        throw Error(`${params.algorithm.name} failed expected: "${params.expected}" vs "${r}"`);
+    }
+
+    return "SUCCESS";
+}
+
+let derive_tsuite = {
+    name: "derive",
+    opts: {
+        text: "secReT",
+        pass: "passW0rd",
+        derive: "key",
+        optional: false,
+        length: 256,
+        algorithm: {
+            name: "PBKDF2",
+            salt: "00112233001122330011223300112233",
+            hash: "SHA-256",
+            info: "deadbeef",
+            iterations: 100000
+        },
+        derivedAlgorithm: {
+          name: "AES-GCM",
+          length: 256,
+          iv: "55667788556677885566778855667788"
+        }
+    },
+
+    tests: [
+        { expected: "e7b55c9f9fda69b87648585f76c58109174aaa400cfa" },
+        { pass: "pass2", expected: "e87d1787f2807ea0e1f7e1cb265b23004c575cf2ad7e" },
+        { algorithm: { iterations: 10000 }, expected: "5add0059931ed1db1ca24c26dbe4de5719c43ed18a54" },
+        { algorithm: { hash: "SHA-512" }, expected: "544d64e5e246fdd2ba290ea932b2d80ef411c76139f4" },
+        { algorithm: { salt: "aabbccddaabbccddaabbccddaabbccdd" }, expected: "5c1304bedf840b1f6f7d1aa804fe870a8f949d762c32" },
+        { algorithm: { salt: "aabbccddaabbccddaabbccddaabb" },
+          exception: "TypeError: PBKDF2 algorithm.salt must be at least 16 bytes long" },
+        { derivedAlgorithm: { length: 128 }, expected: "9e2d7bcc1f21f30ec3c32af9129b64507d086d129f2a" },
+        { derivedAlgorithm: { length: 32 },
+          exception: "TypeError: deriveKey \"AES-GCM\" length must be 128 or 256" },
+        { derivedAlgorithm: { name: "AES-CBC" }, expected: "3ad6523692d44b6a7a90be7c2721786f" },
+
+        { derive: "bits", expected: "6458ed6e16b998d4e646422171087be8a1ee34bed463dfcb3dcd30842b1228fe" },
+        { derive: "bits", pass: "pass2", expected: "ef8f75073fcadfd504d26610c743873e297ad90340c23ddc0e5f6bdb83cbabb2" },
+        { derive: "bits", algorithm: { salt: "aabbccddaabbccddaabbccddaabbccdd" },
+          expected: "22ceb295aa25b59c6bc5b383a089bd6999006c03f273ce3614a4fa0d90bd29ae" },
+        { derive: "bits", algorithm: { hash: "SHA-1" },
+          expected: "a2fc83498f7d07b4c8180c7ebfec2af0f3a7d6cb08bf8593d41d3c5c1e1c4d67" },
+        { derive: "bits", algorithm: { hash: "SHA-1" }, length: 128,
+          expected: "a2fc83498f7d07b4c8180c7ebfec2af0" },
+        { derive: "bits", algorithm: { hash: "SHA-1" }, length: 64,
+          expected: "a2fc83498f7d07b4" },
+
+        { algorithm: { name: "HKDF" }, optional: true,
+          expected: "18ea069ee3317d2db02e02f4a228f50dc80d9a2396e6" },
+        { derive: "bits", algorithm: { name: "HKDF" }, optional: true,
+          expected: "e089c7491711306c69e077aa19fae6bfd2d4a6d240b0d37317d50472d7291a3e" },
+]};
+
+run([derive_tsuite], test, p);
diff --git a/test/webcrypto/digest.js b/test/webcrypto/digest.js
new file mode 100644
index 0000000..4eed191
--- /dev/null
+++ b/test/webcrypto/digest.js
@@ -0,0 +1,88 @@
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+    function validate(t, r, i) {
+        if (r.status == "fulfilled" && !t[i].exception) {
+            return r.value === "SUCCESS";
+        }
+
+        if (r.status == "rejected" && t[i].exception) {
+            if (process.argv[2] === '--match-exception-text') {
+                /* is not compatible with node.js format */
+                return r.reason.toString().startsWith(t[i].exception);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    for (let k = 0; k < tlist.length; k++) {
+        let ts = tlist[k];
+        let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+        let r = results.map((r, i) => validate(ts.tests, r, i));
+
+        console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+        r.forEach((v, i) => {
+            if (!v) {
+                console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n    with reason: ${results[i].reason}`);
+            }
+        })
+    }
+}
+
+function p(args) {
+    let params = Object.assign({}, args);
+    params.data = Buffer.from(params.data, "hex");
+    return params;
+}
+
+async function test(params) {
+    let digest = await crypto.subtle.digest(params.name, params.data);
+    digest = Buffer.from(digest).toString("hex");
+
+    if (params.expected != digest) {
+        throw Error(`${params.name} digest failed expected: "${params.expected}" vs "${digest}"`);
+    }
+
+    return 'SUCCESS';
+}
+
+let digest_tsuite = {
+    name: "SHA digest",
+    opts: { },
+
+    tests: [
+        { name: "XXX", data: "",
+          exception: "TypeError: unknown hash name: \"XXX\"" },
+        { name: "SHA-256", data: "",
+          expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" },
+        { name: "SHA-256", data: "aabbccdd",
+          expected: "8d70d691c822d55638b6e7fd54cd94170c87d19eb1f628b757506ede5688d297" },
+        { name: "SHA-256", data: "aabbccdd".repeat(4096),
+          expected: "25077ac2e5ba760f015ef34b93bc2b4682b6b48a94d65e21aaf2c8a3a62f6368" },
+        { name: "SHA-384", data: "",
+          expected: "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b" },
+        { name: "SHA-384", data: "aabbccdd",
+          expected: "f9616ef3495efbae2f6af1a754620f3034487e9c60f3a9ef8138b5ed55cdd8d18ad9565653a5d68f678bd34cfa6f4490" },
+        { name: "SHA-384", data: "aabbccdd".repeat(4096),
+          expected: "50502d6e89bc34ecc826e0d56ccba0e010eff7b2b532e3bd627f4c828f6c741bf518fc834559360ccf7770f1b4d655d8" },
+        { name: "SHA-512", data: "",
+          expected: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" },
+        { name: "SHA-512", data: "aabbccdd",
+          expected: "48e218b30d4ea16305096fe35e84002a0d262eb3853131309423492228980c60238f9eed238285036f22e37c4662e40c80a461000a7aa9a03fb3cb6e4223e83b" },
+        { name: "SHA-512", data: "aabbccdd".repeat(4096),
+          expected: "9fcd0bd297646e207a2d655feb4ed4473e07ff24560a1e180a5eb2a67824f68affd9c7b5a8f747b9c39201f5f86a0085bb636c6fc34c216d9c10b4d728be096a" },
+        { name: "SHA-1", data: "",
+          expected: "da39a3ee5e6b4b0d3255bfef95601890afd80709" },
+        { name: "SHA-1", data: "aabbccdd",
+          expected: "a7b7e9592daa0896db0517bf8ad53e56b1246923" },
+        { name: "SHA-1", data: "aabbccdd".repeat(4096),
+          expected: "cdea58919606ea9ae078f7595b192b84446f2189" },
+]};
+
+run([digest_tsuite], test, p);
diff --git a/test/webcrypto/ec.pkcs8 b/test/webcrypto/ec.pkcs8
new file mode 100644
index 0000000..9829794
--- /dev/null
+++ b/test/webcrypto/ec.pkcs8
@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE2sW0/4a3QXaSTJ0
+JKbSUbieKTD1UFtr7i/2CuetP6ChRANCAARxRSxlEa5VhF4aJNCX0ypHuKvp1kiD
+D7ykz4XSmElZ3ODc5/+7jc9AAN1OH4aX1cUg+FOUHIhshKDOK94wu24y
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/ec.spki b/test/webcrypto/ec.spki
new file mode 100644
index 0000000..c9f6058
--- /dev/null
+++ b/test/webcrypto/ec.spki
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcUUsZRGuVYReGiTQl9MqR7ir6dZI
+gw+8pM+F0phJWdzg3Of/u43PQADdTh+Gl9XFIPhTlByIbISgziveMLtuMg==
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/ec2.pkcs8 b/test/webcrypto/ec2.pkcs8
new file mode 100644
index 0000000..b835530
--- /dev/null
+++ b/test/webcrypto/ec2.pkcs8
@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg56d4aW5UtAvpKMfr
+E0M8OeCN/6ES0Q1Y+DeymtgvZ2ihRANCAATj283yk3EezOOEF6FRRwfeYNyJ65bj
+1jwJ8w9N0zMIedRGg0OJHnNc/uoyu6s1M/BtG/vZJ8IJNHUayiVbqxVL
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/ec2.spki b/test/webcrypto/ec2.spki
new file mode 100644
index 0000000..afde080
--- /dev/null
+++ b/test/webcrypto/ec2.spki
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE49vN8pNxHszjhBehUUcH3mDcieuW
+49Y8CfMPTdMzCHnURoNDiR5zXP7qMrurNTPwbRv72SfCCTR1GsolW6sVSw==
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/rsa.js b/test/webcrypto/rsa.js
new file mode 100644
index 0000000..36744d8
--- /dev/null
+++ b/test/webcrypto/rsa.js
@@ -0,0 +1,106 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+    function validate(t, r, i) {
+        if (r.status == "fulfilled" && !t[i].exception) {
+            return r.value === "SUCCESS";
+        }
+
+        if (r.status == "rejected" && t[i].exception) {
+            if (process.argv[2] === '--match-exception-text') {
+                /* is not compatible with node.js format */
+                return r.reason.toString().startsWith(t[i].exception);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    for (let k = 0; k < tlist.length; k++) {
+        let ts = tlist[k];
+        let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+        let r = results.map((r, i) => validate(ts.tests, r, i));
+
+        console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+        r.forEach((v, i) => {
+            if (!v) {
+                console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n    with reason: ${results[i].reason}`);
+            }
+        })
+    }
+}
+
+function pem_to_der(pem, type) {
+    const pemJoined = pem.toString().split('\n').join('');
+    const pemHeader = `-----BEGIN ${type} KEY-----`;
+    const pemFooter = `-----END ${type} KEY-----`;
+    const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+    return Buffer.from(pemContents, 'base64');
+}
+
+function p(args, default_opts) {
+    let params = Object.assign({}, default_opts, args);
+
+    params.data = Buffer.from(params.data, "hex");
+
+    return params;
+}
+
+async function test(params) {
+    let spki = await crypto.subtle.importKey("spki",
+                            pem_to_der(fs.readFileSync(`test/webcrypto/${params.spki}`), "PUBLIC"),
+                            {name:"RSA-OAEP", hash:params.spki_hash},
+                            false, ["encrypt"]);
+
+    let pkcs8 = await crypto.subtle.importKey("pkcs8",
+                            pem_to_der(fs.readFileSync(`test/webcrypto/${params.pkcs8}`), "PRIVATE"),
+                            {name:"RSA-OAEP", hash:params.pkcs8_hash},
+                            false, ["decrypt"]);
+
+    let enc = await crypto.subtle.encrypt({name: "RSA-OAEP"}, spki, params.data);
+
+    let plaintext = await crypto.subtle.decrypt({name: "RSA-OAEP"}, pkcs8, enc);
+
+    plaintext = Buffer.from(plaintext);
+
+    if (params.data.compare(plaintext) != 0) {
+        throw Error(`RSA-OAEP encoding/decoding failed expected: "${params.data}" vs "${plaintext}"`);
+    }
+
+    return 'SUCCESS';
+};
+
+let rsa_tsuite = {
+    name: "RSA-OAEP encoding/decoding",
+    opts: {
+        spki: "rsa.spki",
+        spki_hash: "SHA-256",
+        pkcs8: "rsa.pkcs8",
+        pkcs8_hash: "SHA-256",
+    },
+
+    tests: [
+        { data: "aabbcc" },
+        { data: "aabbccdd".repeat(4) },
+        { data: "aabbccdd".repeat(7) },
+        { data: "aabbcc", spki_hash: "SHA-1", pkcs8_hash: "SHA-1" },
+        { data: "aabbccdd".repeat(4), spki_hash: "SHA-1", pkcs8_hash: "SHA-1" },
+        { data: "aabbccdd".repeat(7), spki_hash: "SHA-1", pkcs8_hash: "SHA-1" },
+        { data: "aabbcc", spki_hash: "SHA-384", pkcs8_hash: "SHA-384" },
+        { data: "aabbccdd".repeat(4), spki_hash: "SHA-384", pkcs8_hash: "SHA-384" },
+        { data: "aabbccdd".repeat(7), spki_hash: "SHA-384", pkcs8_hash: "SHA-384" },
+
+        { data: "aabbcc", spki_hash: "SHA-256", pkcs8_hash: "SHA-384", exception: "Error: EVP_PKEY_decrypt() failed" },
+        { data: "aabbcc", spki_hash: "XXX", exception: "TypeError: unknown hash name: \"XXX\"" },
+        { data: "aabbcc", spki: "rsa.spki.broken", exception: "Error: d2i_PUBKEY() failed" },
+        { data: "aabbcc", spki: "rsa2.spki", exception: "Error: EVP_PKEY_decrypt() failed" },
+]};
+
+run([rsa_tsuite], test, p);
diff --git a/test/webcrypto/rsa.pkcs8 b/test/webcrypto/rsa.pkcs8
new file mode 100644
index 0000000..0065b98
--- /dev/null
+++ b/test/webcrypto/rsa.pkcs8
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMlJsaCQvFQDOYcm
+GWvl1AWYNTdcsBTD1KVrBdZGkhnnffD911ID84F/NMKcs3eanRrgC6p39pTHOzvD
+6xgbTuWK70JSPejV9I1KOW3OcM9ttKG9wFAnkJ038flBajOKQsI6A0qNj5aYSXVo
+BWMphgWgQiYJxDUC/R9Tf/P8jYjfAgMBAAECgYEAj06DQyCopFujYoASi0oWmGEU
+SjUYO8BsrdSzVCnsLLsuZBwlZ4Peouyw4Hl2IIoYniCyzYwZJzVtC5Dh2MjgcrJT
+G5nX3FfheuabGl4in0583C51ZYWlVpDvBWw8kJTfXjiKH4z6ZA9dWdT5Y3aH/kOf
++znUc7eTvuzISs61x/kCQQD0BJvbLDlvx3u6esW47LLgQNw9ufMSlu5UYBJ4c+qQ
+5HAeyp4Zt/AaWENhJitjQcLBSxIFIVw7dIN67RnTNK8VAkEA0yvzzgHo/PGYSlVj
++M3965AwQF2wTXz82MZHv6EfcCHKuBfCSecr+igqLHhzfynAQjjf39VrXuPuRL23
+REF1IwJBAKVFydo0peJTljXDmc+aYb0JsSINo9jfaSS0vU3gFOt2DYqNaW+56WGu
+jlRqadCcZbBNjDL1WWbbj4HevTMT59ECQEWaKgzPolykwN5XUNE0DCp1ZwIAH1kb
+Bjfo+sMVt0f9S1TsN9SmBl+4l1X7CY5zU3RATMH5FR+8ns83fM1ZieMCQQDZEQ+d
+FAhouzJrnCXAXDTCHA9oBtNmnaN+C6G2DmCi79iu7sLHP9vzdgU+CgjrG4YTU5ex
+aRFNOhLwW4hYKs0F
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/rsa.pkcs8.broken b/test/webcrypto/rsa.pkcs8.broken
new file mode 100644
index 0000000..6341afe
--- /dev/null
+++ b/test/webcrypto/rsa.pkcs8.broken
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMlJsaCQvFQDOYcm
+GWvl1AWYNTdcsBTD1KVrBdZGkhnnffD911ID84F/NMKcs3eanRrgC6p39pTHOzvD
+6xgbTuWK70JSPejV9I1KOW3OcM9ttKG9wFAnkJ038flBajOKQsI6A0qNj5aYSXVo
+BWMphgWgQiYJxDUC/R9Tf/P8jYjfAgMBAAECgYEAj06DQyCopFujYoASi0oWmGEU
+SjUYO8BsrdSzVCnsLLsuZBwlZ4Peouyw4Hl2IIoYniCyzYwZJzVtC5Dh2MjgcrJT
+G5nX3FfheuabGl4in0583C51ZYWlVpDvBWw8kJTfXjiKH4z6ZA9dWdT5Y3aH/kOf
++znUc7eTvuzISs61x/kCQQD0BJvbLDlvx3u6esW47LLgQNw9ufMSlu5UYBJ4c+qQ
+5HAeyp4Zt/AaWENhJitjQcLBSxIFIVw7dIN67RnTNK8VAkEA0yvzzgHo/PGYSlVj
++M3965AwQF2wTXz82MZHv6EfcCHKuBfCSecr+igqLHhzfynAQjjf39VrXuPuRL23
+REF1IwJBAKVFydo0peJTljXDmc+aYb0JsSINo9jfaSS0vU3gFOt2DYqNaW+56WGu
+jlRqadCcZbBNjDL1WWbbj4HevTMT59ECQEWaKgzPolykwN5XUNE0DCp1ZwIAH1kb
+Bjfo+sMVt0f9S1TsN9SmBl+4l1X7CY5zU3RATMH5FR+8ns83fM1ZieMCQQDZEQ+d
+FAhouzJrnCXAXDTCHA9oBtNmnaN+C6G2DmCi79iu7sLHP9vzdgU+CgjrG4YTU5ex
+aRFNOhLwW4hYKs
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/rsa.spki b/test/webcrypto/rsa.spki
new file mode 100644
index 0000000..6ff75cf
--- /dev/null
+++ b/test/webcrypto/rsa.spki
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJSbGgkLxUAzmHJhlr5dQFmDU3
+XLAUw9SlawXWRpIZ533w/ddSA/OBfzTCnLN3mp0a4Auqd/aUxzs7w+sYG07liu9C
+Uj3o1fSNSjltznDPbbShvcBQJ5CdN/H5QWozikLCOgNKjY+WmEl1aAVjKYYFoEIm
+CcQ1Av0fU3/z/I2I3wIDAQAB
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/rsa.spki.broken b/test/webcrypto/rsa.spki.broken
new file mode 100644
index 0000000..d3f35d8
--- /dev/null
+++ b/test/webcrypto/rsa.spki.broken
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJSbGgkLxUAzmHJhlr5dQFmDU3
+XLAUw9SlawXWRpIZ533w/ddSA/OBfzTCnLN3mp0a4Auqd/aUxzs7w+sYG07liu9C
+Uj3o1fSNSjltznDPbbShvcBQJ5CdN/H5QWozikLCOgNKjY+WmEl1aAVjKYYFoEIm
+CcQ1Av0fU3/z/I2I3IDAQAB
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/rsa2.pkcs8 b/test/webcrypto/rsa2.pkcs8
new file mode 100644
index 0000000..2520b2d
--- /dev/null
+++ b/test/webcrypto/rsa2.pkcs8
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANUukQQW3CVlMsS6
+VaRnBhExg2gTCB6tvevFupCpq6sEuX5X6+4/QUTt4/wyWeMn5amaNAjGmDtsmeKX
+85MGHPuKceLhjToJT5JGFFDwVB6pNyqBuRzEHYsvQz1TlqcSdo7U/YaQtqkEZsA/
+ZAjBSR4lP9ggOBpEq+bBs+2GaZZFAgMBAAECgYB8HMdK1TBICTncdQtlUqGiouv5
+TJM+oTJgMNbkYBPU1kRUPUXbiDIsuj8wVfQlHtZDvsYqkcyRVDHnTUX+w+FctIox
+OU+3bKEZ/winaO3znvPVdy59/evpvQ0rnAGsxYBmyfZTqQgCxX+nMqAsLIplzORM
+5zAOawEQfRyHGETHQQJBAPe6YoMHMM/ZVpQ0xwQasbQahcL2GCD4Hwv3neGEKZVz
+Aos89/qkA+78hg6OxChxSJFxK0p35lu5TqF0QFX3MNUCQQDcTOBFZaGMHnnL+2uc
+tTmjKMjt47Es5G/NLg5z4cLBeeaz2St8ISbtvuVtl3K9cjeNy4J30zvF5puZrTvw
+/wexAkEAsfzRcNEGyh+erCdrYlCHox53QsesOGvtapzDa9eYRQ94IXBxvzx+swPu
+kaET4Pbbq9wCvaN9+CMhErHC08Eh7QJAAQWaRLgj97JsfjW8Wg29JrSZugDEYaDt
+o9YC2ybA8ITQPSVUvk6pD5FDHy8EqTxOZan8APJJ5LEdJ6lWDdghAQJBANKmYYmk
+OcQtU29dwuzPkwZWFdl6mhwZdcOrFcjq2pSfxKjBfygXXykscF8pHeQsjPrKK3u6
+HNED24fqNlbYHi8=
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/rsa2.spki b/test/webcrypto/rsa2.spki
new file mode 100644
index 0000000..fde2938
--- /dev/null
+++ b/test/webcrypto/rsa2.spki
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVLpEEFtwlZTLEulWkZwYRMYNo
+Ewgerb3rxbqQqaurBLl+V+vuP0FE7eP8MlnjJ+WpmjQIxpg7bJnil/OTBhz7inHi
+4Y06CU+SRhRQ8FQeqTcqgbkcxB2LL0M9U5anEnaO1P2GkLapBGbAP2QIwUkeJT/Y
+IDgaRKvmwbPthmmWRQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/rsa_decoding.js b/test/webcrypto/rsa_decoding.js
new file mode 100644
index 0000000..c5e0f65
--- /dev/null
+++ b/test/webcrypto/rsa_decoding.js
@@ -0,0 +1,81 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+    function validate(t, r, i) {
+        if (r.status == "fulfilled" && !t[i].exception) {
+            return r.value === "SUCCESS";
+        }
+
+        if (r.status == "rejected" && t[i].exception) {
+            if (process.argv[2] === '--match-exception-text') {
+                /* is not compatible with node.js format */
+                return r.reason.toString().startsWith(t[i].exception);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    for (let k = 0; k < tlist.length; k++) {
+        let ts = tlist[k];
+        let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+        let r = results.map((r, i) => validate(ts.tests, r, i));
+
+        console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+        r.forEach((v, i) => {
+            if (!v) {
+                console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n    with reason: ${results[i].reason}`);
+            }
+        })
+    }
+}
+
+function pem_to_der(pem) {
+    const pemJoined = pem.toString().split('\n').join('');
+    const pemHeader = '-----BEGIN PRIVATE KEY-----';
+    const pemFooter = '-----END PRIVATE KEY-----';
+    const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+    return Buffer.from(pemContents, 'base64');
+}
+
+function base64decode(b64) {
+    const joined = b64.toString().split('\n').join('');
+    return Buffer.from(joined, 'base64');
+}
+
+async function test(params) {
+    let pem = fs.readFileSync(`test/webcrypto/${params.pem}`);
+    let enc = base64decode(fs.readFileSync(`test/webcrypto/${params.src}`));
+
+    let key = await crypto.subtle.importKey("pkcs8", pem_to_der(pem),
+                                            {name:"RSA-OAEP", hash:"SHA-1"},
+                                            false, ["decrypt"]);
+
+    let plaintext = await crypto.subtle.decrypt({name: "RSA-OAEP"}, key, enc);
+    plaintext = new TextDecoder().decode(plaintext);
+
+    if (params.expected != plaintext) {
+        throw Error(`RSA-OAEP decoding failed expected: "${params.expected}" vs "${plaintext}"`);
+    }
+
+    return "SUCCESS";
+}
+
+let rsa_tsuite = {
+    name: "RSA-OAEP decoding",
+    opts: { },
+
+    tests: [
+        { pem: "rsa.pkcs8", src: "text.base64.rsa-oaep.enc", expected: "WAKAWAKA" },
+        { pem: "ec.pkcs8", src: "text.base64.rsa-oaep.enc", exception: "Error: RSA key is not found" },
+        { pem: "rsa.pkcs8.broken", src: "text.base64.rsa-oaep.enc", exception: "Error: d2i_PKCS8_PRIV_KEY_INFO_bio() failed" },
+]};
+
+run([rsa_tsuite], test, (v) => v);
diff --git a/test/webcrypto/sign.js b/test/webcrypto/sign.js
new file mode 100644
index 0000000..0473d2a
--- /dev/null
+++ b/test/webcrypto/sign.js
@@ -0,0 +1,282 @@
+const fs = require('fs');
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+    function validate(t, r, i) {
+        if (r.status == "fulfilled" && !t[i].exception) {
+            return r.value === "SUCCESS";
+        }
+
+        if (r.status == "rejected" && t[i].exception) {
+            if (process.argv[2] === '--match-exception-text') {
+                /* is not compatible with node.js format */
+                return r.reason.toString().startsWith(t[i].exception);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    for (let k = 0; k < tlist.length; k++) {
+        let ts = tlist[k];
+        let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+        let r = results.map((r, i) => validate(ts.tests, r, i));
+
+        console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+        r.forEach((v, i) => {
+            if (!v) {
+                console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n    with reason: ${results[i].reason}`);
+            }
+        })
+    }
+}
+
+function merge(to, from) {
+    let r = Object.assign({}, to);
+    Object.keys(from).forEach(v => {
+        if (typeof r[v] == 'object' && typeof from[v] == 'object') {
+            r[v] = merge(r[v], from[v]);
+
+        } else if (typeof from[v] == 'object') {
+            r[v] = Object.assign({}, from[v]);
+
+        } else {
+            r[v] = from[v];
+        }
+    })
+
+    return r;
+};
+
+function pem_to_der(pem, type) {
+    const pemJoined = pem.toString().split('\n').join('');
+    const pemHeader = `-----BEGIN ${type} KEY-----`;
+    const pemFooter = `-----END ${type} KEY-----`;
+    const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+    return Buffer.from(pemContents, 'base64');
+}
+
+function base64decode(b64) {
+    const joined = b64.toString().split('\n').join('');
+    return Buffer.from(joined, 'base64');
+}
+
+function p(args, default_opts) {
+    let key;
+    let encoder = new TextEncoder();
+    let params = merge({}, default_opts);
+    params = merge(params, args);
+
+    switch (params.sign_key.fmt) {
+    case "pkcs8":
+        let pem = fs.readFileSync(`test/webcrypto/${params.sign_key.key}`);
+        key = pem_to_der(pem, "PRIVATE");
+        break;
+    case "raw":
+        key = encoder.encode(params.sign_key.key);
+        break;
+    default:
+        throw Error("Unknown sign key format");
+    }
+
+    params.sign_key.key = key;
+
+    switch (params.verify_key.fmt) {
+    case "spki":
+        let pem = fs.readFileSync(`test/webcrypto/${params.verify_key.key}`);
+        key = pem_to_der(pem, "PUBLIC");
+        break;
+    case "raw":
+        key = encoder.encode(params.verify_key.key);
+        break;
+    default:
+        throw Error("Unknown verify key format");
+    }
+
+    params.verify_key.key = key;
+
+    return params;
+}
+
+async function test(params) {
+    let encoder = new TextEncoder();
+    let sign_key = await crypto.subtle.importKey(params.sign_key.fmt,
+                                                 params.sign_key.key,
+                                                 params.import_alg,
+                                                 false, [ "sign" ]);
+
+    let sig = await crypto.subtle.sign(params.sign_alg, sign_key,
+                                     encoder.encode(params.text));
+
+    if (params.verify) {
+        let verify_key = await crypto.subtle.importKey(params.verify_key.fmt,
+                                                       params.verify_key.key,
+                                                       params.import_alg,
+                                                       false, [ "verify" ]);
+
+        let r = await crypto.subtle.verify(params.sign_alg, verify_key, sig,
+                                           encoder.encode(params.text));
+
+        if (params.expected !== r) {
+            throw Error(`${params.sign_alg.name} failed expected: "${params.expected}" vs "${r}"`);
+        }
+
+        if (params.expected === true) {
+            let broken_sig = Buffer.concat([Buffer.from(sig)]);
+            broken_sig[8] = 255 - broken_sig[8];
+
+            r = await crypto.subtle.verify(params.sign_alg, verify_key, broken_sig,
+                                           encoder.encode(params.text));
+            if (r !== false) {
+                throw Error(`${params.sign_alg.name} BROKEN SIG failed expected: "false" vs "${r}"`);
+            }
+
+            let broken_text = encoder.encode(params.text);
+            broken_text[0] = 255 - broken_text[0];
+
+            r = await crypto.subtle.verify(params.sign_alg, verify_key, sig,
+                                           broken_text);
+            if (r !== false) {
+                throw Error(`${params.sign_alg.name} BROKEN TEXT failed expected: "false" vs "${r}"`);
+            }
+        }
+
+    } else {
+        sig = Buffer.from(sig).toString("hex");
+
+        if (params.expected !== sig) {
+            throw Error(`${params.sign_alg.name} failed expected: "${params.expected}" vs "${sig}"`);
+        }
+    }
+
+
+    return "SUCCESS";
+}
+
+let hmac_tsuite = {
+    name: "HMAC sign",
+    opts: {
+        text: "TExt-T0-SiGN",
+        sign_key: { key: "secretKEY", fmt: "raw" },
+        verify_key: { key: "secretKEY", fmt: "raw" },
+        verify: false,
+        import_alg: {
+            name: "HMAC",
+            hash: "SHA-256",
+        },
+        sign_alg: {
+            name: "HMAC",
+        },
+    },
+
+    tests: [
+        { expected: "76d4f1b22d7544c34e86380c9ab7c756311810dc31e4af3b705045d263db1212" },
+        { import_alg: { hash: "SHA-384" },
+          expected: "4bdaa7e80868a9cda35ad78ae5d88c29f1ff97680317c5bc3df1deccf2dad0cf3edce945ed90ec53fa48d887a04d4963" },
+        { import_alg: { hash: "SHA-512" },
+          expected: "9dd589ae5e75b6fb8d453c072cc05e6f5eb3d29034d3a0df2559ffe158f3f99fef98a9d1ab2fca459cceea0be3cb7aa3269d77fc9382b56a9cd0571851339938" },
+        { import_alg: { hash: "SHA-1" },
+          expected: "0540c587e7ee607fb4fd5e814438ed50f261c244" },
+        { sign_alg: { name: "ECDSA" }, exception: "TypeError: cannot sign using \"HMAC\" with \"ECDSA\" key" },
+
+        { verify: true, expected: true },
+        { verify: true, import_alg: { hash: "SHA-384" }, expected: true },
+        { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
+        { verify: true, import_alg: { hash: "SHA-1" }, expected: true },
+        { verify: true, verify_key: { key: "secretKEY2" }, expected: false },
+]};
+
+let rsassa_pkcs1_v1_5_tsuite = {
+    name: "RSASSA-PKCS1-v1_5 sign",
+    opts: {
+        text: "TExt-T0-SiGN",
+        sign_key: { key: "rsa.pkcs8", fmt: "pkcs8" },
+        verify_key: { key: "rsa.spki", fmt: "spki" },
+        import_alg: {
+            name: "RSASSA-PKCS1-v1_5",
+            hash: "SHA-256",
+        },
+        sign_alg: {
+            name: "RSASSA-PKCS1-v1_5",
+        },
+    },
+
+    tests: [
+        { expected: "b126c528abd305dc2b7234de44ffa2190bd55f57087f75620196e8bdb05ba205e52ceca03e4799f30a6d61a6610878b1038a5dd869ab8c04ffe80d49d14407b2c2fe52ca78c9c409fcf7fee26188941f5072179c2bf2de43e637b089c32cf04f14ca01e7b9c33bbbec603b2815de0180b12a3269b0453aba158642e00303890d" },
+        { import_alg: { hash: "SHA-512" },
+          expected: "174adca014132f5b9871e1bda2c23fc50f57673c6915b9170d601c626022a03d66c1b8c2a4b8efa08edee83ad27cc05c0d33c7a52a9125fa5be0f99be40483d8123570f91d53f2af51ef0f2b43987182fd114db242f146ea0d7c4ead5d4a11043f83e67d5400fc66dc2b08d7d63122fcd11b495fb4115ecf57c51994f6c516b9" },
+        { import_alg: { hash: "SHA-1" },
+          expected: "0cc6377ae31a1b09a7c0a18d12e785e9734565bdeb808b3e41d8bc03adab9ffbd8b1764830fea8f1d8f327034f24296f3aad6112cc3a380db6ef01989f8f9cb608f75b1d9558c36785b6f932ee06729b139b5f02bb886fd1d4fb0f06246064993a421e55579c490c77c27a44c7cc0ea7dd6579cc69402177712ba0f69cac967d" },
+
+        { verify: true, expected: true },
+        { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
+        { verify: true, import_alg: { hash: "SHA-1" }, expected: true },
+        { verify: true, verify_key: { key: "rsa2.spki" }, expected: false },
+]};
+
+let rsa_pss_tsuite = {
+    name: "RSA-PSS sign",
+    opts: {
+        text: "TExt-T0-SiGN",
+        sign_key: { key: "rsa.pkcs8", fmt: "pkcs8" },
+        verify_key: { key: "rsa.spki", fmt: "spki" },
+        import_alg: {
+            name: "RSA-PSS",
+            hash: "SHA-256",
+        },
+        sign_alg: {
+            name: "RSA-PSS",
+            saltLength: 0,
+        },
+    },
+
+    tests: [
+        { expected: "c126f05ea6e13b3208540bd833f5886d95fe2c89f9b3102b564c9da3bc0c00d224e6ed9be664dee61dfcc0eee790f816c5cf6a0ffc320112d818b72d57de9adbb31d239c225d42395c906bde719bf4ad21c18c679d70186d2efc044fc4995773c5085c64c6d9b7a5fc96dd28176e2cd702a9f35fe64b960f21523ec19bb44408" },
+        { import_alg: { hash: "SHA-512" }, expected: "3764287839843d25cb8ad109d0ffffd54a8f47fae02e9d2fa8a9363a7b0f98d0ede417c57c0d99a8c11cd502bbc95767a5f437b99cb30341c7af840889633e08cfdaae472bed3e68d451c67182ccd583457c6a9cf81c7e17fb391606f1bc02a83253975f153582ca1c31e9ba9b89dec4bf1d2a9b7b5024dd4dde317432ff26b1" },
+        { import_alg: { hash: "SHA-1" }, expected: "73d39d22b028b13142b257d405a4a09d0622b97ef7b74e0953274744a76fedee0f283b678cfcaa8e4c38ef84033259f84c59ae987f9d049adea4379a9b0addb9f8b53ee6b64a4e32d8165d057444a1056706da648b88c6a4613022e03be5b6b9e8948d9527a95478f871bfe88dbc67127b038520af3400b942c85e0733bcad27" },
+
+        { verify: true, expected: true },
+        { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
+        { verify: true, sign_alg: { saltLength: 32 }, expected: true },
+        { verify: true, import_alg: { hash: "SHA-512" }, sign_alg: { saltLength: 32 },
+          expected: true },
+        { verify: true, verify_key: { key: "rsa2.spki" }, expected: false },
+]};
+
+let ecdsa_tsuite = {
+    name: "ECDSA sign",
+    opts: {
+        text: "TExt-T0-SiGN",
+        sign_key: { key: "ec.pkcs8", fmt: "pkcs8" },
+        verify_key: { key: "ec.spki", fmt: "spki" },
+        import_alg: {
+            name: "ECDSA",
+            namedCurve: "P-256",
+        },
+        sign_alg: {
+            name: "ECDSA",
+            hash: "SHA-256",
+        },
+    },
+
+    tests: [
+        { verify: true, expected: true },
+        { verify: true, import_alg: { hash: "SHA-384" }, expected: true },
+        { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
+        { verify: true, import_alg: { hash: "SHA-1" }, expected: true },
+        { verify: true, verify_key: { key: "ec2.spki" }, expected: false },
+        { verify: true, verify_key: { key: "rsa.spki" }, exception: "Error: EC key is not found" },
+        { verify: true, import_alg: { namedCurve: "P-384" }, exception: "Error: name curve mismatch" },
+]};
+
+run([
+    hmac_tsuite,
+    rsassa_pkcs1_v1_5_tsuite,
+    rsa_pss_tsuite,
+    ecdsa_tsuite
+], test, p);
diff --git a/test/webcrypto/text.base64.aes-cbc128.enc b/test/webcrypto/text.base64.aes-cbc128.enc
new file mode 100644
index 0000000..8ee6ad6
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-cbc128.enc
@@ -0,0 +1 @@
+pKzTDFjJuyyWxBpM0++pVETg9638AXJwa9yXCL3Av0c=
diff --git a/test/webcrypto/text.base64.aes-cbc256.enc b/test/webcrypto/text.base64.aes-cbc256.enc
new file mode 100644
index 0000000..b4b623f
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-cbc256.enc
@@ -0,0 +1 @@
+3gnuDWCYtwPW5TMPtj1LM/uJxnKknbvPn9gURBEcegE=
diff --git a/test/webcrypto/text.base64.aes-ctr128.enc b/test/webcrypto/text.base64.aes-ctr128.enc
new file mode 100644
index 0000000..4ed5331
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-ctr128.enc
@@ -0,0 +1 @@
+UsVG2TjNHGbXaTZ3fG67MsxXPw==
diff --git a/test/webcrypto/text.base64.aes-ctr256.enc b/test/webcrypto/text.base64.aes-ctr256.enc
new file mode 100644
index 0000000..f1b4a41
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-ctr256.enc
@@ -0,0 +1 @@
+jnIqHDDRajcKkHwo4IormMSsBSDEI40=
diff --git a/test/webcrypto/text.base64.aes-gcm128-96.enc b/test/webcrypto/text.base64.aes-gcm128-96.enc
new file mode 100644
index 0000000..64724bb
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-gcm128-96.enc
@@ -0,0 +1 @@
+z4NZNzf3eauJvuFQTopsTxPSERfT4lpbnK1ILuqw81OBxqw8cpheqTfXi7U5
diff --git a/test/webcrypto/text.base64.aes-gcm128-extra.enc b/test/webcrypto/text.base64.aes-gcm128-extra.enc
new file mode 100644
index 0000000..4bb99b4
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-gcm128-extra.enc
@@ -0,0 +1 @@
+z4NZNzf3eavxzIhNW4QOTRfQewfam0gzjLpOKIKwm1+QJfR0ElIvNEPnKHx4d+OxJMpT
diff --git a/test/webcrypto/text.base64.aes-gcm128.enc b/test/webcrypto/text.base64.aes-gcm128.enc
new file mode 100644
index 0000000..8b497d8
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-gcm128.enc
@@ -0,0 +1 @@
+z4NZNzf3eavjzY9WSplsVxPEAuftE7KpHQoIoS+yI/lPxDk=
diff --git a/test/webcrypto/text.base64.aes-gcm256.enc b/test/webcrypto/text.base64.aes-gcm256.enc
new file mode 100644
index 0000000..7bcbb12
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-gcm256.enc
@@ -0,0 +1 @@
+JCRgSCD1e7h0ogveLrzbaUBby151RIajzxFhHyD4JpD36kBOL2Kz
diff --git a/test/webcrypto/text.base64.rsa-oaep.enc b/test/webcrypto/text.base64.rsa-oaep.enc
new file mode 100644
index 0000000..829f059
--- /dev/null
+++ b/test/webcrypto/text.base64.rsa-oaep.enc
@@ -0,0 +1,3 @@
+lRLk3t6LYvQBJkOYqWYSWYcHaPmsskb+vgcV3bwnfHF2MNp5oALe14mn/4m759oWCQIgXA/3kC1E
+kHXOBERS2+wKOXD2hS68kZnnrfWq6//7yw7Fvzv9OjG5mYSUaKcO/p+zaJmorsgvOy+nyZJs+BPD
+dKU3ohuz1MJ0wrGkki4=
diff --git a/test/webcrypto/text.base64.sha1.ecdsa.sig b/test/webcrypto/text.base64.sha1.ecdsa.sig
new file mode 100644
index 0000000..be59cc2
--- /dev/null
+++ b/test/webcrypto/text.base64.sha1.ecdsa.sig
@@ -0,0 +1,2 @@
+MEQCIAZ/sGPfuYivvm5UsqZgiR2jtT88d2moIgnAh6h1jKdVAiALKiu3myhI046rhEThSLyReuTu
+eIEgeCPBa2xGZnFXEg==
diff --git a/test/webcrypto/text.base64.sha1.hmac.sig b/test/webcrypto/text.base64.sha1.hmac.sig
new file mode 100644
index 0000000..2515963
--- /dev/null
+++ b/test/webcrypto/text.base64.sha1.hmac.sig
@@ -0,0 +1 @@
+eVw25ESkzl+mDQs7z5VGkxqneZ4=
diff --git a/test/webcrypto/text.base64.sha1.pkcs1.sig b/test/webcrypto/text.base64.sha1.pkcs1.sig
new file mode 100644
index 0000000..95ba1e5
--- /dev/null
+++ b/test/webcrypto/text.base64.sha1.pkcs1.sig
@@ -0,0 +1,3 @@
+H4lVoQebJkYFFJyXwBT2C6QDJ2OUQhQ153WjnOzaXLtlUtHdI7EOv8/hJ84ojDRJ4IyLXtGO8up9
+3WUIPw1tfwAI3X36MbMN04+HKzVabg4cTy0HnFu3k7D2hq+1vn6rT1Q7xT9C2SJBFmR/HxC2oHKz
+NcpELOP8crsoqu0c3QY=
diff --git a/test/webcrypto/text.base64.sha1.rsa-pss.16.sig b/test/webcrypto/text.base64.sha1.rsa-pss.16.sig
new file mode 100644
index 0000000..0587647
--- /dev/null
+++ b/test/webcrypto/text.base64.sha1.rsa-pss.16.sig
@@ -0,0 +1,3 @@
+aHWhiEOYGTRZyJNeNoEELFFyN+ZYgFLI+rNyuuaQpLEWDHqbUY0tdGenvIiiUxN0GKq/72g6CvyH
+RXq9VUL4Q+qkDbmROzBC0/P+RqgxpcNVJQx04RyGRVnw+l+GzE3rwbDCQG+95okBOxnac21thk/k
+GBJQGXhimg6XkCK3vVo=
diff --git a/test/webcrypto/text.base64.sha256.ecdsa.sig b/test/webcrypto/text.base64.sha256.ecdsa.sig
new file mode 100644
index 0000000..c7e72be
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.ecdsa.sig
@@ -0,0 +1,2 @@
+MEUCIFEw11evEWohKswRe3Za0P0u7mvGj4kSnHix/EOKhxApAiEAq2QtwNvFg8RdY6t01ff8mUTP
+nT1lEfMSRZmtuVxQuQA=
diff --git a/test/webcrypto/text.base64.sha256.hmac.sig b/test/webcrypto/text.base64.sha256.hmac.sig
new file mode 100644
index 0000000..130cc62
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.hmac.sig
@@ -0,0 +1 @@
+UbsmYPe0ek53QMpDPP/Y4V50bRXQQGNrYCTuzg1tznE=
diff --git a/test/webcrypto/text.base64.sha256.hmac.sig.broken b/test/webcrypto/text.base64.sha256.hmac.sig.broken
new file mode 100644
index 0000000..f2b59c0
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.hmac.sig.broken
@@ -0,0 +1 @@
+UbsmYPe0ek53QMpDPP/Y4V50bRXQQGNrYCAuzg1tznE=
diff --git a/test/webcrypto/text.base64.sha256.pkcs1.sig b/test/webcrypto/text.base64.sha256.pkcs1.sig
new file mode 100644
index 0000000..078d42d
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.pkcs1.sig
@@ -0,0 +1,3 @@
+IyGM/e2IMJCrJmE91GkddTyCble3554d8KvpbL1k10QrRlDE1afCab7iwmz4j1yl8pNMbSTKIb0y
+RfMQ3YlsKxzoJm2+pIrXErb2jF3emnVkUxNuSY3/ROK3rU8YirPbKhvkjulVwVlh4b6YpiXwuKTL
+HDmHp7AOr7yzjS3VZrs=
diff --git a/test/webcrypto/text.base64.sha256.rsa-pss.0.sig b/test/webcrypto/text.base64.sha256.rsa-pss.0.sig
new file mode 100644
index 0000000..ef84420
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.rsa-pss.0.sig
@@ -0,0 +1,3 @@
+DNQrIYaW4opZG1OdyFujH2rxgk06HB2eTUuCGyiN971pAVxCqYn0NhL7iMBrUrgsxnqBH+nNC1jg
+AMFGe0rtJE/9blWb9QiNz/kwitFI4oztXkcCHcQYwatbQTGQgqeA2rY9N6w6QwMAYJeEd4Jm0lTE
+oJx9N+C5QoArKBPgXxw=
diff --git a/test/webcrypto/text.base64.sha256.rsa-pss.32.sig b/test/webcrypto/text.base64.sha256.rsa-pss.32.sig
new file mode 100644
index 0000000..184bcf8
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.rsa-pss.32.sig
@@ -0,0 +1,3 @@
+B+69Fvykn5ncPxA91DQv7K5r5D25f0LuEK60h4WNWev8Hl/Bzseq315o/Ja7RhlgtYltBZTETqUk
+hkaWEWExCG1kw+xUbsz1HsdHJOwF+e52zirKGifondHVqTOl95aU5msRcuKtCEifnvgKqNE9c+Dz
+l1hoPlgGrhtnyI0DlcM=
diff --git a/test/webcrypto/verify.js b/test/webcrypto/verify.js
new file mode 100644
index 0000000..a969e3b
--- /dev/null
+++ b/test/webcrypto/verify.js
@@ -0,0 +1,207 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+    crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+    function validate(t, r, i) {
+        if (r.status == "fulfilled" && !t[i].exception) {
+            return r.value === "SUCCESS";
+        }
+
+        if (r.status == "rejected" && t[i].exception) {
+            if (process.argv[2] === '--match-exception-text') {
+                /* is not compatible with node.js format */
+                return r.reason.toString().startsWith(t[i].exception);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    for (let k = 0; k < tlist.length; k++) {
+        let ts = tlist[k];
+        let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+        let r = results.map((r, i) => validate(ts.tests, r, i));
+
+        console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+        r.forEach((v, i) => {
+            if (!v) {
+                console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n    with reason: ${results[i].reason}`);
+            }
+        })
+    }
+}
+
+function merge(to, from) {
+    let r = Object.assign({}, to);
+    Object.keys(from).forEach(v => {
+        if (typeof r[v] == 'object' && typeof from[v] == 'object') {
+            r[v] = merge(r[v], from[v]);
+
+        } else if (typeof from[v] == 'object') {
+            r[v] = Object.assign({}, from[v]);
+
+        } else {
+            r[v] = from[v];
+        }
+    })
+
+    return r;
+};
+
+function base64decode(b64) {
+    const joined = b64.toString().split('\n').join('');
+    return Buffer.from(joined, 'base64');
+}
+
+function pem_to_der(pem, type) {
+    const pemJoined = pem.toString().split('\n').join('');
+    const pemHeader = `-----BEGIN ${type} KEY-----`;
+    const pemFooter = `-----END ${type} KEY-----`;
+    const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+    return Buffer.from(pemContents, 'base64');
+}
+
+function p(args, default_opts) {
+    let encoder = new TextEncoder();
+    let params = merge({}, default_opts);
+    params = merge(params, args);
+
+    switch (params.key.fmt) {
+    case "spki":
+        let pem = fs.readFileSync(`test/webcrypto/${params.key.file}`);
+        params.key.file = pem_to_der(pem, "PUBLIC");
+        break;
+    case "raw":
+        params.key.file = Buffer.from(params.key.file, "hex");
+        break;
+    }
+
+    params.signature = base64decode(fs.readFileSync(`test/webcrypto/${params.signature}`));
+    params.text = encoder.encode(params.text);
+
+    return params;
+}
+
+
+async function test(params) {
+    let key = await crypto.subtle.importKey(params.key.fmt,
+                                            params.key.file,
+                                            params.import_alg,
+                                            false, ["verify"]);
+
+    let r = await crypto.subtle.verify(params.verify_alg,
+                                       key, params.signature,
+                                       params.text);
+
+    if (params.expected !== r) {
+        throw Error(`${params.import_alg.name} failed expected: "${params.expected}" vs "${r}"`);
+    }
+
+    return 'SUCCESS';
+}
+
+let hmac_tsuite = {
+    name: "HMAC verify",
+    opts: {
+        text: "SigneD-TExt",
+        key: { fmt: "raw", file: "aabbcc" },
+        import_alg: {
+            name: "HMAC",
+            hash: "SHA-256",
+        },
+        verify_alg: {
+            name: "HMAC",
+        },
+    },
+
+    tests: [
+        { signature: "text.base64.sha256.hmac.sig", expected: true },
+        { signature: "text.base64.sha256.hmac.sig.broken", expected: false },
+        { import_alg: { hash: "SHA-1" }, signature: "text.base64.sha1.hmac.sig", expected: true },
+        { import_alg: { hash: "SHA-1" }, signature: "text.base64.sha256.hmac.sig", expected: false },
+        { key: { file: "aabbccdd" }, signature: "text.base64.sha256.hmac.sig", expected: false },
+]};
+
+let rsassa_pkcs1_v1_5_tsuite = {
+    name: "RSASSA-PKCS1-v1_5 verify",
+    opts: {
+        text: "SigneD-TExt",
+        key: { fmt: "spki", file: "rsa.spki" },
+        import_alg: {
+            name: "RSASSA-PKCS1-v1_5",
+            hash: "SHA-256",
+        },
+        verify_alg: {
+            name: "RSASSA-PKCS1-v1_5",
+        },
+    },
+
+    tests: [
+        { signature: "text.base64.sha256.pkcs1.sig", expected: true },
+        { text: "SigneD-TExt2", signature: "text.base64.sha256.pkcs1.sig", expected: false },
+        { signature: "text.base64.sha1.pkcs1.sig", expected: false },
+        { import_alg: { hash: "SHA-1" }, signature: "text.base64.sha1.pkcs1.sig", expected: true },
+        { key: { file: "rsa2.spki"}, signature: "text.base64.sha256.pkcs1.sig", expected: false },
+]};
+
+let rsa_pss_tsuite = {
+    name: "RSA-PSS verify",
+    opts: {
+        text: "SigneD-TExt",
+        key: { fmt: "spki", file: "rsa.spki" },
+        import_alg: {
+            name: "RSA-PSS",
+            hash: "SHA-256",
+        },
+        verify_alg: {
+            name: "RSA-PSS",
+            saltLength: 32,
+        },
+    },
+
+    tests: [
+        { signature: "text.base64.sha256.rsa-pss.32.sig", expected: true },
+        { text: "SigneD-TExt2", signature: "text.base64.sha256.rsa-pss.32.sig", expected: false },
+        { key: { file: "rsa2.spki"}, signature: "text.base64.sha256.rsa-pss.32.sig", expected: false },
+        { verify_alg: { saltLength: 0 }, signature: "text.base64.sha256.rsa-pss.0.sig", expected: true },
+        { verify_alg: { saltLength: 0 }, signature: "text.base64.sha256.rsa-pss.0.sig", expected: true },
+        { import_alg: { hash: "SHA-1" }, signature: "text.base64.sha256.rsa-pss.32.sig", expected: false },
+        { import_alg: { hash: "SHA-1" }, verify_alg: { saltLength: 16 }, signature: "text.base64.sha1.rsa-pss.16.sig",
+          expected: true },
+        { verify_alg: { saltLength: 16 }, signature: "text.base64.sha256.rsa-pss.32.sig", expected: false },
+]};
+
+let ecdsa_tsuite = {
+    name: "ECDSA verify",
+    opts: {
+        text: "SigneD-TExt",
+        key: { fmt: "spki", file: "ec.spki" },
+        import_alg: {
+            name: "ECDSA",
+            namedCurve: "P-256",
+        },
+        verify_alg: {
+            name: "ECDSA",
+            hash: "SHA-256",
+        },
+    },
+
+    tests: [
+        { signature: "text.base64.sha256.ecdsa.sig", expected: true },
+        { signature: "text.base64.sha1.ecdsa.sig", expected: false },
+        { verify_alg: { hash: "SHA-1"}, signature: "text.base64.sha1.ecdsa.sig", expected: true },
+        { key: { file: "ec2.spki" }, signature: "text.base64.sha256.ecdsa.sig", expected: false },
+]};
+
+run([
+    hmac_tsuite,
+    rsassa_pkcs1_v1_5_tsuite,
+    rsa_pss_tsuite,
+    ecdsa_tsuite,
+], test, p);
diff --git a/ts/index.d.ts b/ts/index.d.ts
index 8a324cc..eec5c34 100644
--- a/ts/index.d.ts
+++ b/ts/index.d.ts
@@ -1,4 +1,5 @@
 /// <reference path="njs_core.d.ts" />
+/// <reference path="njs_webcrypto.d.ts" />
 /// <reference path="njs_modules/crypto.d.ts" />
 /// <reference path="njs_modules/fs.d.ts" />
 /// <reference path="njs_modules/querystring.d.ts" />
diff --git a/ts/njs_core.d.ts b/ts/njs_core.d.ts
index 808c08f..7037845 100644
--- a/ts/njs_core.d.ts
+++ b/ts/njs_core.d.ts
@@ -584,7 +584,7 @@
     writeFloatLE(value: number, offset?: number): number;
 }
 
-type NjsStringOrBuffer = NjsStringLike | Buffer | DataView | TypedArray;
+type NjsStringOrBuffer = NjsStringLike | Buffer | DataView | TypedArray | ArrayBuffer;
 
 // Global objects
 
diff --git a/ts/njs_webcrypto.d.ts b/ts/njs_webcrypto.d.ts
new file mode 100644
index 0000000..b67ee0d
--- /dev/null
+++ b/ts/njs_webcrypto.d.ts
@@ -0,0 +1,226 @@
+interface  RsaOaepParams {
+    name: "RSA-OAEP";
+}
+
+interface  AesCtrParams {
+    name: "AES-CTR";
+    counter: NjsStringOrBuffer;
+    length: number;
+}
+
+interface  AesCbcParams {
+    name: "AES-CBC";
+    iv: NjsStringOrBuffer;
+}
+
+interface  AesGcmParams {
+    name: "AES-GCM";
+    iv: NjsStringOrBuffer;
+    additionalData?: NjsStringOrBuffer;
+    tagLength?: number;
+}
+
+type CipherAlgorithm =
+    | RsaOaepParams
+    | AesCtrParams
+    | AesCbcParams
+    | AesCbcParams;
+
+type HashVariants = "SHA-256" | "SHA-384" | "SHA-512" | "SHA-1";
+
+interface  RsaHashedImportParams {
+    name: "RSASSA-PKCS1-v1_5" | "RSA-PSS" | "RSA-OAEP";
+    hash: HashVariants;
+}
+
+interface  EcKeyImportParams {
+    name: "ECDSA";
+    namedCurve: "P-256" | "P-384" | "P-521";
+}
+
+interface  HmacImportParams {
+    name: "HMAC";
+    hash: HashVariants;
+}
+
+type AesVariants = "AES-CTR" | "AES-CBC" | "AES-GCM";
+
+interface  AesImportParams {
+    name: AesVariants;
+}
+
+type ImportAlgorithm =
+    | RsaHashedImportParams
+    | EcKeyImportParams
+    | HmacImportParams
+    | AesImportParams
+    | AesVariants
+    | "PBKDF2"
+    | "HKDF";
+
+interface   HkdfParams {
+    name: "HKDF";
+    hash: HashVariants;
+    salt: NjsStringOrBuffer;
+    info: NjsStringOrBuffer;
+}
+
+interface   Pbkdf2Params {
+    name: "PBKDF2";
+    hash: HashVariants;
+    salt: NjsStringOrBuffer;
+    interations: number;
+}
+
+type DeriveAlgorithm =
+    | HkdfParams
+    | Pbkdf2Params;
+
+interface   HmacKeyGenParams {
+    name: "HMAC";
+    hash: HashVariants;
+}
+
+interface   AesKeyGenParams {
+    name: AesVariants;
+    length: number;
+}
+
+type DeriveKeyAlgorithm =
+    | HmacKeyGenParams
+    | AesKeyGenParams;
+
+interface   RsaPssParams {
+    name: "RSA-PSS";
+    saltLength: number;
+}
+
+interface   EcdsaParams {
+    name: "ECDSA";
+    hash: HashVariants;
+}
+
+type SignOrVerifyAlgorithm =
+    | RsaPssParams
+    | EcdsaParams
+    | { name: "HMAC"; }
+    | { name: "RSASSA-PKCS1-v1_5"; }
+    | "HMAC"
+    | "RSASSA-PKCS1-v1_5";
+
+interface CryptoKey {
+}
+
+interface SubtleCrypto {
+    /**
+     * Decrypts encrypted data.
+     *
+     * @param algorithm Object specifying the algorithm to be used,
+     *  and any extra parameters as required.
+     * @param key CryptoKey containing the key to be used for decryption.
+     * @param data Data to be decrypted.
+     */
+    decrypt(algorithm: CipherAlgorithm,
+            key: CryptoKey,
+            data: NjsStringOrBuffer): Promise<ArrayBuffer>;
+
+    /**
+     * Derives an array of bits from a base key.
+     *
+     * @param algorithm Object defining the derivation algorithm to use.
+     * @param baseKey CryptoKey representing the input to the derivation algorithm.
+     * @param length Number representing the number of bits to derive.
+     */
+    deriveBits(algorithm: DeriveAlgorithm,
+               baseKey: CryptoKey,
+               length: number): Promise<ArrayBuffer>;
+
+    /**
+     * Derives a secret key from a master key.
+     *
+     * @param algorithm Object defining the derivation algorithm to use.
+     * @param baseKey CryptoKey representing the input to the derivation algorithm.
+     * @param derivedKeyAlgorithm Object defining the algorithm the
+     *  derived key will be used for.
+     * @param extractable Unsupported.
+     * @param usage Array indicating what can be done with the key.
+     *  Possible array values: "encrypt", "decrypt", "sign", "verify",
+     *  "deriveKey", "deriveBits", "wrapKey", "unwrapKey".
+     */
+    deriveKey(algorithm: DeriveAlgorithm,
+              baseKey: CryptoKey,
+              derivedKeyAlgorithm: DeriveKeyAlgorithm,
+              extractable: boolean,
+              usage: Array<string>): Promise<CryptoKey>;
+
+    /**
+     * Generates a digest of the given data.
+     *
+     * @param algorithm String defining the hash function to use.
+     */
+    digest(algorithm: HashVariants,
+           data: NjsStringOrBuffer): Promise<ArrayBuffer>;
+
+    /**
+     * Encrypts data.
+     *
+     * @param algorithm Object specifying the algorithm to be used,
+     *  and any extra parameters as required.
+     * @param key CryptoKey containing the key to be used for encryption.
+     * @param data Data to be encrypted.
+     */
+    encrypt(algorithm: CipherAlgorithm,
+            key: CryptoKey,
+            data: NjsStringOrBuffer): Promise<ArrayBuffer>;
+
+    /**
+     * Imports a key.
+     *
+     * @param format String describing the data format of the key to import.
+     * @param keyData Object containing the key in the given format.
+     * @param algorithm Dictionary object defining the type of key to import
+     *  and providing extra algorithm-specific parameters.
+     * @param extractable Unsupported.
+     * @param usage Array indicating what can be done with the key.
+     *  Possible array values: "encrypt", "decrypt", "sign", "verify",
+     *  "deriveKey", "deriveBits", "wrapKey", "unwrapKey".
+     */
+    importKey(format: "raw" | "pkcs8" | "spki",
+              keyData: NjsStringOrBuffer,
+              algorithm: ImportAlgorithm,
+              extractable: boolean,
+              usage: Array<string>): Promise<CryptoKey>;
+
+    /**
+     * Generates a digital signature.
+     *
+     * @param algorithm String or object that specifies the signature
+     *  algorithm to use and its parameters.
+     * @param key CryptoKey containing the key to be used for signing.
+     * @param data Data to be signed.
+     */
+    sign(algorithm: SignOrVerifyAlgorithm,
+         key: CryptoKey,
+         data: NjsStringOrBuffer): Promise<ArrayBuffer>;
+
+    /**
+     * Verifies a digital signature.
+     *
+     * @param algorithm String or object that specifies the signature
+     *  algorithm to use and its parameters.
+     * @param key CryptoKey containing the key to be used for verifying.
+     * @param signature Signature to verify.
+     * @param data Data to be verified.
+     */
+    verify(algorithm: SignOrVerifyAlgorithm,
+           key: CryptoKey,
+           signature: NjsStringOrBuffer,
+           data: NjsStringOrBuffer): Promise<boolean>;
+}
+
+interface Crypto {
+    readonly subtle: SubtleCrypto;
+    getRandomValues(ta:TypedArray): TypedArray;
+}
+
+declare const crypto: Crypto;