aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Volyntsev <xeioex@nginx.com>2025-05-08 17:13:01 -0700
committerDmitry Volyntsev <xeioexception@gmail.com>2025-05-16 12:15:29 -0700
commit1b69415d8c29bde08cc4c79dbb4b88827c55d8b9 (patch)
treec09dd59e1ecfb7a8c65183359c665c2ab74cddd8
parent637fc26eac8622ccf8c73cfa4604e9afe54c3f34 (diff)
downloadnjs-1b69415d8c29bde08cc4c79dbb4b88827c55d8b9.tar.gz
njs-1b69415d8c29bde08cc4c79dbb4b88827c55d8b9.zip
WebCrypto: added ECDH support.
-rw-r--r--external/njs_webcrypto_module.c239
-rw-r--r--external/qjs_webcrypto_module.c245
-rw-r--r--test/harness/webCryptoUtils.js16
-rw-r--r--test/webcrypto/derive_ecdh.t.mjs188
-rw-r--r--test/webcrypto/ec2_secp384r1.pkcs86
-rw-r--r--test/webcrypto/ec2_secp384r1.spki5
-rw-r--r--test/webcrypto/ec2_secp521r1.pkcs88
-rw-r--r--test/webcrypto/ec2_secp521r1.spki6
-rw-r--r--test/webcrypto/ec_secp384r1.pkcs86
-rw-r--r--test/webcrypto/ec_secp384r1.spki5
-rw-r--r--test/webcrypto/ec_secp521r1.pkcs88
-rw-r--r--test/webcrypto/ec_secp521r1.spki6
-rw-r--r--test/webcrypto/export.t.mjs24
13 files changed, 728 insertions, 34 deletions
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)
},
@@ -1442,6 +1443,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),
@@ -1665,6 +1669,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" },