Skip to content

Commit c2352c6

Browse files
AndrewKahrdanlapid
authored andcommitted
Fixed sign/verify behavior when using ECDSA keys and/or crypto.subtle keys
1 parent 81a7a0c commit c2352c6

4 files changed

Lines changed: 71 additions & 15 deletions

File tree

src/workerd/api/node/crypto-keys.c++

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <workerd/api/crypto/impl.h>
88
#include <workerd/api/crypto/jwk.h>
9+
#include <workerd/api/crypto/keys.h>
910

1011
#include <ncrypto.h>
1112
#include <openssl/crypto.h>
@@ -862,10 +863,28 @@ jsg::JsUint8Array CryptoImpl::statelessDH(
862863
JSG_FAIL_REQUIRE(Error, "Unsupported keys for stateless diffie-hellman");
863864
}
864865

865-
kj::Maybe<const ncrypto::EVPKeyPointer&> CryptoImpl::tryGetKey(jsg::Ref<CryptoKey>& key) {
866-
KJ_IF_SOME(key, kj::dynamicDowncastIfAvailable<AsymmetricKey>(*key->impl)) {
867-
const ncrypto::EVPKeyPointer& evp = key;
868-
return evp;
866+
kj::Maybe<ncrypto::EVPKeyPointer> CryptoImpl::tryGetKey(jsg::Ref<CryptoKey>& key) {
867+
// AsymmetricKeyCryptoKeyImpl doesn't provide a reference like AsymmetricKey,
868+
// so this function must return a value type since we create the EVPKeyPointer
869+
// in here
870+
KJ_IF_SOME(asymKey, kj::dynamicDowncastIfAvailable<AsymmetricKey>(*key->impl)) {
871+
const ncrypto::EVPKeyPointer& evp = asymKey;
872+
// Internally just incrementing the ref count and returning a new pointer, no
873+
// copied key data so impact should be minimal.
874+
return evp.clone();
875+
}
876+
// Also handle keys created via the Web Crypto API (crypto.subtle), which use a
877+
// different CryptoKey::Impl subclass.
878+
KJ_IF_SOME(webCryptoKey, kj::dynamicDowncastIfAvailable<AsymmetricKeyCryptoKeyImpl>(*key->impl)) {
879+
EVP_PKEY* raw = webCryptoKey.getEvpPkey();
880+
// Mimicking the internal implementation of EVPKeyPointer::clone() since getEvpPkey()
881+
// returns a raw pointer
882+
if (raw != nullptr) {
883+
if (!EVP_PKEY_up_ref(raw)) {
884+
return kj::none;
885+
}
886+
return ncrypto::EVPKeyPointer(raw);
887+
}
869888
}
870889
return kj::none;
871890
}

src/workerd/api/node/crypto.c++

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ jsg::JsUint8Array CryptoImpl::SignHandle::sign(jsg::Lock& js,
441441
ncrypto::ClearErrorOnReturn clear_error_on_return;
442442
JSG_REQUIRE(ctx, Error, "Signing context has already been finalized");
443443

444-
const auto& pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "Invalid key for sign operation");
444+
auto pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "Invalid key for sign operation");
445445
JSG_REQUIRE(pkey.validateDsaParameters(), Error, "Invalid DSA parameters");
446446

447447
// There's a bug in ncrypto that doesn't clear the EVPMDCtxPointer when
@@ -497,7 +497,7 @@ bool CryptoImpl::VerifyHandle::verify(jsg::Lock& js,
497497

498498
JSG_REQUIRE(ctx, Error, "Verification context has already been finalized");
499499

500-
const auto& pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "Invalid key for verify operation");
500+
auto pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "Invalid key for verify operation");
501501

502502
JSG_REQUIRE(!pkey.isOneShotVariant(), Error, "Unsupported operation for this key");
503503

@@ -529,7 +529,7 @@ jsg::JsUint8Array CryptoImpl::signOneShot(jsg::Lock& js,
529529
auto mdctx = ncrypto::EVPMDCtxPointer::New();
530530
JSG_REQUIRE(mdctx, Error, "Failed to create signing context");
531531

532-
const auto& pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "Invalid key for sign operation");
532+
auto pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "Invalid key for sign operation");
533533

534534
// The version of BoringSSL we use does not support DSA keys with EVP
535535
// When signing/verification. This may change in the future.
@@ -546,7 +546,13 @@ jsg::JsUint8Array CryptoImpl::signOneShot(jsg::Lock& js,
546546
.len = data.size(),
547547
};
548548

549-
ncrypto::DataPointer sig = mdctx.signOneShot(buf);
549+
// For one-shot-only key types (e.g. Ed25519, Ed448), we must use
550+
// EVP_DigestSign via signOneShot(). For other key types (e.g. ECDSA),
551+
// we use sign() which calls EVP_DigestSignUpdate + EVP_DigestSignFinal
552+
// and correctly resizes the output buffer to the actual signature length.
553+
// This matters for ECDSA where DER-encoded signatures can be shorter
554+
// than the maximum estimated size.
555+
ncrypto::DataPointer sig = pkey.isOneShotVariant() ? mdctx.signOneShot(buf) : mdctx.sign(buf);
550556
auto sigBuf =
551557
jsg::JsUint8Array::create(js, kj::ArrayPtr<const kj::byte>(sig.get<kj::byte>(), sig.size()));
552558

@@ -574,8 +580,7 @@ bool CryptoImpl::verifyOneShot(jsg::Lock& js,
574580
auto mdctx = ncrypto::EVPMDCtxPointer::New();
575581
JSG_REQUIRE(mdctx, Error, "Failed to create verification context");
576582

577-
const auto& pkey =
578-
JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "Invalid key for verification operation");
583+
auto pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "Invalid key for verification operation");
579584

