]> git.kaiwu.me - njs.git/commitdiff
WebCrypto: added Ed25519 and X25519 support.
authorDmitry Volyntsev <xeioex@nginx.com>
Fri, 3 Apr 2026 07:00:58 +0000 (00:00 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Mon, 6 Apr 2026 22:24:37 +0000 (15:24 -0700)
Implemented Ed25519 sign/verify/generateKey/importKey/exportKey
Supports raw, PKCS8, SPKI, and JWK (OKP) key formats.

Implemented X25519 deriveBits/deriveKey/generateKey/importKey/
exportKey.

auto/openssl
external/njs_webcrypto_module.c
external/qjs_webcrypto_module.c
test/webcrypto/README.rst
test/webcrypto/ed25519.t.mjs [new file with mode: 0644]
test/webcrypto/x25519.t.mjs [new file with mode: 0644]
ts/njs_webcrypto.d.ts

index bedd711bcf18f6dae17cb182f75ee3e19c4a2125..b5f65a7048e757a11730ba722c0c1af92a9d7aa2 100644 (file)
@@ -44,6 +44,22 @@ if [ $NJS_OPENSSL = YES ]; then
                          }"
         . auto/feature
 
+        # Ed25519 and X25519 share the same Curve25519 implementation and
+        # were introduced together in OpenSSL 1.1.1, LibreSSL 3.7.0,
+        # BoringSSL, and AWS-LC.  No known build configuration enables
+        # one without the other, so a single probe is sufficient.
+
+        njs_feature="EVP_PKEY_ED25519"
+        njs_feature_name=NJS_HAVE_ED25519
+        njs_feature_run=
+        njs_feature_test="#include <openssl/evp.h>
+
+                          int main() {
+                              return EVP_PKEY_new_raw_public_key(
+                                  EVP_PKEY_ED25519, NULL, NULL, 0) == NULL;
+                         }"
+        . auto/feature
+
         njs_feature="OpenSSL version"
         njs_feature_name=NJS_OPENSSL_VERSION
         njs_feature_run=value
