}"
. auto/feature
+ # Ed25519 and X25519 share the same Curve25519 implementation and
+ # were introduced together in OpenSSL 1.1.1, LibreSSL 3.7.0,
+ # BoringSSL, and AWS-LC. No known build configuration enables
+ # one without the other, so a single probe is sufficient.
+
+ njs_feature="EVP_PKEY_ED25519"
+ njs_feature_name=NJS_HAVE_ED25519
+ njs_feature_run=
+ njs_feature_test="#include <openssl/evp.h>
+
+ int main() {
+ return EVP_PKEY_new_raw_public_key(
+ EVP_PKEY_ED25519, NULL, NULL, 0) == NULL;
+ }"
+ . auto/feature
+
njs_feature="OpenSSL version"
njs_feature_name=NJS_OPENSSL_VERSION
njs_feature_run=value
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,
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,
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,
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;
}
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);
}
}
+#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)
}
+#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)
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:
}
+#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)
{
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;
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);
njs_webcrypto_algorithm_t *alg;
unsigned char m[EVP_MAX_MD_SIZE];
+ dst = NULL;
mctx = NULL;
pctx = NULL;
}
}
+ 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);
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:
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);
}
break;
+ case NJS_ALGORITHM_ED25519:
+ case NJS_ALGORITHM_X25519:
+ break;
+
case NJS_ALGORITHM_HMAC:
default:
/* HmacKeyGenParams */
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;
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,
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,
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,
{ 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 }
};
}
+#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)
{
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;
}
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);
}
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:
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);
}
}
+#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)
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:
}
+#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)
{
break;
case QJS_KEY_JWK_KTY_OCT:
- default:
if (!alg->raw) {
JS_ThrowTypeError(cx, "JWK kty \"oct\" doesn't "
"match algorithm \"%s\"",
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;
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);
}
}
+ 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);
break;
+ case QJS_ALGORITHM_ED25519:
+ case QJS_ALGORITHM_X25519:
+ break;
+
case QJS_ALGORITHM_HMAC:
default:
/* HmacKeyGenParams */
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
===============
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
========
--- /dev/null
+/*---
+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);
--- /dev/null
+/*---
+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);
| AesVariants
| "PBKDF2"
| "HKDF"
- | "ECDH";
+ | "ECDH"
+ | "Ed25519"
+ | "X25519";
type GenerateAlgorithm =
| RsaHashedKeyGenParams
type JWK =
| { kty: "RSA"; }
| { kty: "EC"; }
- | { kty: "oct"; };
+ | { kty: "oct"; }
+ | { kty: "OKP"; };
type KeyData =
| NjsStringOrBuffer
public: CryptoKey;
}
+interface X25519Params {
+ name: "X25519";
+ public: CryptoKey;
+}
+
type DeriveAlgorithm =
| HkdfParams
| Pbkdf2Params
- | EcdhParams;
+ | EcdhParams
+ | X25519Params;
interface HmacKeyGenParams {
name: "HMAC";
| { name: "HMAC"; }
| { name: "RSASSA-PKCS1-v1_5"; }
| "HMAC"
- | "RSASSA-PKCS1-v1_5";
+ | "RSASSA-PKCS1-v1_5"
+ | "Ed25519";
interface CryptoKey {
/*
* Possible array values: "encrypt", "decrypt", "sign", "verify",
* "deriveKey", "deriveBits", "wrapKey", "unwrapKey".
*/
- generateKey(algorithm: RsaHashedKeyGenParams | EcKeyGenParams,
+ generateKey(algorithm: RsaHashedKeyGenParams | EcKeyGenParams
+ | "Ed25519" | "X25519",
extractable: boolean,
usage: Array<string>): Promise<CryptoKeyPair>;