580585
// The version of BoringSSL we use does not support DSA keys with EVP
581586
// When signing/verification. This may change in the future.
@@ -1317,7 +1322,7 @@ jsg::JsUint8Array CryptoImpl::publicEncrypt(jsg::Lock& js,
13171322
jsg::Ref<CryptoKey> key,
13181323
jsg::JsBufferSource buffer,
13191324
CryptoImpl::PublicPrivateCipherOptions options) {
1320-
auto& pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "No key provided");
1325+
auto pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "No key provided");
13211326
JSG_REQUIRE(pkey.isRsaVariant(), Error, "publicEncrypt() currently only supports RSA keys");
13221327
auto ctx = pkey.newCtx();
13231328
JSG_REQUIRE(ctx.initForEncrypt(), Error, "Failed to init for encryption");
@@ -1328,7 +1333,7 @@ jsg::JsUint8Array CryptoImpl::privateDecrypt(jsg::Lock& js,
13281333
jsg::Ref<CryptoKey> key,
13291334
jsg::JsBufferSource buffer,
13301335
CryptoImpl::PublicPrivateCipherOptions options) {
1331-
auto& pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "No key provided");
1336+
auto pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "No key provided");
13321337
JSG_REQUIRE(pkey.isRsaVariant(), Error, "publicEncrypt() currently only supports RSA keys");
13331338
auto ctx = pkey.newCtx();
13341339
JSG_REQUIRE(ctx.initForDecrypt(), Error, "Failed to init for decryption");
@@ -1339,7 +1344,7 @@ jsg::JsUint8Array CryptoImpl::publicDecrypt(jsg::Lock& js,
13391344
jsg::Ref<CryptoKey> key,
13401345
jsg::JsBufferSource buffer,
13411346
CryptoImpl::PublicPrivateCipherOptions options) {
1342-
auto& pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "No key provided");
1347+
auto pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "No key provided");
13431348
JSG_REQUIRE(pkey.isRsaVariant(), Error, "publicEncrypt() currently only supports RSA keys");
13441349
auto ctx = pkey.newCtx();
13451350
JSG_REQUIRE(EVP_PKEY_verify_recover_init(ctx.get()) == 1, Error, "Failed to init for decryption");
@@ -1353,7 +1358,7 @@ jsg::JsUint8Array CryptoImpl::privateEncrypt(jsg::Lock& js,
13531358
jsg::Ref<CryptoKey> key,
13541359
jsg::JsBufferSource buffer,
13551360
CryptoImpl::PublicPrivateCipherOptions options) {
1356-
auto& pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "No key provided");
1361+
auto pkey = JSG_REQUIRE_NONNULL(tryGetKey(key), Error, "No key provided");
13571362
JSG_REQUIRE(pkey.isRsaVariant(), Error, "publicEncrypt() currently only supports RSA keys");
13581363
auto ctx = pkey.newCtx();
13591364
JSG_REQUIRE(EVP_PKEY_sign_init(ctx.get()) == 1, Error, "Failed to init for encryption");

src/workerd/api/node/crypto.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ class CryptoImpl final: public jsg::Object {
213213
jsg::Ref<CryptoKey> createSecretKey(jsg::Lock& js, jsg::JsBufferSource keyData);
214214
jsg::Ref<CryptoKey> createPrivateKey(jsg::Lock& js, CreateAsymmetricKeyOptions options);
215215
jsg::Ref<CryptoKey> createPublicKey(jsg::Lock& js, CreateAsymmetricKeyOptions options);
216-
static kj::Maybe<const ncrypto::EVPKeyPointer&> tryGetKey(jsg::Ref<CryptoKey>& key);
216+
static kj::Maybe<ncrypto::EVPKeyPointer> tryGetKey(jsg::Ref<CryptoKey>& key);
217217
static kj::Maybe<kj::ArrayPtr<const kj::byte>> tryGetSecretKeyData(jsg::Ref<CryptoKey>& key);
218218

219219
struct RsaKeyPairOptions {

src/workerd/api/node/tests/crypto_sign-test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,35 @@ export const testSignLength = {
182182
}
183183
},
184184
};
185+
186+
// Test that Web Crypto keys (from crypto.subtle) can be used with
187+
// Node.js crypto sign/verify one-shot functions.
188+
export const webCryptoKeySignVerify = {
189+
async test() {
190+
const keyPair = await crypto.subtle.generateKey(
191+
{ name: 'ECDSA', namedCurve: 'P-256' },
192+
true,
193+
['sign', 'verify']
194+
);
195+
const data = Buffer.from('hello world');
196+
const sig = sign('SHA256', data, keyPair.privateKey);
197+
ok(sig instanceof Buffer);
198+
ok(sig.length > 0);
199+
ok(verify('SHA256', data, keyPair.publicKey, sig));
200+
},
201+
};
202+
203+
export const webCryptoKeySignVerifyP384 = {
204+
async test() {
205+
const keyPair = await crypto.subtle.generateKey(
206+
{ name: 'ECDSA', namedCurve: 'P-384' },
207+
true,
208+
['sign', 'verify']
209+
);
210+
const data = Buffer.from('test data');
211+
const sig = sign('SHA384', data, keyPair.privateKey);
212+
ok(sig instanceof Buffer);
213+
ok(sig.length > 0);
214+
ok(verify('SHA384', data, keyPair.publicKey, sig));
215+
},
216+
};

0 commit comments

Comments
 (0)