index 1b67b2fe227382c32e85d6e562fe6702ca50efc8..a421dfac5f194e6a415f835def37986206be8e35 100644 (file)
@@ -43,6 +43,8 @@ typedef enum {
     NJS_ALGORITHM_AES_KW,
     NJS_ALGORITHM_ECDSA,
     NJS_ALGORITHM_ECDH,
+    NJS_ALGORITHM_ED25519,
+    NJS_ALGORITHM_X25519,
     NJS_ALGORITHM_PBKDF2,
     NJS_ALGORITHM_HKDF,
     NJS_ALGORITHM_MAX,
@@ -124,6 +126,12 @@ static njs_int_t njs_ext_export_key(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
 static njs_int_t njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
+#if (NJS_HAVE_ED25519)
+static njs_int_t njs_webcrypto_generate_25519_keypair(njs_vm_t *vm,
+    int pkey_id, njs_webcrypto_key_t *key, njs_webcrypto_algorithm_t *alg,
+    unsigned usage, njs_bool_t extractable, unsigned priv_usage,
+    unsigned pub_usage, njs_value_t *retval);
+#endif
 static njs_int_t njs_ext_import_key(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
 static njs_int_t njs_ext_sign(njs_vm_t *vm, njs_value_t *args,
@@ -314,6 +322,34 @@ static njs_webcrypto_entry_t njs_webcrypto_alg[] = {
                               0)
     },
 
+#if (NJS_HAVE_ED25519)
+    {
+      njs_str("Ed25519"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_ED25519,
+                              NJS_KEY_USAGE_SIGN |
+                              NJS_KEY_USAGE_VERIFY |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_PKCS8 |
+                              NJS_KEY_FORMAT_SPKI |
+                              NJS_KEY_FORMAT_RAW |
+                              NJS_KEY_FORMAT_JWK,
+                              0)
+    },
+
+    {
+      njs_str("X25519"),
+      njs_webcrypto_algorithm(NJS_ALGORITHM_X25519,
+                              NJS_KEY_USAGE_DERIVE_KEY |
+                              NJS_KEY_USAGE_DERIVE_BITS |
+                              NJS_KEY_USAGE_GENERATE_KEY,
+                              NJS_KEY_FORMAT_PKCS8 |
+                              NJS_KEY_FORMAT_SPKI |
+                              NJS_KEY_FORMAT_RAW |
+                              NJS_KEY_FORMAT_JWK,
+                              0)
+    },
+#endif
+
     {
       njs_str("PBKDF2"),
       njs_webcrypto_algorithm(NJS_ALGORITHM_PBKDF2,
@@ -1649,12 +1685,14 @@ njs_ext_derive_ecdh(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
         goto fail;
     }
 
-    if (njs_slow_path(pkey->alg->type != NJS_ALGORITHM_ECDH)) {
-        njs_vm_type_error(vm, "algorithm.public is not an ECDH key");
+    if (njs_slow_path(pkey->alg->type != key->alg->type)) {
+        njs_vm_type_error(vm, "algorithm.public key type mismatch");
         goto fail;
     }
 
-    if (njs_slow_path(key->u.a.curve != pkey->u.a.curve)) {
+    if (key->alg->type == NJS_ALGORITHM_ECDH
+        && njs_slow_path(key->u.a.curve != pkey->u.a.curve))
+    {
         njs_vm_type_error(vm, "ECDH keys must use the same curve");
         goto fail;
     }
@@ -1801,7 +1839,9 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
         goto fail;
     }
 
-    if (alg->type == NJS_ALGORITHM_ECDH) {
+    if (alg->type == NJS_ALGORITHM_ECDH
+        || alg->type == NJS_ALGORITHM_X25519)
+    {
         return njs_ext_derive_ecdh(vm, args, nargs, derive_key, key, retval);
     }
 
@@ -2542,6 +2582,116 @@ njs_export_jwk_oct(njs_vm_t *vm, njs_webcrypto_key_t *key, njs_value_t *retval)
 }
 
 
+#if (NJS_HAVE_ED25519)
+static njs_int_t
+njs_export_jwk_okp(njs_vm_t *vm, njs_webcrypto_key_t *key,
+    njs_value_t *retval)
+{
+    size_t              len;
+    njs_int_t           ret;
+    njs_str_t           raw;
+    const njs_str_t     *crv_name;
+    njs_opaque_value_t  x, d, ops, extractable, okp_s, crv_s;
+    u_char              buf[64];
+
+    static const njs_str_t  ed25519 = njs_str("Ed25519");
+    static const njs_str_t  x25519 = njs_str("X25519");
+
+    crv_name = (key->alg->type == NJS_ALGORITHM_X25519) ? &x25519 : &ed25519;
+
+    njs_assert(key->u.a.pkey != NULL);
+
+    len = 32;
+    if (EVP_PKEY_get_raw_public_key(key->u.a.pkey, buf, &len) != 1) {
+        njs_webcrypto_error(vm, "EVP_PKEY_get_raw_public_key() failed");
+        return NJS_ERROR;
+    }
+
+    raw.start = buf;
+    raw.length = len;
+
+    ret = njs_string_base64url(vm, njs_value_arg(&x), &raw);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    if (key->u.a.privat) {
+        len = 32;
+        if (EVP_PKEY_get_raw_private_key(key->u.a.pkey, buf, &len) != 1) {
+            njs_webcrypto_error(vm, "EVP_PKEY_get_raw_private_key() failed");
+            return NJS_ERROR;
+        }
+
+        raw.start = buf;
+        raw.length = len;
+
+        ret = njs_string_base64url(vm, njs_value_arg(&d), &raw);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    ret = njs_key_ops(vm, njs_value_arg(&ops), key->usage);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    njs_value_boolean_set(njs_value_arg(&extractable), key->extractable);
+
+    ret = njs_vm_object_alloc(vm, retval, NULL);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_value_string_create(vm, njs_value_arg(&okp_s),
+                                     (u_char *) "OKP", 3);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_value_string_create(vm, njs_value_arg(&crv_s), crv_name->start,
+                               crv_name->length);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_object_prop_set(vm, retval, &string_kty, &okp_s);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_object_prop_set(vm, retval, &string_crv, &crv_s);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_object_prop_set(vm, retval, &string_x, &x);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    if (key->u.a.privat) {
+        ret = njs_vm_object_prop_set(vm, retval, &string_d, &d);
+        if (ret != NJS_OK) {
+            return NJS_ERROR;
+        }
+    }
+
+    ret = njs_vm_object_prop_set(vm, retval, &key_ops, &ops);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_object_prop_set(vm, retval, &string_ext, &extractable);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
+#endif
+
+
 static njs_int_t
 njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused, njs_value_t *retval)
@@ -2588,6 +2738,94 @@ fail:
 }
 
 
+#if (NJS_HAVE_ED25519)
+static njs_int_t
+njs_webcrypto_generate_25519_keypair(njs_vm_t *vm, int pkey_id,
+    njs_webcrypto_key_t *key, njs_webcrypto_algorithm_t *alg, unsigned usage,
+    njs_bool_t extractable, unsigned priv_usage, unsigned pub_usage,
+    njs_value_t *retval)
+{
+    njs_int_t            ret;
+    EVP_PKEY_CTX         *ctx;
+    njs_opaque_value_t   value, pub, priv;
+    njs_webcrypto_key_t  *keypub;
+
+    static const njs_str_t  string_priv = njs_str("privateKey");
+    static const njs_str_t  string_pub = njs_str("publicKey");
+
+    ctx = EVP_PKEY_CTX_new_id(pkey_id, NULL);
+    if (njs_slow_path(ctx == NULL)) {
+        njs_webcrypto_error(vm, "EVP_PKEY_CTX_new_id() failed");
+        return NJS_ERROR;
+    }
+
+    if (EVP_PKEY_keygen_init(ctx) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        njs_webcrypto_error(vm, "EVP_PKEY_keygen_init() failed");
+        return NJS_ERROR;
+    }
+
+    if (EVP_PKEY_keygen(ctx, &key->u.a.pkey) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        njs_webcrypto_error(vm, "EVP_PKEY_keygen() failed");
+        return NJS_ERROR;
+    }
+
+    EVP_PKEY_CTX_free(ctx);
+
+    key->u.a.privat = 1;
+    key->usage = priv_usage & usage;
+
+    keypub = njs_webcrypto_key_alloc(vm, alg, usage, extractable);
+    if (njs_slow_path(keypub == NULL)) {
+        return NJS_ERROR;
+    }
+
+    if (njs_pkey_up_ref(key->u.a.pkey) <= 0) {
+        njs_webcrypto_error(vm, "njs_pkey_up_ref() failed");
+        return NJS_ERROR;
+    }
+
+    keypub->u.a.pkey = key->u.a.pkey;
+    keypub->usage = pub_usage & usage;
+
+    ret = njs_vm_external_create(vm, njs_value_arg(&priv),
+                                 njs_webcrypto_crypto_key_proto_id, key, 0);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_external_create(vm, njs_value_arg(&pub),
+                                 njs_webcrypto_crypto_key_proto_id, keypub,
+                                 0);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_object_alloc(vm, njs_value_arg(&value), NULL);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_object_prop_set(vm, njs_value_arg(&value), &string_priv,
+                                 &priv);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_object_prop_set(vm, njs_value_arg(&value), &string_pub,
+                                 &pub);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    njs_value_assign(retval, &value);
+
+    return NJS_OK;
+}
+#endif
+
+
 static njs_int_t
 njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused, njs_value_t *retval)
@@ -2821,6 +3059,35 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 
         break;
 
+#if (NJS_HAVE_ED25519)
+    case NJS_ALGORITHM_ED25519:
+        ret = njs_webcrypto_generate_25519_keypair(vm, EVP_PKEY_ED25519,
+                                                   key, alg, usage,
+                                                   extractable,
+                                                   NJS_KEY_USAGE_SIGN,
+                                                   NJS_KEY_USAGE_VERIFY,
+                                                   njs_value_arg(&value));
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        break;
+
+    case NJS_ALGORITHM_X25519:
+        ret = njs_webcrypto_generate_25519_keypair(vm, EVP_PKEY_X25519,
+                                                   key, alg, usage,
+                                                   extractable,
+                                                   NJS_KEY_USAGE_DERIVE_KEY
+                                                   | NJS_KEY_USAGE_DERIVE_BITS,
+                                                   0,
+                                                   njs_value_arg(&value));
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto fail;
+        }
+
+        break;
+#endif
+
     case NJS_ALGORITHM_AES_GCM:
     case NJS_ALGORITHM_AES_CTR:
     case NJS_ALGORITHM_AES_CBC:
@@ -3379,6 +3646,88 @@ fail:
 }
 
 
+#if (NJS_HAVE_ED25519)
+static EVP_PKEY *
+njs_import_jwk_okp(njs_vm_t *vm, njs_value_t *jwk, njs_webcrypto_key_t *key)
+{
+    int                 pkey_id;
+    njs_str_t           crv, x_b64, d_b64, x_raw, d_raw;
+    njs_value_t         *val;
+    EVP_PKEY            *pkey;
+    njs_opaque_value_t  value;
+    u_char              x_buf[32], d_buf[32];
+
+    static const njs_str_t  ed25519 = njs_str("Ed25519");
+    static const njs_str_t  x25519 = njs_str("X25519");
+
+    val = njs_vm_object_prop(vm, jwk, &string_crv, &value);
+    if (njs_slow_path(val == NULL || !njs_value_is_string(val))) {
+        njs_vm_type_error(vm, "Invalid JWK OKP crv");
+        return NULL;
+    }
+
+    njs_value_string_get(vm, val, &crv);
+
+    if (njs_strstr_eq(&crv, &ed25519)) {
+        pkey_id = EVP_PKEY_ED25519;
+
+    } else if (njs_strstr_eq(&crv, &x25519)) {
+        pkey_id = EVP_PKEY_X25519;
+
+    } else {
+        njs_vm_type_error(vm, "unsupported JWK OKP curve: \"%V\"", &crv);
+        return NULL;
+    }
+
+    val = njs_vm_object_prop(vm, jwk, &string_x, &value);
+    if (njs_slow_path(val == NULL || !njs_value_is_string(val))) {
+        njs_vm_type_error(vm, "Invalid JWK OKP x");
+        return NULL;
+    }
+
+    njs_value_string_get(vm, val, &x_b64);
+
+    x_raw.start = x_buf;
+    (void) njs_decode_base64url_length(&x_b64, &x_raw.length);
+
+    if (x_raw.length != 32) {
+        njs_vm_type_error(vm, "Invalid JWK OKP x length");
+        return NULL;
+    }
+
+    njs_decode_base64url(&x_raw, &x_b64);
+
+    val = njs_vm_object_prop(vm, jwk, &string_d, &value);
+    if (val != NULL && njs_value_is_string(val)) {
+        njs_value_string_get(vm, val, &d_b64);
+
+        d_raw.start = d_buf;
+        (void) njs_decode_base64url_length(&d_b64, &d_raw.length);
+
+        if (d_raw.length != 32) {
+            njs_vm_type_error(vm, "Invalid JWK OKP d length");
+            return NULL;
+        }
+
+        njs_decode_base64url(&d_raw, &d_b64);
+
+        pkey = EVP_PKEY_new_raw_private_key(pkey_id, NULL, d_buf, 32);
+        key->u.a.privat = 1;
+
+    } else {
+        pkey = EVP_PKEY_new_raw_public_key(pkey_id, NULL, x_buf, 32);
+    }
+
+    if (njs_slow_path(pkey == NULL)) {
+        njs_webcrypto_error(vm, "EVP_PKEY_new_raw_*_key() failed");
+        return NULL;
+    }
+
+    return pkey;
+}
+#endif
+
+
 static njs_int_t
 njs_import_jwk_oct(njs_vm_t *vm, njs_value_t *jwk, njs_webcrypto_key_t *key)
 {
@@ -3682,6 +4031,23 @@ njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
                 goto fail;
             }
 
+#if (NJS_HAVE_ED25519)
+        } else if (njs_strstr_eq(&kty, &njs_str_value("OKP"))) {
+            if (alg->type != NJS_ALGORITHM_ED25519
+                && alg->type != NJS_ALGORITHM_X25519)
+            {
+                njs_vm_type_error(vm, "JWK kty \"OKP\" doesn't match "
+                                  "algorithm \"%V\"",
+                                  njs_algorithm_string(alg));
+                goto fail;
+            }
+
+            pkey = njs_import_jwk_okp(vm, jwk, key);
+            if (njs_slow_path(pkey == NULL)) {
+                goto fail;
+            }
+#endif
+
         } else {
             njs_vm_type_error(vm, "invalid JWK key type: %V", &kty);
             goto fail;
@@ -3816,6 +4182,68 @@ njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 
         break;
 
+#if (NJS_HAVE_ED25519)
+    case NJS_ALGORITHM_ED25519:
+        if (fmt == NJS_KEY_FORMAT_RAW) {
+            pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL,
+                                               key_data.start, key_data.length);
+            if (njs_slow_path(pkey == NULL)) {
+                njs_webcrypto_error(vm,
+                    "EVP_PKEY_new_raw_public_key() failed");
+                goto fail;
+            }
+        }
+
+        if (EVP_PKEY_id(pkey) != EVP_PKEY_ED25519) {
+            njs_vm_type_error(vm, "Ed25519 key is not found");
+            goto fail;
+        }
+
+        mask = key->u.a.privat ? ~NJS_KEY_USAGE_SIGN : ~NJS_KEY_USAGE_VERIFY;
+
+        if (key->usage & mask) {
+            njs_vm_type_error(vm, "key usage mismatch for \"%V\" key",
+                              njs_algorithm_string(alg));
+            goto fail;
+        }
+
+        key->u.a.pkey = pkey;
+
+        break;
+
+    case NJS_ALGORITHM_X25519:
+        if (fmt == NJS_KEY_FORMAT_RAW) {
+            pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL,
+                                               key_data.start, key_data.length);
+            if (njs_slow_path(pkey == NULL)) {
+                njs_webcrypto_error(vm,
+                    "EVP_PKEY_new_raw_public_key() failed");
+                goto fail;
+            }
+        }
+
+        if (EVP_PKEY_id(pkey) != EVP_PKEY_X25519) {
+            njs_vm_type_error(vm, "X25519 key is not found");
+            goto fail;
+        }
+
+        if (key->u.a.privat) {
+            mask = ~(NJS_KEY_USAGE_DERIVE_KEY | NJS_KEY_USAGE_DERIVE_BITS);
+        } else {
+            mask = 0;
+        }
+
+        if (key->usage & mask) {
+            njs_vm_type_error(vm, "key usage mismatch for \"%V\" key",
+                              njs_algorithm_string(alg));
+            goto fail;
+        }
+
+        key->u.a.pkey = pkey;
+
+        break;
+#endif
+
     case NJS_ALGORITHM_HMAC:
         if (fmt == NJS_KEY_FORMAT_RAW) {
             ret = njs_algorithm_hash(vm, options, &key->hash);
@@ -4134,6 +4562,7 @@ njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_webcrypto_algorithm_t  *alg;
     unsigned char              m[EVP_MAX_MD_SIZE];
 
+    dst = NULL;
     mctx = NULL;
     pctx = NULL;
 
@@ -4183,21 +4612,86 @@ njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
         }
     }
 
