From: Dmitry Volyntsev Date: Fri, 3 Apr 2026 07:00:58 +0000 (-0700) Subject: WebCrypto: added Ed25519 and X25519 support. X-Git-Tag: 0.9.7~3 X-Git-Url: http://www.kaiwu.me/postgresql/commit/static/gitweb.js?a=commitdiff_plain;h=744039e2ece0bab6dc7829a44b559d2177eaa4e8;p=njs.git WebCrypto: added Ed25519 and X25519 support. Implemented Ed25519 sign/verify/generateKey/importKey/exportKey Supports raw, PKCS8, SPKI, and JWK (OKP) key formats. Implemented X25519 deriveBits/deriveKey/generateKey/importKey/ exportKey. --- diff --git a/auto/openssl b/auto/openssl index bedd711b..b5f65a70 100644 --- a/auto/openssl +++ b/auto/openssl @@ -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 + + 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 diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index 1b67b2fe..a421dfac 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -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 */ diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index 443e6851..b4f97bcc 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -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 */ diff --git a/test/webcrypto/README.rst b/test/webcrypto/README.rst index be5e50b0..071027f3 100644 --- a/test/webcrypto/README.rst +++ b/test/webcrypto/README.rst @@ -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 index 00000000..fd8695e9 --- /dev/null +++ b/test/webcrypto/ed25519.t.mjs @@ -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 index 00000000..5489e4ec --- /dev/null +++ b/test/webcrypto/x25519.t.mjs @@ -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); diff --git a/ts/njs_webcrypto.d.ts b/ts/njs_webcrypto.d.ts index f9bdbc98..0980d05f 100644 --- a/ts/njs_webcrypto.d.ts +++ b/ts/njs_webcrypto.d.ts @@ -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): Promise;