From 1b69415d8c29bde08cc4c79dbb4b88827c55d8b9 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 8 May 2025 17:13:01 -0700 Subject: WebCrypto: added ECDH support. --- external/njs_webcrypto_module.c | 239 +++++++++++++++++++++++++++++++++--- external/qjs_webcrypto_module.c | 245 ++++++++++++++++++++++++++++++++++--- test/harness/webCryptoUtils.js | 16 +++ test/webcrypto/derive_ecdh.t.mjs | 188 ++++++++++++++++++++++++++++ test/webcrypto/ec2_secp384r1.pkcs8 | 6 + test/webcrypto/ec2_secp384r1.spki | 5 + test/webcrypto/ec2_secp521r1.pkcs8 | 8 ++ test/webcrypto/ec2_secp521r1.spki | 6 + test/webcrypto/ec_secp384r1.pkcs8 | 6 + test/webcrypto/ec_secp384r1.spki | 5 + test/webcrypto/ec_secp521r1.pkcs8 | 8 ++ test/webcrypto/ec_secp521r1.spki | 6 + test/webcrypto/export.t.mjs | 24 +++- 13 files changed, 728 insertions(+), 34 deletions(-) create mode 100644 test/webcrypto/derive_ecdh.t.mjs create mode 100644 test/webcrypto/ec2_secp384r1.pkcs8 create mode 100644 test/webcrypto/ec2_secp384r1.spki create mode 100644 test/webcrypto/ec2_secp521r1.pkcs8 create mode 100644 test/webcrypto/ec2_secp521r1.spki create mode 100644 test/webcrypto/ec_secp384r1.pkcs8 create mode 100644 test/webcrypto/ec_secp384r1.spki create mode 100644 test/webcrypto/ec_secp521r1.pkcs8 create mode 100644 test/webcrypto/ec_secp521r1.spki diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index 8cc172cc..d9b05d09 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -28,7 +28,6 @@ typedef enum { NJS_KEY_USAGE_SIGN = 1 << 6, NJS_KEY_USAGE_VERIFY = 1 << 7, NJS_KEY_USAGE_WRAP_KEY = 1 << 8, - NJS_KEY_USAGE_UNSUPPORTED = 1 << 9, NJS_KEY_USAGE_UNWRAP_KEY = 1 << 10, } njs_webcrypto_key_usage_t; @@ -281,9 +280,11 @@ static njs_webcrypto_entry_t njs_webcrypto_alg[] = { njs_webcrypto_algorithm(NJS_ALGORITHM_ECDH, NJS_KEY_USAGE_DERIVE_KEY | NJS_KEY_USAGE_DERIVE_BITS | - NJS_KEY_USAGE_GENERATE_KEY | - NJS_KEY_USAGE_UNSUPPORTED, - NJS_KEY_FORMAT_UNKNOWN, + NJS_KEY_USAGE_GENERATE_KEY, + NJS_KEY_FORMAT_PKCS8 | + NJS_KEY_FORMAT_SPKI | + NJS_KEY_FORMAT_RAW | + NJS_KEY_FORMAT_JWK, 0) }, @@ -1441,6 +1442,188 @@ fail: } +static njs_int_t +njs_ext_derive_ecdh(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t derive_key, njs_webcrypto_key_t *key, njs_value_t *retval) +{ + u_char *k; + size_t olen; + int64_t length; + unsigned usage; + EVP_PKEY *priv_pkey, *pub_pkey; + njs_int_t ret; + njs_value_t *value, *dobject; + EVP_PKEY_CTX *pctx; + njs_opaque_value_t lvalue; + njs_webcrypto_key_t *dkey, *pkey; + njs_webcrypto_algorithm_t *dalg; + + static const njs_str_t string_public = njs_str("public"); + + dobject = njs_arg(args, nargs, 3); + + if (derive_key) { + dalg = njs_key_algorithm(vm, dobject); + if (njs_slow_path(dalg == NULL)) { + goto fail; + } + + value = njs_vm_object_prop(vm, dobject, &string_length, &lvalue); + if (value == NULL) { + njs_vm_type_error(vm, "derivedKeyAlgorithm.length is not provided"); + goto fail; + } + + } else { + dalg = NULL; + value = dobject; + } + + ret = njs_value_to_integer(vm, value, &length); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + dkey = NULL; + length /= 8; + + if (derive_key) { + ret = njs_key_usage(vm, njs_arg(args, nargs, 5), &usage); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + if (njs_slow_path(usage & ~dalg->usage)) { + njs_vm_type_error(vm, "unsupported key usage for \"ECDH\" key"); + goto fail; + } + + dkey = njs_mp_zalloc(njs_vm_memory_pool(vm), + sizeof(njs_webcrypto_key_t)); + if (njs_slow_path(dkey == NULL)) { + njs_vm_memory_error(vm); + goto fail; + } + + dkey->alg = dalg; + dkey->usage = usage; + } + + value = njs_vm_object_prop(vm, njs_arg(args, nargs, 1), &string_public, + &lvalue); + if (value == NULL) { + njs_vm_type_error(vm, "ECDH algorithm.public is not provided"); + goto fail; + } + + pkey = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id, value); + if (njs_slow_path(pkey == NULL)) { + njs_vm_type_error(vm, "algorithm.public is not a CryptoKey object"); + goto fail; + } + + if (njs_slow_path(pkey->alg->type != NJS_ALGORITHM_ECDH)) { + njs_vm_type_error(vm, "algorithm.public is not an ECDH key"); + goto fail; + } + + if (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; + } + + if (!key->u.a.privat) { + njs_vm_type_error(vm, "baseKey must be a private key for ECDH"); + goto fail; + } + + if (pkey->u.a.privat) { + njs_vm_type_error(vm, "algorithm.public must be a public key"); + goto fail; + } + + priv_pkey = key->u.a.pkey; + pub_pkey = pkey->u.a.pkey; + + pctx = EVP_PKEY_CTX_new(priv_pkey, NULL); + if (njs_slow_path(pctx == NULL)) { + njs_webcrypto_error(vm, "EVP_PKEY_CTX_new() failed"); + goto fail; + } + + if (EVP_PKEY_derive_init(pctx) != 1) { + njs_webcrypto_error(vm, "EVP_PKEY_derive_init() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (EVP_PKEY_derive_set_peer(pctx, pub_pkey) != 1) { + njs_webcrypto_error(vm, "EVP_PKEY_derive_set_peer() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + olen = (size_t) length; + if (EVP_PKEY_derive(pctx, NULL, &olen) != 1) { + njs_webcrypto_error(vm, "EVP_PKEY_derive() failed (size query)"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (njs_slow_path(olen < (size_t) length)) { + njs_vm_type_error(vm, "derived bit length is too small"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + k = njs_mp_alloc(njs_vm_memory_pool(vm), olen); + if (njs_slow_path(k == NULL)) { + njs_vm_memory_error(vm); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (EVP_PKEY_derive(pctx, k, &olen) != 1) { + njs_webcrypto_error(vm, "EVP_PKEY_derive() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + EVP_PKEY_CTX_free(pctx); + + if (derive_key) { + if (dalg->type == NJS_ALGORITHM_HMAC) { + ret = njs_algorithm_hash(vm, dobject, &dkey->hash); + if (njs_slow_path(ret == NJS_ERROR)) { + goto fail; + } + } + + dkey->extractable = njs_value_bool(njs_arg(args, nargs, 4)); + + dkey->u.s.raw.start = k; + dkey->u.s.raw.length = length; + + ret = njs_vm_external_create(vm, njs_value_arg(&lvalue), + njs_webcrypto_crypto_key_proto_id, + dkey, 0); + } else { + ret = njs_vm_value_array_buffer_set(vm, njs_value_arg(&lvalue), k, + length); + } + + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + return njs_webcrypto_result(vm, &lvalue, NJS_OK, retval); + +fail: + + return njs_webcrypto_result(vm, NULL, NJS_ERROR, retval); +} + + static njs_int_t njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t derive_key, njs_value_t *retval) @@ -1454,8 +1637,8 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_value_t *value, *aobject, *dobject; const EVP_MD *md; EVP_PKEY_CTX *pctx; - njs_webcrypto_key_t *key, *dkey; njs_opaque_value_t lvalue; + njs_webcrypto_key_t *key, *dkey; njs_webcrypto_hash_t hash; njs_webcrypto_algorithm_t *alg, *dalg; @@ -1491,6 +1674,10 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } + if (alg->type == NJS_ALGORITHM_ECDH) { + return njs_ext_derive_ecdh(vm, args, nargs, derive_key, key, retval); + } + dobject = njs_arg(args, nargs, 3); if (derive_key) { @@ -1707,7 +1894,6 @@ free: (void) &info; #endif - case NJS_ALGORITHM_ECDH: default: njs_vm_internal_error(vm, "not implemented deriveKey " "algorithm: \"%V\"", njs_algorithm_string(alg)); @@ -2270,6 +2456,7 @@ njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NJS_ALGORITHM_RSA_PSS: case NJS_ALGORITHM_RSA_OAEP: case NJS_ALGORITHM_ECDSA: + case NJS_ALGORITHM_ECDH: ret = njs_export_jwk_asymmetric(vm, key, njs_value_arg(&value)); if (njs_slow_path(ret != NJS_OK)) { goto fail; @@ -2373,7 +2560,9 @@ njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NJS_KEY_FORMAT_RAW: default: - if (key->alg->type == NJS_ALGORITHM_ECDSA) { + if (key->alg->type == NJS_ALGORITHM_ECDSA + || key->alg->type == NJS_ALGORITHM_ECDH) + { ret = njs_export_raw_ec(vm, key, njs_value_arg(&value)); if (njs_slow_path(ret != NJS_OK)) { goto fail; @@ -2540,6 +2729,7 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, break; case NJS_ALGORITHM_ECDSA: + case NJS_ALGORITHM_ECDH: nid = 0; ret = njs_algorithm_curve(vm, aobject, &nid); if (njs_slow_path(ret == NJS_ERROR)) { @@ -2572,7 +2762,14 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, ctx = NULL; key->u.a.privat = 1; - key->usage = NJS_KEY_USAGE_SIGN; + + if (alg->type == NJS_ALGORITHM_ECDSA) { + key->usage = NJS_KEY_USAGE_SIGN; + + } else { + /* ECDH */ + key->usage = NJS_KEY_USAGE_DERIVE_KEY | NJS_KEY_USAGE_DERIVE_BITS; + } keypub = njs_webcrypto_key_alloc(vm, alg, usage, extractable); if (njs_slow_path(keypub == NULL)) { @@ -2586,7 +2783,15 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, keypub->u.a.pkey = key->u.a.pkey; keypub->u.a.curve = key->u.a.curve; - keypub->usage = NJS_KEY_USAGE_VERIFY; + + if (alg->type == NJS_ALGORITHM_ECDSA) { + keypub->usage = NJS_KEY_USAGE_VERIFY; + + } else { + /* ECDH */ + keypub->usage = NJS_KEY_USAGE_DERIVE_KEY + | NJS_KEY_USAGE_DERIVE_BITS; + } ret = njs_vm_external_create(vm, njs_value_arg(&priv), njs_webcrypto_crypto_key_proto_id, key, 0); @@ -3565,7 +3770,17 @@ njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } - mask = key->u.a.privat ? ~NJS_KEY_USAGE_SIGN : ~NJS_KEY_USAGE_VERIFY; + if (alg->type == NJS_ALGORITHM_ECDSA) { + mask = key->u.a.privat ? ~NJS_KEY_USAGE_SIGN + : ~NJS_KEY_USAGE_VERIFY; + } else { + 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", @@ -4598,10 +4813,6 @@ njs_key_algorithm(njs_vm_t *vm, njs_value_t *options) for (e = &njs_webcrypto_alg[0]; e->name.length != 0; e++) { if (njs_strstr_case_eq(&a, &e->name)) { alg = (njs_webcrypto_algorithm_t *) e->value; - if (alg->usage & NJS_KEY_USAGE_UNSUPPORTED) { - njs_vm_type_error(vm, "unsupported algorithm: \"%V\"", &a); - return NULL; - } return alg; } diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index f26b6505..b9c645d9 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -115,6 +115,8 @@ static JSValue qjs_cipher_aes_ctr(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key, JSValue options, int encrypt); static JSValue qjs_cipher_aes_cbc(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key, JSValue options, int encrypt); +static JSValue qjs_derive_ecdh(JSContext *cx, JSValueConst *argv, int argc, + int derive_key, qjs_webcrypto_key_t *key); static JSValue qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv, int derive_key); static JSValue qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val, @@ -272,9 +274,11 @@ static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = { qjs_webcrypto_algorithm(QJS_ALGORITHM_ECDH, QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS | - QJS_KEY_USAGE_GENERATE_KEY | - QJS_KEY_USAGE_UNSUPPORTED, - QJS_KEY_FORMAT_UNKNOWN, + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_PKCS8 | + QJS_KEY_FORMAT_SPKI | + QJS_KEY_FORMAT_RAW | + QJS_KEY_FORMAT_JWK, 0) }, @@ -430,7 +434,7 @@ static const JSCFunctionListEntry qjs_webcrypto_subtle[] = { JS_CFUNC_DEF("importKey", 5, qjs_webcrypto_import_key), JS_CFUNC_MAGIC_DEF("decrypt", 4, qjs_webcrypto_cipher, 0), JS_CFUNC_MAGIC_DEF("deriveBits", 4, qjs_webcrypto_derive, 0), - JS_CFUNC_MAGIC_DEF("deriveKey", 4, qjs_webcrypto_derive, 1), + JS_CFUNC_MAGIC_DEF("deriveKey", 5, qjs_webcrypto_derive, 1), JS_CFUNC_DEF("digest", 3, qjs_webcrypto_digest), JS_CFUNC_MAGIC_DEF("encrypt", 4, qjs_webcrypto_cipher, 1), JS_CFUNC_DEF("exportKey", 3, qjs_webcrypto_export_key), @@ -1664,6 +1668,190 @@ qjs_export_raw_ec(JSContext *cx, qjs_webcrypto_key_t *key) } +static JSValue +qjs_derive_ecdh(JSContext *cx, JSValueConst *argv, int argc, int derive_key, + qjs_webcrypto_key_t *key) +{ + u_char *k; + size_t olen; + int64_t length; + JSValue ret, result, value, dobject; + unsigned usage; + EVP_PKEY *priv_pkey, *pub_pkey; + EVP_PKEY_CTX *pctx; + qjs_webcrypto_key_t *dkey, *pkey; + qjs_webcrypto_algorithm_t *dalg; + + result = JS_UNDEFINED; + dobject = argv[2]; + + if (derive_key) { + dalg = qjs_key_algorithm(cx, dobject); + if (dalg == NULL) { + goto fail; + } + + value = JS_GetPropertyStr(cx, dobject, "length"); + if (JS_IsException(value)) { + goto fail; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "derivedKeyAlgorithm.length is not provided"); + JS_FreeValue(cx, value); + goto fail; + } + + } else { + dalg = NULL; + value = JS_DupValue(cx, dobject); + } + + if (JS_ToInt64(cx, &length, value) < 0) { + JS_FreeValue(cx, value); + goto fail; + } + + JS_FreeValue(cx, value); + + dkey = NULL; + length /= 8; + + if (derive_key) { + ret = qjs_key_usage(cx, argv[4], &usage); + if (JS_IsException(ret)) { + goto fail; + } + + if (usage & ~dalg->usage) { + JS_ThrowTypeError(cx, "unsupported key usage for \"ECDH\" key"); + goto fail; + } + + result = qjs_webcrypto_key_make(cx, dalg, usage, 0); + if (JS_IsException(result)) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + dkey = JS_GetOpaque(result, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + } + + value = JS_GetPropertyStr(cx, argv[0], "public"); + if (JS_IsException(value)) { + goto fail; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "ECDH algorithm.public is not provided"); + JS_FreeValue(cx, value); + goto fail; + } + + pkey = JS_GetOpaque(value, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + JS_FreeValue(cx, value); + if (pkey == NULL) { + JS_ThrowTypeError(cx, "algorithm.public is not a CryptoKey object"); + goto fail; + } + + if (pkey->alg->type != QJS_ALGORITHM_ECDH) { + JS_ThrowTypeError(cx, "algorithm.public is not an ECDH key"); + goto fail; + } + + if (key->u.a.curve != pkey->u.a.curve) { + JS_ThrowTypeError(cx, "ECDH keys must use the same curve"); + goto fail; + } + + if (!key->u.a.privat) { + JS_ThrowTypeError(cx, "baseKey must be a private key for ECDH"); + goto fail; + } + + if (pkey->u.a.privat) { + JS_ThrowTypeError(cx, "algorithm.public must be a public key"); + goto fail; + } + + priv_pkey = key->u.a.pkey; + pub_pkey = pkey->u.a.pkey; + + pctx = EVP_PKEY_CTX_new(priv_pkey, NULL); + if (pctx == NULL) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed"); + goto fail; + } + + if (EVP_PKEY_derive_init(pctx) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive_init() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (EVP_PKEY_derive_set_peer(pctx, pub_pkey) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive_set_peer() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + olen = (size_t) length; + if (EVP_PKEY_derive(pctx, NULL, &olen) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed (size query)"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (olen < (size_t) length) { + JS_ThrowTypeError(cx, "derived bit length is too small"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + k = js_malloc(cx, olen); + if (k == NULL) { + JS_ThrowOutOfMemory(cx); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (EVP_PKEY_derive(pctx, k, &olen) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed"); + EVP_PKEY_CTX_free(pctx); + js_free(cx, k); + goto fail; + } + + EVP_PKEY_CTX_free(pctx); + + if (derive_key) { + if (dalg->type == QJS_ALGORITHM_HMAC) { + ret = qjs_algorithm_hash(cx, dobject, &dkey->hash); + if (JS_IsException(ret)) { + js_free(cx, k); + goto fail; + } + } + + dkey->extractable = JS_ToBool(cx, argv[3]); + + dkey->u.s.raw.start = k; + dkey->u.s.raw.length = length; + + } else { + result = qjs_new_array_buffer(cx, k, length); + } + + return qjs_promise_result(cx, result); + +fail: + JS_FreeValue(cx, result); + + return qjs_promise_result(cx, JS_EXCEPTION); +} + + static JSValue qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv, int derive_key) @@ -1709,6 +1897,10 @@ qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } + if (alg->type == QJS_ALGORITHM_ECDH) { + return qjs_derive_ecdh(cx, argv, argc, derive_key, key); + } + dobject = argv[2]; if (derive_key) { @@ -1933,7 +2125,6 @@ free: (void) &info; #endif - case QJS_ALGORITHM_ECDH: default: JS_ThrowTypeError(cx, "not implemented deriveKey algorithm: \"%s\"", qjs_algorithm_string(alg)); @@ -2051,6 +2242,7 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, case QJS_ALGORITHM_RSA_PSS: case QJS_ALGORITHM_RSA_OAEP: case QJS_ALGORITHM_ECDSA: + case QJS_ALGORITHM_ECDH: ret = qjs_export_jwk_asymmetric(cx, key); if (JS_IsException(ret)) { goto fail; @@ -2156,7 +2348,9 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, case QJS_KEY_FORMAT_RAW: default: - if (key->alg->type == QJS_ALGORITHM_ECDSA) { + if (key->alg->type == QJS_ALGORITHM_ECDSA + || key->alg->type == QJS_ALGORITHM_ECDH) + { ret = qjs_export_raw_ec(cx, key); if (JS_IsException(ret)) { goto fail; @@ -2311,6 +2505,7 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, break; case QJS_ALGORITHM_ECDSA: + case QJS_ALGORITHM_ECDH: ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve); if (JS_IsException(ret)) { goto fail; @@ -2342,7 +2537,14 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, ctx = NULL; wkey->u.a.privat = 1; - wkey->usage = QJS_KEY_USAGE_SIGN; + + if (alg->type == QJS_ALGORITHM_ECDSA) { + wkey->usage = QJS_KEY_USAGE_SIGN; + + } else { + /* ECDH */ + wkey->usage = QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS; + } keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable); if (JS_IsException(keypub)) { @@ -2358,7 +2560,15 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, wkeypub->u.a.pkey = wkey->u.a.pkey; wkeypub->u.a.curve = wkey->u.a.curve; - wkeypub->usage = QJS_KEY_USAGE_VERIFY; + + if (alg->type == QJS_ALGORITHM_ECDSA) { + wkeypub->usage = QJS_KEY_USAGE_VERIFY; + + } else { + /* ECDH */ + wkeypub->usage = QJS_KEY_USAGE_DERIVE_KEY + | QJS_KEY_USAGE_DERIVE_BITS; + } obj = JS_NewObject(cx); if (JS_IsException(obj)) { @@ -3408,7 +3618,17 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc, goto fail; } - mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN : ~QJS_KEY_USAGE_VERIFY; + if (alg->type == QJS_ALGORITHM_ECDSA) { + mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN + : ~QJS_KEY_USAGE_VERIFY; + } else { + 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", @@ -4369,13 +4589,6 @@ qjs_key_algorithm(JSContext *cx, JSValue options) for (e = &qjs_webcrypto_alg[0]; e->name.length != 0; e++) { if (njs_strstr_case_eq(&a, &e->name)) { alg = (qjs_webcrypto_algorithm_t *) e->value; - if (alg->usage & QJS_KEY_USAGE_UNSUPPORTED) { - JS_ThrowTypeError(cx, "unsupported algorithm: \"%.*s\"", - (int) a.length, a.start); - JS_FreeCString(cx, (char *) a.start); - return NULL; - } - JS_FreeCString(cx, (char *) a.start); return alg; } diff --git a/test/harness/webCryptoUtils.js b/test/harness/webCryptoUtils.js index d403f39a..c37fb489 100644 --- a/test/harness/webCryptoUtils.js +++ b/test/harness/webCryptoUtils.js @@ -19,3 +19,19 @@ function load_jwk(data) { return data; } + +function compareUsage(a, b) { + a.sort(); + b.sort(); + + if (b.length !== a.length) { + return false; + } + + for (var i = 0; i < a.length; i++) { + if (b[i] !== a[i]) { + return false; + } + } + return true; +} diff --git a/test/webcrypto/derive_ecdh.t.mjs b/test/webcrypto/derive_ecdh.t.mjs new file mode 100644 index 00000000..50c1925e --- /dev/null +++ b/test/webcrypto/derive_ecdh.t.mjs @@ -0,0 +1,188 @@ +/*--- +includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, webCryptoUtils.js, runTsuite.js] +flags: [async] +---*/ + +async function testDeriveBits(params) { + let aliceKeyPair = await load_key_pair(params.pair[0]); + let bobKeyPair = await load_key_pair(params.pair[1]); + + let ecdhParams = { name: "ECDH", public: bobKeyPair.publicKey }; + + let result = await crypto.subtle.deriveBits(ecdhParams, aliceKeyPair.privateKey, params.length); + result = Buffer.from(result).toString('base64url'); + + if (result !== params.expected) { + throw Error(`ECDH deriveBits failed expected: "${params.expected}" vs "${result}"`); + } + + let ecdhParamsReverse = { name: "ECDH", public: aliceKeyPair.publicKey }; + + let secondResult = await crypto.subtle.deriveBits(ecdhParamsReverse, bobKeyPair.privateKey, params.length); + secondResult = Buffer.from(secondResult).toString('base64url'); + + if (secondResult !== params.expected) { + throw Error(`ECDH reverse deriveBits failed expected: "${params.expected}" vs "${secondResult}"`); + } + + return "SUCCESS"; +} + +function deriveCurveFromName(name) { + if (/secp384r1/.test(name)) { + return "P-384"; + } + + if (/secp521r1/.test(name)) { + return "P-521"; + } + + return "P-256"; +} + +async function load_key_pair(name) { + let pair = {}; + let pem = fs.readFileSync(`test/webcrypto/${name}.pkcs8`); + let key = pem_to_der(pem, "private"); + + pair.privateKey = await crypto.subtle.importKey("pkcs8", key, + { name: "ECDH", namedCurve: deriveCurveFromName(name) }, + true, ["deriveBits", "deriveKey"]); + + pem = fs.readFileSync(`test/webcrypto/${name}.spki`); + key = pem_to_der(pem, "public"); + pair.publicKey = await crypto.subtle.importKey("spki", key, + { name: "ECDH", namedCurve: deriveCurveFromName(name) }, + true, []); + + return pair; +} + +let ecdh_bits_tsuite = { + name: "ECDH-DeriveBits", + skip: () => (!has_buffer() || !has_webcrypto()), + T: testDeriveBits, + opts: { + pair: ['ec', 'ec2'], + length: 256 + }, + tests: [ + { expected: "mMAGhQ_1Wr3u6Y6VyzVuolCA7x8RM-15e73laLJMUok" }, + { pair: ['ec_secp384r1', 'ec2_secp384r1'], + expected: "4OmeRzZZ53eCgn09zI2TumH4n4Zp-nfHsfZTOBEu8Hg" }, + { pair: ['ec_secp384r1', 'ec2_secp384r1'], + length: 384, + expected: "4OmeRzZZ53eCgn09zI2TumH4n4Zp-nfHsfZTOBEu8HjB0GF2YrOw5dCUgavKZaNR" }, + { pair: ['ec_secp521r1', 'ec2_secp521r1'], + length: 528, + expected: "ATBls20ukLQI7AJQ6LRnyD6wLDR_FDmBoAdVX5_DB_bMDe_uYMjN-jQqPTkGNIo6NOqmXMX9KNQ-AqL8aPjySMMm" }, + ] +}; + +async function testDeriveKey(params) { + let aliceKeyPair = await load_key_pair(params.pair[0]); + let bobKeyPair = await load_key_pair(params.pair[1]); + let eveKeyPair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: deriveCurveFromName(params.pair[0]) }, + true, ["deriveKey", "deriveBits"]); + + let ecdhParamsAlice = { name: "ECDH", public: bobKeyPair.publicKey }; + let ecdhParamsBob = { name: "ECDH", public: aliceKeyPair.publicKey }; + let ecdhParamsEve = { name: "ECDH", public: eveKeyPair.publicKey }; + + let derivedAlgorithm = { name: params.derivedAlgorithm.name }; + + derivedAlgorithm.length = params.derivedAlgorithm.length; + derivedAlgorithm.hash = params.derivedAlgorithm.hash; + + let aliceDerivedKey = await crypto.subtle.deriveKey(ecdhParamsAlice, aliceKeyPair.privateKey, + derivedAlgorithm, params.extractable, params.usage); + + if (aliceDerivedKey.extractable !== params.extractable) { + throw Error(`ECDH extractable test failed: ${params.extractable} vs ${aliceDerivedKey.extractable}`); + } + + if (compareUsage(aliceDerivedKey.usages, params.usage) !== true) { + throw Error(`ECDH usage test failed: ${params.usage} vs ${aliceDerivedKey.usages}`); + } + + let bobDerivedKey = await crypto.subtle.deriveKey(ecdhParamsBob, bobKeyPair.privateKey, + derivedAlgorithm, params.extractable, params.usage); + + let eveDerivedKey = await crypto.subtle.deriveKey(ecdhParamsEve, eveKeyPair.privateKey, + derivedAlgorithm, params.extractable, params.usage); + + if (params.extractable && + (params.derivedAlgorithm.name === "AES-GCM" + || params.derivedAlgorithm.name === "AES-CBC" + || params.derivedAlgorithm.name === "AES-CTR" + || params.derivedAlgorithm.name === "HMAC")) + { + const aliceRawKey = await crypto.subtle.exportKey("raw", aliceDerivedKey); + const bobRawKey = await crypto.subtle.exportKey("raw", bobDerivedKey); + const eveRawKey = await crypto.subtle.exportKey("raw", eveDerivedKey); + + const aliceKeyData = Buffer.from(aliceRawKey).toString("base64url"); + const bobKeyData = Buffer.from(bobRawKey).toString("base64url"); + const eveKeyData = Buffer.from(eveRawKey).toString("base64url"); + + if (aliceKeyData !== bobKeyData) { + throw Error(`ECDH key symmetry test failed: keys are not equal`); + } + + if (aliceKeyData !== params.expected) { + throw Error(`ECDH key symmetry test failed: expected: "${params.expected}" vs "${aliceKeyData}"`); + } + + if (aliceKeyData === eveKeyData) { + throw Error(`ECDH key symmetry test failed: keys are equal`); + } + } + + return "SUCCESS"; +} + +let ecdh_key_tsuite = { + name: "ECDH-DeriveKey", + skip: () => (!has_buffer() || !has_webcrypto()), + T: testDeriveKey, + opts: { + pair: ['ec', 'ec2'], + extractable: true, + derivedAlgorithm: { + name: "AES-GCM", + length: 256 + }, + expected: "mMAGhQ_1Wr3u6Y6VyzVuolCA7x8RM-15e73laLJMUok", + usage: ["encrypt", "decrypt"] + }, + tests: [ + { }, + { extractable: false }, + { derivedAlgorithm: { name: "AES-CBC", length: 256 } }, + { derivedAlgorithm: { name: "AES-CTR", length: 256 } }, + { derivedAlgorithm: { name: "AES-GCM", length: 256 } }, + { derivedAlgorithm: { name: "HMAC", hash: "SHA-256", length: 256 }, + usage: ["sign", "verify"] }, + + { pair: ['ec_secp384r1', 'ec2_secp384r1'], + expected: "4OmeRzZZ53eCgn09zI2TumH4n4Zp-nfHsfZTOBEu8Hg" }, + { pair: ['ec_secp384r1', 'ec2_secp384r1'], extractable: false }, + + { pair: ['ec_secp521r1', 'ec_secp384r1'], + exception: "TypeError: ECDH keys must use the same curve" }, + + { pair: ['ec_secp521r1', 'ec2_secp521r1'], + derivedAlgorithm: { name: "AES-GCM", length: 128 }, + expected: "ATBls20ukLQI7AJQ6LRnyA" }, + { pair: ['ec_secp521r1', 'ec2_secp521r1'], + derivedAlgorithm: { name: "HMAC", hash: "SHA-384", length: 512 }, + expected: "ATBls20ukLQI7AJQ6LRnyD6wLDR_FDmBoAdVX5_DB_bMDe_uYMjN-jQqPTkGNIo6NOqmXMX9KNQ-AqL8aPjySA", + usage: ["sign", "verify"] } + ] +}; + +run([ + ecdh_bits_tsuite, + ecdh_key_tsuite, +]) +.then($DONE, $DONE); diff --git a/test/webcrypto/ec2_secp384r1.pkcs8 b/test/webcrypto/ec2_secp384r1.pkcs8 new file mode 100644 index 00000000..c94fdf9f --- /dev/null +++ b/test/webcrypto/ec2_secp384r1.pkcs8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDM3LQ/iPLxtGh4I0IH +tkE14NPOSBWWxa0C5Dt7KFA2T5Goh/C9hulx4waSJtJgpsmhZANiAAQr0yzNebjy +oATeQbsSQGcBgC6Vm31MqarylkteLBxC+tWVgrCjxps/ZN9l+wOBo6kceuGrmoi6 +YJYkRAZk9QOCODFou+VyW741sQRtenfkCb904Iy83tLXw9CCOZ3M5tM= +-----END PRIVATE KEY----- diff --git a/test/webcrypto/ec2_secp384r1.spki b/test/webcrypto/ec2_secp384r1.spki new file mode 100644 index 00000000..52379e77 --- /dev/null +++ b/test/webcrypto/ec2_secp384r1.spki @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEK9MszXm48qAE3kG7EkBnAYAulZt9TKmq +8pZLXiwcQvrVlYKwo8abP2TfZfsDgaOpHHrhq5qIumCWJEQGZPUDgjgxaLvlclu+ +NbEEbXp35Am/dOCMvN7S18PQgjmdzObT +-----END PUBLIC KEY----- diff --git a/test/webcrypto/ec2_secp521r1.pkcs8 b/test/webcrypto/ec2_secp521r1.pkcs8 new file mode 100644 index 00000000..8d6000cf --- /dev/null +++ b/test/webcrypto/ec2_secp521r1.pkcs8 @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBK+Wq6/RhJ0n1s/+r +qcwVBZYo6OFeOpwlmvFfrrsRwxWnptigR6kKXm1/w7AX7eHFuc+kyVI5KXu7hJUP +S9sAwcmhgYkDgYYABAE5InvhsngiOkoFhRcSDgxmFMjWMZG6BAw57Cwz2ar9VoyY +GYIJtw976kc8Yz+NPz6BNJpfo2wv6YnyrV6CEFqbtQAXI5DY7kk1qsaawgZcFoaH +ngIII80o6Eo9OMwsVzTUmkkAmWGySwvqRge3eVMJTkPjY1AxoP5aOJr+qcDRbZLr +0A== +-----END PRIVATE KEY----- diff --git a/test/webcrypto/ec2_secp521r1.spki b/test/webcrypto/ec2_secp521r1.spki new file mode 100644 index 00000000..8834d890 --- /dev/null +++ b/test/webcrypto/ec2_secp521r1.spki @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBOSJ74bJ4IjpKBYUXEg4MZhTI1jGR +ugQMOewsM9mq/VaMmBmCCbcPe+pHPGM/jT8+gTSaX6NsL+mJ8q1eghBam7UAFyOQ +2O5JNarGmsIGXBaGh54CCCPNKOhKPTjMLFc01JpJAJlhsksL6kYHt3lTCU5D42NQ +MaD+Wjia/qnA0W2S69A= +-----END PUBLIC KEY----- diff --git a/test/webcrypto/ec_secp384r1.pkcs8 b/test/webcrypto/ec_secp384r1.pkcs8 new file mode 100644 index 00000000..17f8d319 --- /dev/null +++ b/test/webcrypto/ec_secp384r1.pkcs8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDA50z1r3E3NARroawH9 +eAXuoQPu1xVbcRDZ0bTNgdOHDh2E0uW5fybZnAYVjbbEPxuhZANiAATu1zCeNJ+n +F65Wjdoltr1AnHDxn+k+9KdQeXd//JMWaBReirIcmU40qvSzLmQtPiDoMHFpMf11 +UCjSMLA8sVNtEwD0bdUmYfoGBNgwzk/4y5vTiyCNSozso3xx+4/WuGs= +-----END PRIVATE KEY----- diff --git a/test/webcrypto/ec_secp384r1.spki b/test/webcrypto/ec_secp384r1.spki new file mode 100644 index 00000000..820adad8 --- /dev/null +++ b/test/webcrypto/ec_secp384r1.spki @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE7tcwnjSfpxeuVo3aJba9QJxw8Z/pPvSn +UHl3f/yTFmgUXoqyHJlONKr0sy5kLT4g6DBxaTH9dVAo0jCwPLFTbRMA9G3VJmH6 +BgTYMM5P+Mub04sgjUqM7KN8cfuP1rhr +-----END PUBLIC KEY----- diff --git a/test/webcrypto/ec_secp521r1.pkcs8 b/test/webcrypto/ec_secp521r1.pkcs8 new file mode 100644 index 00000000..7a6fe728 --- /dev/null +++ b/test/webcrypto/ec_secp521r1.pkcs8 @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAEGh8E2g1TbnN0xzm +7nKGSvDbSVZHaA+XQEuTbhklfRcMJH8X8oqOJRzl4m9nIGmXy6TGPwIMlA6maRwB +PSEGqsChgYkDgYYABADSOIb4Rpa/7WDON8vDH6DPTR9gOFcFkI2lOa68MEdE7pF1 +m57cuJ0X2qTlFS6YuurbpiF6h4cltB1pM3eGQXKNcgD6stL3WjMjpC8Phv9Q391Z +2E0ezlX0nDtFPIXAwmxptIC2U7WxHRQqkQwgJyq6xklp3vkD/eFeOSi/j0qvKrlD +SA== +-----END PRIVATE KEY----- diff --git a/test/webcrypto/ec_secp521r1.spki b/test/webcrypto/ec_secp521r1.spki new file mode 100644 index 00000000..9bd02998 --- /dev/null +++ b/test/webcrypto/ec_secp521r1.spki @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA0jiG+EaWv+1gzjfLwx+gz00fYDhX +BZCNpTmuvDBHRO6RdZue3LidF9qk5RUumLrq26YheoeHJbQdaTN3hkFyjXIA+rLS +91ozI6QvD4b/UN/dWdhNHs5V9Jw7RTyFwMJsabSAtlO1sR0UKpEMICcqusZJad75 +A/3hXjkov49Kryq5Q0g= +-----END PUBLIC KEY----- diff --git a/test/webcrypto/export.t.mjs b/test/webcrypto/export.t.mjs index 81b549df..58fbf8bb 100644 --- a/test/webcrypto/export.t.mjs +++ b/test/webcrypto/export.t.mjs @@ -375,17 +375,18 @@ let ec_tsuite = { key: { fmt: "spki", key: "ec.spki", alg: { name: "ECDSA", namedCurve: "P-256" }, - extractable: true, - usage: [ "verify" ] }, + extractable: true }, export: { fmt: "jwk" }, expected: { ext: true, kty: "EC" }, }, tests: [ - { expected: { key_ops: [ "verify" ], + { key: { usage: [ "verify" ] }, + expected: { key_ops: [ "verify" ], x: "cUUsZRGuVYReGiTQl9MqR7ir6dZIgw-8pM-F0phJWdw", y: "4Nzn_7uNz0AA3U4fhpfVxSD4U5QciGyEoM4r3jC7bjI", crv: "P-256" } }, + { key: { alg: { name: "ECDH", namedCurve: "P-256" }, usage: [ ] } }, { key: { fmt: "pkcs8", key: "ec.pkcs8", usage: [ "sign" ] }, expected: { key_ops: [ "sign" ], x: "cUUsZRGuVYReGiTQl9MqR7ir6dZIgw-8pM-F0phJWdw", @@ -397,7 +398,22 @@ let ec_tsuite = { expected: "ArrayBuffer:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE2sW0_4a3QXaSTJ0JKbSUbieKTD1UFtr7i_2CuetP6ChRANCAARxRSxlEa5VhF4aJNCX0ypHuKvp1kiDD7ykz4XSmElZ3ODc5_-7jc9AAN1OH4aX1cUg-FOUHIhshKDOK94wu24y" }, { export: { fmt: "pkcs8" }, exception: "TypeError: public key of \"ECDSA\" cannot be exported as PKCS8" }, - { export: { fmt: "raw" }, + { key: { alg: { name: "ECDH", namedCurve: "P-256" }, + fmt: "pkcs8", key: "ec.pkcs8", + usage: [ "deriveKey" ] }, + export: { fmt: "pkcs8" }, + expected: "ArrayBuffer:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE2sW0_4a3QXaSTJ0JKbSUbieKTD1UFtr7i_2CuetP6ChRANCAARxRSxlEa5VhF4aJNCX0ypHuKvp1kiDD7ykz4XSmElZ3ODc5_-7jc9AAN1OH4aX1cUg-FOUHIhshKDOK94wu24y" }, + { key: { usage: [ "verify" ] }, + export: { fmt: "spki" }, + expected: "ArrayBuffer:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcUUsZRGuVYReGiTQl9MqR7ir6dZIgw-8pM-F0phJWdzg3Of_u43PQADdTh-Gl9XFIPhTlByIbISgziveMLtuMg"}, + { key: { alg: { name: "ECDH", namedCurve: "P-256" }, usage: [ ] }, + export: { fmt: "spki" }, + expected: "ArrayBuffer:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcUUsZRGuVYReGiTQl9MqR7ir6dZIgw-8pM-F0phJWdzg3Of_u43PQADdTh-Gl9XFIPhTlByIbISgziveMLtuMg"}, + { key: { usage: [ "verify" ] }, + export: { fmt: "raw" }, + expected: "ArrayBuffer:BHFFLGURrlWEXhok0JfTKke4q-nWSIMPvKTPhdKYSVnc4Nzn_7uNz0AA3U4fhpfVxSD4U5QciGyEoM4r3jC7bjI" }, + { key: { alg: { name: "ECDH", namedCurve: "P-256" }, usage: [ ] }, + export: { fmt: "raw" }, expected: "ArrayBuffer:BHFFLGURrlWEXhok0JfTKke4q-nWSIMPvKTPhdKYSVnc4Nzn_7uNz0AA3U4fhpfVxSD4U5QciGyEoM4r3jC7bjI" }, { key: { fmt: "pkcs8", key: "ec.pkcs8", usage: [ "sign" ] }, export: { fmt: "raw" }, -- cgit v1.2.3