+    md = NULL;
+
     if (alg->type == NJS_ALGORITHM_ECDSA) {
         ret = njs_algorithm_hash(vm, options, &hash);
         if (njs_slow_path(ret == NJS_ERROR)) {
             goto fail;
         }
 
-    } else {
+        md = njs_algorithm_hash_digest(hash);
+
+    } else if (alg->type != NJS_ALGORITHM_ED25519) {
         hash = key->hash;
+        md = njs_algorithm_hash_digest(hash);
     }
 
-    md = njs_algorithm_hash_digest(hash);
-
     outlen = 0;
 
     switch (alg->type) {
+#if (NJS_HAVE_ED25519)
+    case NJS_ALGORITHM_ED25519:
+        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 (!verify) {
+            ret = EVP_DigestSignInit(mctx, NULL, NULL, NULL,
+                                     key->u.a.pkey);
+            if (njs_slow_path(ret <= 0)) {
+                njs_webcrypto_error(vm,
+                                    "EVP_DigestSignInit() failed");
+                goto fail;
+            }
+
+            outlen = 0;
+            ret = EVP_DigestSign(mctx, NULL, &outlen, data.start,
+                                 data.length);
+            if (njs_slow_path(ret <= 0)) {
+                njs_webcrypto_error(vm, "EVP_DigestSign() failed");
+                goto fail;
+            }
+
+            dst = njs_mp_alloc(njs_vm_memory_pool(vm), outlen);
+            if (njs_slow_path(dst == NULL)) {
+                njs_vm_memory_error(vm);
+                goto fail;
+            }
+
+            ret = EVP_DigestSign(mctx, dst, &outlen, data.start,
+                                 data.length);
+            if (njs_slow_path(ret <= 0)) {
+                njs_webcrypto_error(vm, "EVP_DigestSign() failed");
+                goto fail;
+            }
+
+        } else {
+            ret = EVP_DigestVerifyInit(mctx, NULL, NULL, NULL,
+                                       key->u.a.pkey);
+            if (njs_slow_path(ret <= 0)) {
+                njs_webcrypto_error(vm,
+                                    "EVP_DigestVerifyInit() failed");
+                goto fail;
+            }
+
+            ret = EVP_DigestVerify(mctx, sig.start, sig.length,
+                                   data.start, data.length);
+            if (njs_slow_path(ret < 0)) {
+                njs_webcrypto_error(vm,
+                                    "EVP_DigestVerify() failed");
+                goto fail;
+            }
+        }
+
+        njs_evp_md_ctx_free(mctx);
+        mctx = NULL;
+
+        break;
+#endif
+
     case NJS_ALGORITHM_HMAC:
         m_len = EVP_MD_size(md);
 
@@ -4381,6 +4875,12 @@ njs_webcrypto_export_key_raw(njs_vm_t *vm, njs_webcrypto_key_t *key,
         case NJS_ALGORITHM_ECDH:
             return njs_export_jwk_asymmetric(vm, key, retval);
 
+#if (NJS_HAVE_ED25519)
+        case NJS_ALGORITHM_ED25519:
+        case NJS_ALGORITHM_X25519:
+            return njs_export_jwk_okp(vm, key, retval);
+#endif
+
         case NJS_ALGORITHM_AES_GCM:
         case NJS_ALGORITHM_AES_CTR:
         case NJS_ALGORITHM_AES_CBC:
@@ -4470,8 +4970,26 @@ njs_webcrypto_export_key_raw(njs_vm_t *vm, njs_webcrypto_key_t *key,
             return njs_export_raw_ec(vm, key, retval);
         }
 
-        return njs_vm_value_array_buffer_set(vm, retval,
-                                             key->u.s.raw.start,
+#if (NJS_HAVE_ED25519)
+        if (key->alg->type == NJS_ALGORITHM_ED25519
+            || key->alg->type == NJS_ALGORITHM_X25519)
+        {
+            size_t  raw_len;
+            u_char  raw_buf[32];
+
+            raw_len = 32;
+            if (EVP_PKEY_get_raw_public_key(key->u.a.pkey, raw_buf, &raw_len)
+                != 1)
+            {
+                njs_webcrypto_error(vm, "EVP_PKEY_get_raw_public_key() failed");
+                return NJS_ERROR;
+            }
+
+            return njs_webcrypto_array_buffer(vm, retval, raw_buf, raw_len);
+        }
+#endif
+
+        return njs_vm_value_array_buffer_set(vm, retval, key->u.s.raw.start,
                                              key->u.s.raw.length);
     }
 
@@ -4868,6 +5386,10 @@ njs_key_ext_algorithm(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
 
         break;
 
+    case NJS_ALGORITHM_ED25519:
+    case NJS_ALGORITHM_X25519:
+        break;
+
     case NJS_ALGORITHM_HMAC:
     default:
         /* HmacKeyGenParams */
index 443e68517e5ad11d8230e5c24468769495195b46..b4f97bcc1542126b26473b933d205701ac927533 100644 (file)
@@ -22,6 +22,7 @@ typedef enum {
     QJS_KEY_JWK_KTY_RSA,
     QJS_KEY_JWK_KTY_EC,
     QJS_KEY_JWK_KTY_OCT,
+    QJS_KEY_JWK_KTY_OKP,
     QJS_KEY_JWK_KTY_UNKNOWN,
 } qjs_webcrypto_jwk_kty_t;
 
@@ -51,6 +52,8 @@ typedef enum {
     QJS_ALGORITHM_AES_KW,
     QJS_ALGORITHM_ECDSA,
     QJS_ALGORITHM_ECDH,
+    QJS_ALGORITHM_ED25519,
+    QJS_ALGORITHM_X25519,
     QJS_ALGORITHM_PBKDF2,
     QJS_ALGORITHM_HKDF,
     QJS_ALGORITHM_MAX,
@@ -130,6 +133,12 @@ static JSValue qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv);
 static JSValue qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv);
+#if (NJS_HAVE_ED25519)
+static int qjs_webcrypto_generate_25519_keypair(JSContext *cx, int pkey_id,
+    qjs_webcrypto_key_t *wkey, qjs_webcrypto_algorithm_t *alg,
+    unsigned usage, int extractable, unsigned priv_usage,
+    unsigned pub_usage, JSValue key, JSValue *obj_ret);
+#endif
 static JSValue qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv);
 static JSValue qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val,
@@ -311,6 +320,34 @@ static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = {
                               0)
     },
 
+#if (NJS_HAVE_ED25519)
+    {
+      njs_str("Ed25519"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_ED25519,
+                              QJS_KEY_USAGE_SIGN |
+                              QJS_KEY_USAGE_VERIFY |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_PKCS8 |
+                              QJS_KEY_FORMAT_SPKI |
+                              QJS_KEY_FORMAT_RAW |
+                              QJS_KEY_FORMAT_JWK,
+                              0)
+    },
+
+    {
+      njs_str("X25519"),
+      qjs_webcrypto_algorithm(QJS_ALGORITHM_X25519,
+                              QJS_KEY_USAGE_DERIVE_KEY |
+                              QJS_KEY_USAGE_DERIVE_BITS |
+                              QJS_KEY_USAGE_GENERATE_KEY,
+                              QJS_KEY_FORMAT_PKCS8 |
+                              QJS_KEY_FORMAT_SPKI |
+                              QJS_KEY_FORMAT_RAW |
+                              QJS_KEY_FORMAT_JWK,
+                              0)
+    },
+#endif
+
     {
       njs_str("PBKDF2"),
       qjs_webcrypto_algorithm(QJS_ALGORITHM_PBKDF2,
@@ -366,6 +403,7 @@ static qjs_webcrypto_entry_t qjs_webcrypto_jwk_kty[] = {
     { njs_str("RSA"), QJS_KEY_JWK_KTY_RSA },
     { njs_str("EC"), QJS_KEY_JWK_KTY_EC },
     { njs_str("oct"), QJS_KEY_JWK_KTY_OCT },
+    { njs_str("OKP"), QJS_KEY_JWK_KTY_OKP },
     { njs_null_str, QJS_KEY_JWK_KTY_UNKNOWN }
 };
 
@@ -1733,6 +1771,98 @@ fail:
 }
 
 
+#if (NJS_HAVE_ED25519)
+static JSValue
+qjs_export_jwk_okp(JSContext *cx, qjs_webcrypto_key_t *key)
+{
+    size_t     len;
+    JSValue    jwk, ops;
+    njs_str_t  raw;
+    u_char     buf[64];
+
+    njs_assert(key->u.a.pkey != NULL);
+
+    len = 32;
+    if (EVP_PKEY_get_raw_public_key(key->u.a.pkey, buf, &len) != 1) {
+        qjs_webcrypto_error(cx,
+                            "EVP_PKEY_get_raw_public_key() failed");
+        return JS_EXCEPTION;
+    }
+
+    jwk = JS_NewObject(cx);
+    if (JS_IsException(jwk)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "OKP"),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "crv",
+                                  JS_NewString(cx,
+                                    key->alg->type == QJS_ALGORITHM_X25519
+                                    ? "X25519" : "Ed25519"),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        goto fail;
+    }
+
+    raw.start = buf;
+    raw.length = len;
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "x", qjs_string_base64url(cx, &raw),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        goto fail;
+    }
+
+    if (key->u.a.privat) {
+        len = 32;
+        if (EVP_PKEY_get_raw_private_key(key->u.a.pkey, buf, &len) != 1) {
+            qjs_webcrypto_error(cx, "EVP_PKEY_get_raw_private_key() failed");
+            goto fail;
+        }
+
+        raw.start = buf;
+        raw.length = len;
+
+        if (JS_DefinePropertyValueStr(cx, jwk, "d",
+                                      qjs_string_base64url(cx, &raw),
+                                      JS_PROP_C_W_E) < 0)
+        {
+            goto fail;
+        }
+    }
+
+    ops = qjs_key_ops(cx, key->usage);
+    if (JS_IsException(ops)) {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "key_ops", ops, JS_PROP_C_W_E) < 0) {
+        goto fail;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, jwk, "ext",
+                                  JS_NewBool(cx, key->extractable),
+                                  JS_PROP_C_W_E) < 0)
+    {
+        goto fail;
+    }
+
+    return jwk;
+
+fail:
+
+    JS_FreeValue(cx, jwk);
+
+    return JS_EXCEPTION;
+}
+#endif
+
+
 static JSValue
 qjs_export_raw_ec(JSContext *cx, qjs_webcrypto_key_t *key)
 {
@@ -1867,12 +1997,14 @@ qjs_derive_ecdh(JSContext *cx, JSValueConst *argv, int argc, int derive_key,
         goto fail;
     }
 
-    if (pkey->alg->type != QJS_ALGORITHM_ECDH) {
-        JS_ThrowTypeError(cx, "algorithm.public is not an ECDH key");
+    if (pkey->alg->type != key->alg->type) {
+        JS_ThrowTypeError(cx, "algorithm.public key type mismatch");
         goto fail;
     }
 
-    if (key->u.a.curve != pkey->u.a.curve) {
+    if (key->alg->type == QJS_ALGORITHM_ECDH
+        && key->u.a.curve != pkey->u.a.curve)
+    {
         JS_ThrowTypeError(cx, "ECDH keys must use the same curve");
         goto fail;
     }
@@ -2009,7 +2141,9 @@ qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc,
         return JS_EXCEPTION;
     }
 
-    if (alg->type == QJS_ALGORITHM_ECDH) {
+    if (alg->type == QJS_ALGORITHM_ECDH
+        || alg->type == QJS_ALGORITHM_X25519)
+    {
         return qjs_derive_ecdh(cx, argv, argc, derive_key, key);
     }
 
@@ -2337,6 +2471,12 @@ qjs_webcrypto_export_key_raw(JSContext *cx, qjs_webcrypto_key_t *key,
         case QJS_ALGORITHM_ECDH:
             return qjs_export_jwk_asymmetric(cx, key);
 
+#if (NJS_HAVE_ED25519)
+        case QJS_ALGORITHM_ED25519:
+        case QJS_ALGORITHM_X25519:
+            return qjs_export_jwk_okp(cx, key);
+#endif
+
         case QJS_ALGORITHM_AES_GCM:
         case QJS_ALGORITHM_AES_CTR:
         case QJS_ALGORITHM_AES_CBC:
@@ -2426,6 +2566,25 @@ qjs_webcrypto_export_key_raw(JSContext *cx, qjs_webcrypto_key_t *key,
             return qjs_export_raw_ec(cx, key);
         }
 
+#if (NJS_HAVE_ED25519)
+        if (key->alg->type == QJS_ALGORITHM_ED25519
+            || key->alg->type == QJS_ALGORITHM_X25519)
+        {
+            size_t  raw_len;
+            u_char  raw_buf[32];
+
+            raw_len = 32;
+            if (EVP_PKEY_get_raw_public_key(key->u.a.pkey, raw_buf, &raw_len)
+                != 1)
+            {
+                qjs_webcrypto_error(cx, "EVP_PKEY_get_raw_public_key() failed");
+                return JS_EXCEPTION;
+            }
+
+            return JS_NewArrayBufferCopy(cx, raw_buf, raw_len);
+        }
+#endif
+
         return JS_NewArrayBufferCopy(cx, key->u.s.raw.start,
                                      key->u.s.raw.length);
     }
@@ -2499,6 +2658,81 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc,
 }
 
 
+#if (NJS_HAVE_ED25519)
+static int
+qjs_webcrypto_generate_25519_keypair(JSContext *cx, int pkey_id,
+    qjs_webcrypto_key_t *wkey, qjs_webcrypto_algorithm_t *alg, unsigned usage,
+    int extractable, unsigned priv_usage, unsigned pub_usage, JSValue key,
+    JSValue *obj_ret)
+{
+    JSValue              keypub, obj;
+    EVP_PKEY_CTX         *ctx;
+    qjs_webcrypto_key_t  *wkeypub;
+
+    keypub = JS_UNDEFINED;
+
+    ctx = EVP_PKEY_CTX_new_id(pkey_id, NULL);
+    if (ctx == NULL) {
+        qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed");
+        return -1;
+    }
+
+    if (EVP_PKEY_keygen_init(ctx) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        qjs_webcrypto_error(cx, "EVP_PKEY_keygen_init() failed");
+        return -1;
+    }
+
+    if (EVP_PKEY_keygen(ctx, &wkey->u.a.pkey) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        qjs_webcrypto_error(cx, "EVP_PKEY_keygen() failed");
+        return -1;
+    }
+
+    EVP_PKEY_CTX_free(ctx);
+
+    wkey->u.a.privat = 1;
+    wkey->usage = priv_usage & usage;
+
+    keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable);
+    if (JS_IsException(keypub)) {
+        return -1;
+    }
+
+    if (njs_pkey_up_ref(wkey->u.a.pkey) <= 0) {
+        JS_FreeValue(cx, keypub);
+        qjs_webcrypto_error(cx, "njs_pkey_up_ref() failed");
+        return -1;
+    }
+
+    wkeypub = JS_GetOpaque(keypub, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
+    wkeypub->u.a.pkey = wkey->u.a.pkey;
+    wkeypub->usage = pub_usage & usage;
+
+    obj = JS_NewObject(cx);
+    if (JS_IsException(obj)) {
+        JS_FreeValue(cx, keypub);
+        return -1;
+    }
+
+    if (JS_SetPropertyStr(cx, obj, "privateKey", key) < 0) {
+        JS_FreeValue(cx, keypub);
+        JS_FreeValue(cx, obj);
+        return -1;
+    }
+
+    if (JS_SetPropertyStr(cx, obj, "publicKey", keypub) < 0) {
+        JS_FreeValue(cx, obj);
+        return -1;
+    }
+
+    *obj_ret = obj;
+
+    return 0;
+}
+#endif
+
+
 static JSValue
 qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv)
@@ -2711,6 +2945,36 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val,
 
         break;
 
+#if (NJS_HAVE_ED25519)
+    case QJS_ALGORITHM_ED25519:
+        n = qjs_webcrypto_generate_25519_keypair(cx, EVP_PKEY_ED25519,
+                                                 wkey, alg, usage,
+                                                 extractable,
+                                                 QJS_KEY_USAGE_SIGN,
+                                                 QJS_KEY_USAGE_VERIFY,
+                                                 key, &obj);
+        if (n < 0) {
+            goto fail;
+        }
+
+        key = JS_UNDEFINED;
+        break;
+
+    case QJS_ALGORITHM_X25519:
+        n = qjs_webcrypto_generate_25519_keypair(cx, EVP_PKEY_X25519,
+                                                 wkey, alg, usage,
+                                                 extractable,
+                                                 QJS_KEY_USAGE_DERIVE_KEY
+                                                 | QJS_KEY_USAGE_DERIVE_BITS,
+                                                 0, key, &obj);
+        if (n < 0) {
+            goto fail;
+        }
+
+        key = JS_UNDEFINED;
+        break;
+#endif
+
     case QJS_ALGORITHM_AES_GCM:
     case QJS_ALGORITHM_AES_CTR:
     case QJS_ALGORITHM_AES_CBC:
@@ -3333,6 +3597,109 @@ qjs_import_raw_ec(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key)
 }
 
 
+#if (NJS_HAVE_ED25519)
+static EVP_PKEY *
+qjs_import_jwk_okp(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
+{
+    int         pkey_id;
+    size_t      crv_len, x_len, d_len;
+    JSValue     val;
+    EVP_PKEY    *pkey;
+    njs_str_t   x_b64, d_b64, x_raw, d_raw;
+    const char  *crv_str, *x_str, *d_str;
+    u_char      x_buf[32], d_buf[32];
+
+    val = JS_GetPropertyStr(cx, jwk, "crv");
+    if (!JS_IsString(val)) {
+        JS_FreeValue(cx, val);
+        JS_ThrowTypeError(cx, "Invalid JWK OKP crv");
+        return NULL;
+    }
+
+    crv_str = JS_ToCStringLen(cx, &crv_len, val);
+    JS_FreeValue(cx, val);
+
+    if (crv_str == NULL) {
+        JS_ThrowTypeError(cx, "unsupported JWK OKP curve");
+        return NULL;
+    }
+
+    if (crv_len == 7 && memcmp(crv_str, "Ed25519", 7) == 0) {
+        pkey_id = EVP_PKEY_ED25519;
+
+    } else if (crv_len == 6 && memcmp(crv_str, "X25519", 6) == 0) {
+        pkey_id = EVP_PKEY_X25519;
+
+    } else {
+        JS_FreeCString(cx, crv_str);
+        JS_ThrowTypeError(cx, "unsupported JWK OKP curve");
+        return NULL;
+    }
+
+    JS_FreeCString(cx, crv_str);
+
+    val = JS_GetPropertyStr(cx, jwk, "x");
+    if (!JS_IsString(val)) {
+        JS_FreeValue(cx, val);
+        JS_ThrowTypeError(cx, "Invalid JWK OKP x");
+        return NULL;
+    }
+
+    x_str = JS_ToCStringLen(cx, &x_len, val);
+    JS_FreeValue(cx, val);
+
+    x_b64.start = (u_char *) x_str;
+    x_b64.length = x_len;
+    x_raw.start = x_buf;
+    x_raw.length = qjs_base64url_decode_length(cx, &x_b64);
+
+    if (x_raw.length != 32) {
+        JS_FreeCString(cx, x_str);
+        JS_ThrowTypeError(cx, "Invalid JWK OKP x length");
+        return NULL;
+    }
+
+    qjs_base64url_decode(cx, &x_b64, &x_raw);
+    JS_FreeCString(cx, x_str);
+
+    val = JS_GetPropertyStr(cx, jwk, "d");
+
+    if (JS_IsString(val)) {
+        d_str = JS_ToCStringLen(cx, &d_len, val);
+        JS_FreeValue(cx, val);
+
+        d_b64.start = (u_char *) d_str;
+        d_b64.length = d_len;
+        d_raw.start = d_buf;
+        d_raw.length = qjs_base64url_decode_length(cx, &d_b64);
+
+        if (d_raw.length != 32) {
+            JS_FreeCString(cx, d_str);
+            JS_ThrowTypeError(cx, "Invalid JWK OKP d length");
+            return NULL;
+        }
+
+        qjs_base64url_decode(cx, &d_b64, &d_raw);
+        JS_FreeCString(cx, d_str);
+
+        pkey = EVP_PKEY_new_raw_private_key(pkey_id, NULL, d_buf, 32);
+        key->u.a.privat = 1;
+
+    } else {
+        JS_FreeValue(cx, val);
+        pkey = EVP_PKEY_new_raw_public_key(pkey_id, NULL, x_buf, 32);
+    }
+
+    if (pkey == NULL) {
+        qjs_webcrypto_error(cx, "EVP_PKEY_new_raw_*_key() failed");
+        return NULL;
+    }
+
+    return pkey;
+}
+#endif
+
+
 static JSValue
 qjs_import_jwk_oct(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
 {
@@ -3652,7 +4019,6 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc,
             break;
 
         case QJS_KEY_JWK_KTY_OCT:
-        default:
             if (!alg->raw) {
                 JS_ThrowTypeError(cx, "JWK kty \"oct\" doesn't "
                                   "match algorithm \"%s\"",
@@ -3664,6 +4030,31 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc,
             if (JS_IsException(ret)) {
                 goto fail;
             }
+
+            break;
+
+#if (NJS_HAVE_ED25519)
+        case QJS_KEY_JWK_KTY_OKP:
+            if (alg->type != QJS_ALGORITHM_ED25519
+                && alg->type != QJS_ALGORITHM_X25519)
+            {
+                JS_ThrowTypeError(cx, "JWK kty \"OKP\" doesn't "
+                                  "match algorithm \"%s\"",
+                                  qjs_algorithm_string(alg));
+                goto fail;
+            }
+
+            pkey = qjs_import_jwk_okp(cx, jwk, wkey);
+            if (pkey == NULL) {
+                goto fail;
+            }
+
+            break;
+#endif
+
+        default:
+            JS_ThrowTypeError(cx, "unsupported JWK kty");
+            goto fail;
         }
 
         break;
@@ -3790,6 +4181,66 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc,
         wkey->u.a.pkey = pkey;
         break;
 
+#if (NJS_HAVE_ED25519)
+    case QJS_ALGORITHM_ED25519:
+        if (fmt == QJS_KEY_FORMAT_RAW) {
+            pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL,
+                                               key_data.start, key_data.length);
+            if (pkey == NULL) {
+                qjs_webcrypto_error(cx,
+                    "EVP_PKEY_new_raw_public_key() failed");
+                goto fail;
+            }
+        }
+
+        if (EVP_PKEY_id(pkey) != EVP_PKEY_ED25519) {
+            JS_ThrowTypeError(cx, "Ed25519 key is not found");
+            goto fail;
+        }
+
+        mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN : ~QJS_KEY_USAGE_VERIFY;
+
+        if (wkey->usage & mask) {
+            JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key",
+                              qjs_algorithm_string(alg));
+            goto fail;
+        }
+
+        wkey->u.a.pkey = pkey;
+        break;
+
+    case QJS_ALGORITHM_X25519:
+        if (fmt == QJS_KEY_FORMAT_RAW) {
+            pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL,
+                                               key_data.start, key_data.length);
+            if (pkey == NULL) {
+                qjs_webcrypto_error(cx,
+                    "EVP_PKEY_new_raw_public_key() failed");
+                goto fail;
+            }
+        }
+
+        if (EVP_PKEY_id(pkey) != EVP_PKEY_X25519) {
+            JS_ThrowTypeError(cx, "X25519 key is not found");
+            goto fail;
+        }
+
+        if (wkey->u.a.privat) {
+            mask = ~(QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS);
+        } else {
+            mask = 0;
+        }
+
+        if (wkey->usage & mask) {
+            JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key",
+                              qjs_algorithm_string(alg));
+            goto fail;
+        }
+
+        wkey->u.a.pkey = pkey;
+        break;
+#endif
+
     case QJS_ALGORITHM_HMAC:
         if (fmt == QJS_KEY_FORMAT_RAW) {
             ret = qjs_algorithm_hash(cx, options, &wkey->hash);
@@ -4156,23 +4607,88 @@ qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, int argc,
         }
     }
 
+    md = NULL;
+
     if (alg->type == QJS_ALGORITHM_ECDSA) {
         ret = qjs_algorithm_hash(cx, options, &hash);
         if (JS_IsException(ret)) {
             return JS_EXCEPTION;
         }
 
-    } else {
+        md = qjs_algorithm_hash_digest(hash);
+
+    } else if (alg->type != QJS_ALGORITHM_ED25519) {
         hash = key->hash;
+        md = qjs_algorithm_hash_digest(hash);
     }
 
-    md = qjs_algorithm_hash_digest(hash);
-
     /* Clang complains about uninitialized rc. */
     rc = 0;
     outlen = 0;
 
     switch (alg->type) {
+#if (NJS_HAVE_ED25519)
+    case QJS_ALGORITHM_ED25519:
+        mctx = njs_evp_md_ctx_new();
+        if (mctx == NULL) {
+            qjs_webcrypto_error(cx, "njs_evp_md_ctx_new() failed");
+            goto fail;
+        }
+
+        if (!verify) {
+            if (EVP_DigestSignInit(mctx, NULL, NULL, NULL,
+                                   key->u.a.pkey) <= 0)
+            {
+                qjs_webcrypto_error(cx,
+                                    "EVP_DigestSignInit() failed");
+                goto fail;
+            }
+
+            outlen = 0;
+            if (EVP_DigestSign(mctx, NULL, &outlen, data.start,
+                               data.length) <= 0)
+            {
+                qjs_webcrypto_error(cx, "EVP_DigestSign() failed");
+                goto fail;
+            }
+
+            dst = js_malloc(cx, outlen);
+            if (dst == NULL) {
+                JS_ThrowOutOfMemory(cx);
+                goto fail;
+            }
+
+            if (EVP_DigestSign(mctx, dst, &outlen, data.start,
+                               data.length) <= 0)
+            {
+                qjs_webcrypto_error(cx, "EVP_DigestSign() failed");
+                goto fail;
+            }
+
+        } else {
+            if (EVP_DigestVerifyInit(mctx, NULL, NULL, NULL,
+                                     key->u.a.pkey) <= 0)
+            {
+                qjs_webcrypto_error(cx,
+                                    "EVP_DigestVerifyInit() failed");
+                goto fail;
+            }
+
+            rc = EVP_DigestVerify(mctx, sig.start, sig.length,
+                                  data.start, data.length);
+            if (rc < 0) {
+                qjs_webcrypto_error(cx,
+                                    "EVP_DigestVerify() failed");
+                goto fail;
+            }
+        }
+
+        njs_evp_md_ctx_free(mctx);
+        mctx = NULL;
+
+        break;
+#endif
+
     case QJS_ALGORITHM_HMAC:
         m_len = EVP_MD_size(md);
 
@@ -4755,6 +5271,10 @@ qjs_webcrypto_key_algorithm(JSContext *cx, JSValueConst this_val)
 
         break;
 
+    case QJS_ALGORITHM_ED25519:
+    case QJS_ALGORITHM_X25519:
+        break;
+
     case QJS_ALGORITHM_HMAC:
     default:
          /* HmacKeyGenParams */
index be5e50b03b154dcf466032b32525688ee2df96f8..071027f3812a788b96bb1c61cdf79c212bfdf44e 100644 (file)
@@ -10,8 +10,8 @@ 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
+node: v25.2.1
+openssl: OpenSSL 3.0.13  30 Jan 2024
 
 Keys generation
 ===============
@@ -34,6 +34,24 @@ Generating EC PKCS8/SPKI key files
   openssl pkcs8 -inform PEM -in ec.pem -nocrypt -topk8 -outform PEM -out ec.pkcs8
   openssl ec -in ec.pkcs8 -pubout > ec.spki
 
+Generating Ed25519 PKCS8/SPKI key files
+---------------------------------------
+
+.. code-block:: shell
+
+  openssl genpkey -algorithm Ed25519 -out ed25519.pem
+  openssl pkcs8 -inform PEM -in ed25519.pem -nocrypt -topk8 -outform PEM -out ed25519.pkcs8
+  openssl pkey -in ed25519.pkcs8 -pubout > ed25519.spki
+
+Generating X25519 PKCS8/SPKI key files
+--------------------------------------
+
+.. code-block:: shell
+
+  openssl genpkey -algorithm X25519 -out x25519.pem
+  openssl pkcs8 -inform PEM -in x25519.pem -nocrypt -topk8 -outform PEM -out x25519.pkcs8
+  openssl pkey -in x25519.pkcs8 -pubout > x25519.spki
+
 Encoding
 ========
 
diff --git a/test/webcrypto/ed25519.t.mjs b/test/webcrypto/ed25519.t.mjs
new file mode 100644 (file)
index 0000000..fd8695e
--- /dev/null
@@ -0,0 +1,248 @@
+/*---
+includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, runTsuite.js, webCryptoUtils.js]
+flags: [async]
+---*/
+
+async function test(params) {
+    try {
+        await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
+    } catch (e) {
+        if (e.message.indexOf("Ed25519") !== -1) {
+            return 'SKIPPED';
+        }
+
+        throw e;
+    }
+
+    /* generateKey + sign/verify roundtrip */
+    if (params.generate) {
+        let kp = await crypto.subtle.generateKey("Ed25519", true,
+                                                  ["sign", "verify"]);
+
+        let data = new TextEncoder().encode(params.text || "test data");
+        let sig = await crypto.subtle.sign("Ed25519", kp.privateKey, data);
+
+        if (sig.byteLength !== 64) {
+            throw Error(`signature length ${sig.byteLength}, expected 64`);
+        }
+
+        let ok = await crypto.subtle.verify("Ed25519", kp.publicKey, sig,
+                                            data);
+        if (!ok) {
+            throw Error("verify failed for valid signature");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* raw export/import roundtrip */
+    if (params.raw_roundtrip) {
+        let kp = await crypto.subtle.generateKey("Ed25519", true,
+                                                  ["sign", "verify"]);
+
+        let data = new TextEncoder().encode("raw test");
+        let sig = await crypto.subtle.sign("Ed25519", kp.privateKey, data);
+
+        let raw = await crypto.subtle.exportKey("raw", kp.publicKey);
+        let imp = await crypto.subtle.importKey("raw", raw,
+            "Ed25519", true, ["verify"]);
+
+        let ok = await crypto.subtle.verify("Ed25519", imp, sig, data);
+        if (!ok) {
+            throw Error("verify failed with re-imported raw key");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* JWK export/import roundtrip */
+    if (params.jwk_roundtrip) {
+        let kp = await crypto.subtle.generateKey("Ed25519", true,
+                                                  ["sign", "verify"]);
+
+        let jwk = await crypto.subtle.exportKey("jwk", kp.privateKey);
+        if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !("d" in jwk)) {
+            throw Error(`bad JWK: ${JSON.stringify(jwk)}`);
+        }
+
+        let imp = await crypto.subtle.importKey("jwk", jwk,
+            "Ed25519", true, ["sign"]);
+
+        let data = new TextEncoder().encode("jwk test");
+        let sig = await crypto.subtle.sign("Ed25519", imp, data);
+        let ok = await crypto.subtle.verify("Ed25519", kp.publicKey,
+                                            sig, data);
+        if (!ok) {
+            throw Error("verify failed with re-imported JWK key");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* PKCS8/SPKI roundtrip */
+    if (params.pkcs8_roundtrip) {
+        let kp = await crypto.subtle.generateKey("Ed25519", true,
+                                                  ["sign", "verify"]);
+
+        let pkcs8 = await crypto.subtle.exportKey("pkcs8", kp.privateKey);
+        let spki = await crypto.subtle.exportKey("spki", kp.publicKey);
+
+        let priv = await crypto.subtle.importKey("pkcs8", pkcs8,
+            "Ed25519", true, ["sign"]);
+        let pub = await crypto.subtle.importKey("spki", spki,
+            "Ed25519", true, ["verify"]);
+
+        let data = new TextEncoder().encode("pkcs8 test");
+        let sig = await crypto.subtle.sign("Ed25519", priv, data);
+        let ok = await crypto.subtle.verify("Ed25519", pub, sig, data);
+        if (!ok) {
+            throw Error("verify failed with PKCS8/SPKI keys");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* verify with tampered data must return false */
+    if (params.verify_tampered_data) {
+        let kp = await crypto.subtle.generateKey("Ed25519", true,
+                                                  ["sign", "verify"]);
+
+        let data = new TextEncoder().encode("original");
+        let sig = await crypto.subtle.sign("Ed25519", kp.privateKey, data);
+
+        let tampered = new TextEncoder().encode("tampered");
+        let ok = await crypto.subtle.verify("Ed25519", kp.publicKey, sig,
+                                            tampered);
+        if (ok) {
+            throw Error("verify must fail for tampered data");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* verify with wrong key must return false */
+    if (params.verify_wrong_key) {
+        let kp1 = await crypto.subtle.generateKey("Ed25519", true,
+                                                    ["sign", "verify"]);
+        let kp2 = await crypto.subtle.generateKey("Ed25519", true,
+                                                    ["sign", "verify"]);
+
+        let data = new TextEncoder().encode("test");
+        let sig = await crypto.subtle.sign("Ed25519", kp1.privateKey, data);
+
+        let ok = await crypto.subtle.verify("Ed25519", kp2.publicKey, sig,
+                                            data);
+        if (ok) {
+            throw Error("verify must fail with wrong key");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* RFC 8032 test vector: sign-only (verify uses derived public key) */
+    if (params.rfc8032) {
+        let priv_jwk = {
+            kty: "OKP",
+            crv: "Ed25519",
+            d: params.d,
+            x: params.x,
+        };
+
+        let priv = await crypto.subtle.importKey("jwk", priv_jwk,
+            "Ed25519", false, ["sign"]);
+
+        let data = new Uint8Array(params.msg || []);
+        let sig = await crypto.subtle.sign("Ed25519", priv, data);
+
+        let expected = Buffer.from(params.expected, "hex");
+        if (Buffer.from(sig).compare(expected) !== 0) {
+            throw Error("RFC 8032 signature mismatch:\n"
+                + Buffer.from(sig).toString("hex") + "\n"
+                + "expected:\n" + params.expected);
+        }
+
+        return 'SUCCESS';
+    }
+
+    return 'SUCCESS';
+}
+
+let ed25519_tsuite = {
+    name: "Ed25519 sign/verify",
+    skip: () => (!has_buffer() || !has_webcrypto()),
+    T: test,
+    prepare_args: (args) => args,
+
+    tests: [
+        { generate: true },
+        { raw_roundtrip: true },
+        { jwk_roundtrip: true },
+        { pkcs8_roundtrip: true },
+
+        /* RFC 8032 Test 1: empty message */
+        { rfc8032: true,
+          d: "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
+          x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
+          msg: [],
+          expected: "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" },
+
+        /* RFC 8032 Test 2: 1-byte message 0x72 */
+        { rfc8032: true,
+          d: "TM0Imyj_ltqdtsNG7BFOD1uKMZ81q6Yk2oz27U-4pvs",
+          x: "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw",
+          msg: [0x72],
+          expected: "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00" },
+
+        { verify_tampered_data: true },
+        { verify_wrong_key: true },
+]};
+
+async function test_sign_with_x25519() {
+    try {
+        await crypto.subtle.generateKey("X25519", true, ["deriveBits"]);
+    } catch (e) {
+        if (e.message.indexOf("X25519") !== -1) {
+            return 'SKIPPED';
+        }
+
+        throw e;
+    }
+
+    let kp = await crypto.subtle.generateKey("X25519", true,
+                                              ["deriveBits"]);
+    let data = new TextEncoder().encode("test");
+    await crypto.subtle.sign("Ed25519", kp.privateKey, data);
+}
+
+async function test_derive_with_ed25519() {
+    try {
+        await crypto.subtle.generateKey("Ed25519", true,
+                                         ["sign", "verify"]);
+    } catch (e) {
+        if (e.message.indexOf("Ed25519") !== -1) {
+            return 'SKIPPED';
+        }
+
+        throw e;
+    }
+
+    let kp = await crypto.subtle.generateKey("Ed25519", true,
+                                              ["sign", "verify"]);
+    await crypto.subtle.deriveBits(
+        {name: "X25519", public: kp.publicKey},
+        kp.privateKey, 256);
+}
+
+let ed25519_error_tsuite = {
+    name: "Ed25519 errors",
+    skip: () => (!has_buffer() || !has_webcrypto()),
+    T: (params) => params.T(),
+    prepare_args: (args) => args,
+
+    tests: [
+        { T: test_sign_with_x25519, exception: true },
+        { T: test_derive_with_ed25519, exception: true },
+]};
+
+run([ed25519_tsuite, ed25519_error_tsuite])
+.then($DONE, $DONE);
diff --git a/test/webcrypto/x25519.t.mjs b/test/webcrypto/x25519.t.mjs
new file mode 100644 (file)
index 0000000..5489e4e
--- /dev/null
@@ -0,0 +1,263 @@
+/*---
+includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, runTsuite.js, webCryptoUtils.js]
+flags: [async]
+---*/
+
+async function import_key_pair(params) {
+    let pair = {};
+
+    pair.privateKey = await crypto.subtle.importKey("pkcs8",
+        Buffer.from(params.private_pkcs8, "base64url"),
+        "X25519", true, ["deriveBits", "deriveKey"]);
+
+    pair.publicKey = await crypto.subtle.importKey("spki",
+        Buffer.from(params.public_spki, "base64url"),
+        "X25519", true, []);
+
+    return pair;
+}
+
+
+async function test(params) {
+    try {
+        await crypto.subtle.generateKey("X25519", true, ["deriveBits"]);
+    } catch (e) {
+        if (e.message.indexOf("X25519") !== -1) {
+            return 'SKIPPED';
+        }
+
+        throw e;
+    }
+
+    /* generateKey + deriveBits roundtrip */
+    if (params.derive_bits) {
+        let alice = await crypto.subtle.generateKey("X25519", true,
+            ["deriveBits", "deriveKey"]);
+        let bob = await crypto.subtle.generateKey("X25519", true,
+            ["deriveBits", "deriveKey"]);
+
+        let s1 = await crypto.subtle.deriveBits(
+            {name: "X25519", public: bob.publicKey},
+            alice.privateKey, 256);
+        let s2 = await crypto.subtle.deriveBits(
+            {name: "X25519", public: alice.publicKey},
+            bob.privateKey, 256);
+
+        if (Buffer.from(s1).compare(Buffer.from(s2)) !== 0) {
+            throw Error("shared secrets do not match");
+        }
+
+        if (s1.byteLength !== 32) {
+            throw Error(`shared secret length: ${s1.byteLength}`);
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* deriveKey to AES-GCM */
+    if (params.derive_key) {
+        let alice = await crypto.subtle.generateKey("X25519", true,
+            ["deriveBits", "deriveKey"]);
+        let bob = await crypto.subtle.generateKey("X25519", true,
+            ["deriveBits", "deriveKey"]);
+
+        let aes = await crypto.subtle.deriveKey(
+            {name: "X25519", public: bob.publicKey},
+            alice.privateKey,
+            {name: "AES-GCM", length: 256},
+            true, ["encrypt", "decrypt"]);
+
+        let iv = crypto.getRandomValues(new Uint8Array(12));
+        let data = new TextEncoder().encode("test data");
+
+        let enc = await crypto.subtle.encrypt(
+            {name: "AES-GCM", iv: iv}, aes, data);
+        let dec = await crypto.subtle.decrypt(
+            {name: "AES-GCM", iv: iv}, aes, enc);
+
+        if (Buffer.from(dec).compare(Buffer.from(data)) !== 0) {
+            throw Error("deriveKey encrypt/decrypt failed");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* RFC 7748 vector: imported keys must derive the expected secret */
+    if (params.derive_bits_vector) {
+        let alice = await import_key_pair(params.alice);
+        let bob = await import_key_pair(params.bob);
+
+        let s1 = await crypto.subtle.deriveBits(
+            {name: "X25519", public: bob.publicKey},
+            alice.privateKey, 256);
+        let s2 = await crypto.subtle.deriveBits(
+            {name: "X25519", public: alice.publicKey},
+            bob.privateKey, 256);
+
+        s1 = Buffer.from(s1).toString("base64url");
+        s2 = Buffer.from(s2).toString("base64url");
+
+        if (s1 !== params.expected) {
+            throw Error(`shared secret mismatch: ${s1} != ${params.expected}`);
+        }
+
+        if (s2 !== params.expected) {
+            throw Error(`reverse shared secret mismatch: ${s2} != `
+                        + `${params.expected}`);
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* raw export/import roundtrip */
+    if (params.raw_roundtrip) {
+        let kp = await crypto.subtle.generateKey("X25519", true,
+            ["deriveBits", "deriveKey"]);
+
+        let raw = await crypto.subtle.exportKey("raw", kp.publicKey);
+        if (raw.byteLength !== 32) {
+            throw Error(`raw pub length: ${raw.byteLength}`);
+        }
+
+        let imported = await crypto.subtle.importKey("raw", raw,
+            "X25519", true, []);
+
+        let raw2 = await crypto.subtle.exportKey("raw", imported);
+        if (Buffer.from(raw).compare(Buffer.from(raw2)) !== 0) {
+            throw Error("raw roundtrip mismatch");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* JWK export/import roundtrip */
+    if (params.jwk_roundtrip) {
+        let kp = await crypto.subtle.generateKey("X25519", true,
+            ["deriveBits", "deriveKey"]);
+
+        let jwk = await crypto.subtle.exportKey("jwk", kp.privateKey);
+        if (jwk.kty !== "OKP" || jwk.crv !== "X25519") {
+            throw Error(`bad JWK: kty=${jwk.kty} crv=${jwk.crv}`);
+        }
+
+        if (!("d" in jwk)) {
+            throw Error("private JWK missing d");
+        }
+
+        let imported = await crypto.subtle.importKey("jwk", jwk,
+            "X25519", true, ["deriveBits", "deriveKey"]);
+
+        let jwk2 = await crypto.subtle.exportKey("jwk", imported);
+        if (jwk.d !== jwk2.d || jwk.x !== jwk2.x) {
+            throw Error("JWK roundtrip mismatch");
+        }
+
+        /* public JWK */
+        let pub_jwk = await crypto.subtle.exportKey("jwk", kp.publicKey);
+        if ("d" in pub_jwk) {
+            throw Error("public JWK should not have d");
+        }
+
+        return 'SUCCESS';
+    }
+
+    /* PKCS8/SPKI roundtrip */
+    if (params.pkcs8_roundtrip) {
+        let kp = await crypto.subtle.generateKey("X25519", true,
+            ["deriveBits", "deriveKey"]);
+
+        let pkcs8 = await crypto.subtle.exportKey("pkcs8", kp.privateKey);
+        let spki = await crypto.subtle.exportKey("spki", kp.publicKey);
+
+        let priv = await crypto.subtle.importKey("pkcs8", pkcs8,
+            "X25519", true, ["deriveBits"]);
+        let pub = await crypto.subtle.importKey("spki", spki,
+            "X25519", true, []);
+
+        let s = await crypto.subtle.deriveBits(
+            {name: "X25519", public: pub}, priv, 256);
+        if (s.byteLength !== 32) {
+            throw Error("PKCS8/SPKI derive failed");
+        }
+
+        return 'SUCCESS';
+    }
+
+    return 'SUCCESS';
+}
+
+let x25519_tsuite = {
+    name: "X25519 key agreement",
+    skip: () => (!has_buffer() || !has_webcrypto()),
+    T: test,
+    prepare_args: (args) => args,
+
+    tests: [
+        { derive_bits: true },
+        { derive_key: true },
+        { derive_bits_vector: true,
+          alice: {
+              private_pkcs8: "MC4CAQAwBQYDK2VuBCIEIHcHbQpzGKV9PBbBclGyZkXfTC-H68CZKrF3-6UduSwq",
+              public_spki: "MCowBQYDK2VuAyEAhSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo",
+          },
+          bob: {
+              private_pkcs8: "MC4CAQAwBQYDK2VuBCIEIF2rCH5iSopLeeF_i4OADuZvO7EpJhi2_Rwviyf_iODr",
+              public_spki: "MCowBQYDK2VuAyEA3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08",
+          },
+          expected: "Sl2dW6TOLeFyjjv0gDUPJeB-IclH0Z4zdvCbPB4WF0I" },
+        { raw_roundtrip: true },
+        { jwk_roundtrip: true },
+        { pkcs8_roundtrip: true },
+]};
+
+async function test_derive_with_ed25519_key() {
+    try {
+        await crypto.subtle.generateKey("Ed25519", true, ["sign"]);
+    } catch (e) {
+        if (e.message.indexOf("Ed25519") !== -1) {
+            return 'SKIPPED';
+        }
+
+        throw e;
+    }
+
+    let x_kp = await crypto.subtle.generateKey("X25519", true,
+                                                ["deriveBits"]);
+    let ed_kp = await crypto.subtle.generateKey("Ed25519", true,
+                                                 ["sign"]);
+
+    await crypto.subtle.deriveBits(
+        {name: "X25519", public: ed_kp.publicKey},
+        x_kp.privateKey, 256);
+}
+
+async function test_sign_with_x25519_key() {
+    try {
+        await crypto.subtle.generateKey("X25519", true, ["deriveBits"]);
+    } catch (e) {
+        if (e.message.indexOf("X25519") !== -1) {
+            return 'SKIPPED';
+        }
+
+        throw e;
+    }
+
+    let kp = await crypto.subtle.generateKey("X25519", true,
+                                              ["deriveBits"]);
+    let data = new TextEncoder().encode("test");
+    await crypto.subtle.sign("Ed25519", kp.privateKey, data);
+}
+
+let x25519_error_tsuite = {
+    name: "X25519 errors",
+    skip: () => (!has_buffer() || !has_webcrypto()),
+    T: (params) => params.T(),
+    prepare_args: (args) => args,
+
+    tests: [
+        { T: test_derive_with_ed25519_key, exception: true },
+        { T: test_sign_with_x25519_key, exception: true },
+]};
+
+run([x25519_tsuite, x25519_error_tsuite])
+.then($DONE, $DONE);
index f9bdbc98bf08df5689092b7d8ac898579b08efe6..0980d05fd75ee0270441531813465aecfdc2ac77 100644 (file)
@@ -69,7 +69,9 @@ type ImportAlgorithm =
     | AesVariants
     | "PBKDF2"
     | "HKDF"
-    | "ECDH";
+    | "ECDH"
+    | "Ed25519"
+    | "X25519";
 
 type GenerateAlgorithm =
     | RsaHashedKeyGenParams
@@ -80,7 +82,8 @@ type GenerateAlgorithm =
 type JWK =
     | { kty: "RSA"; }
     | { kty: "EC"; }
-    | { kty: "oct"; };
+    | { kty: "oct"; }
+    | { kty: "OKP"; };
 
 type KeyData =
     | NjsStringOrBuffer
@@ -105,10 +108,16 @@ interface   EcdhParams {
     public: CryptoKey;
 }
 
+interface   X25519Params {
+    name: "X25519";
+    public: CryptoKey;
+}
+
 type DeriveAlgorithm =
     | HkdfParams
     | Pbkdf2Params
-    | EcdhParams;
+    | EcdhParams
+    | X25519Params;
 
 interface   HmacKeyGenParams {
     name: "HMAC";
@@ -140,7 +149,8 @@ type SignOrVerifyAlgorithm =
     | { name: "HMAC"; }
     | { name: "RSASSA-PKCS1-v1_5"; }
     | "HMAC"
-    | "RSASSA-PKCS1-v1_5";
+    | "RSASSA-PKCS1-v1_5"
+    | "Ed25519";
 
 interface CryptoKey {
     /*
@@ -294,7 +304,8 @@ interface SubtleCrypto {
      *  Possible array values: "encrypt", "decrypt", "sign", "verify",
      *  "deriveKey", "deriveBits", "wrapKey", "unwrapKey".
      */
-    generateKey(algorithm: RsaHashedKeyGenParams | EcKeyGenParams,
+    generateKey(algorithm: RsaHashedKeyGenParams | EcKeyGenParams
+                         | "Ed25519" | "X25519",
                 extractable: boolean,
                 usage: Array<string>): Promise<CryptoKeyPair>;