diff --git a/src/workerd/api/crypto/impl.c++ b/src/workerd/api/crypto/impl.c++ index 1d0dd8a91ea..38b03226f5a 100644 --- a/src/workerd/api/crypto/impl.c++ +++ b/src/workerd/api/crypto/impl.c++ @@ -298,4 +298,28 @@ bool CSPRNG(kj::ArrayPtr buffer) { return false; } + + +kj::Maybe> tryGetAsn1Sequence(kj::ArrayPtr data) { + if (data.size() < 2 || data[0] != 0x30) + return kj::none; + + if (data[1] & 0x80) { + // Long form. + size_t n_bytes = data[1] & ~0x80; + if (n_bytes + 2 > data.size() || n_bytes > sizeof(size_t)) + return kj::none; + size_t length = 0; + for (size_t i = 0; i < n_bytes; i++) + length = (length << 8) | data[i + 2]; + auto start = 2 + n_bytes; + auto end = start + kj::min(data.size() - 2 - n_bytes, length); + return data.slice(start, end); + } + + // Short form. + auto start = 2; + auto end = start + kj::min(data.size() - 2, data[1]); + return data.slice(start, end); +} } // namespace workerd::api diff --git a/src/workerd/api/crypto/impl.h b/src/workerd/api/crypto/impl.h index 9bb956963d3..b38bc89759c 100644 --- a/src/workerd/api/crypto/impl.h +++ b/src/workerd/api/crypto/impl.h @@ -373,6 +373,8 @@ kj::Own fromRsaKey(kj::Own key); kj::Own fromEcKey(kj::Own key); kj::Own fromEd25519Key(kj::Own key); + // If the input bytes are a valid ASN.1 sequence, return them minus the prefix. +kj::Maybe> tryGetAsn1Sequence(kj::ArrayPtr data); } // namespace workerd::api KJ_DECLARE_NON_POLYMORPHIC(DH); diff --git a/src/workerd/api/crypto/keys.h b/src/workerd/api/crypto/keys.h index e15705949af..40f4ff121be 100644 --- a/src/workerd/api/crypto/keys.h +++ b/src/workerd/api/crypto/keys.h @@ -4,6 +4,29 @@ namespace workerd::api { +enum class KeyEncoding { + PKCS1, + PKCS8, + SPKI, + SEC1, +}; + +inline kj::StringPtr KJ_STRINGIFY(KeyEncoding encoding) { + switch (encoding) { + case KeyEncoding::PKCS1: return "pkcs1"; + case KeyEncoding::PKCS8: return "pkcs8"; + case KeyEncoding::SPKI: return "spki"; + case KeyEncoding::SEC1: return "sec1"; + } + KJ_UNREACHABLE; +} + +enum class KeyFormat { + PEM, + DER, + JWK, +}; + enum class KeyType { SECRET, PUBLIC, diff --git a/src/workerd/api/crypto/rsa.c++ b/src/workerd/api/crypto/rsa.c++ index 3b3d59d6ea2..d16778c8382 100644 --- a/src/workerd/api/crypto/rsa.c++ +++ b/src/workerd/api/crypto/rsa.c++ @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include "simdutf.h" namespace workerd::api { @@ -28,6 +30,32 @@ kj::Maybe fromBignum(kj::ArrayPtr value) { return asUnsigned; } + +kj::Array bioToArray(BIO* bio) { + BUF_MEM* bptr; + BIO_get_mem_ptr(bio, &bptr); + auto buf = kj::heapArray(bptr->length); + auto aptr = kj::arrayPtr(bptr->data, bptr->length); + buf.asPtr().copyFrom(aptr); + return buf.releaseAsBytes(); +} + +kj::Maybe> simdutfBase64UrlDecode(kj::StringPtr input) { + auto size = simdutf::maximal_binary_length_from_base64(input.begin(), input.size()); + auto buf = kj::heapArray(size); + auto result = simdutf::base64_to_binary(input.begin(), + input.size(), + buf.asChars().begin(), + simdutf::base64_url); + if (result.error != simdutf::SUCCESS) return kj::none; + KJ_ASSERT(result.count <= size); + return buf.first(result.count).attach(kj::mv(buf)); +} + +kj::Array simdutfBase64UrlDecodeChecked(kj::StringPtr input, + kj::StringPtr error) { + return JSG_REQUIRE_NONNULL(simdutfBase64UrlDecode(input), Error, error); +} } // namespace kj::Maybe Rsa::tryGetRsa(const EVP_PKEY* key) { @@ -225,6 +253,195 @@ SubtleCrypto::JsonWebKey Rsa::toJwk(KeyType keyType, return jwk; } +kj::Maybe Rsa::fromJwk(KeyType keyType, + const SubtleCrypto::JsonWebKey& jwk) { + ClearErrorOnReturn clearErrorOnReturn; + + if (jwk.kty != "RSA"_kj) return kj::none; + auto n = JSG_REQUIRE_NONNULL(jwk.n.map([](auto& str) { return str.asPtr(); }), Error, + "Invalid RSA key in JSON Web Key; missing or invalid " + "Modulus parameter (\"n\")."); + auto e = JSG_REQUIRE_NONNULL(jwk.e.map([](auto& str) { return str.asPtr(); }), Error, + "Invalid RSA key in JSON Web Key; missing or invalid " + "Exponent parameter (\"e\")."); + + auto rsa = OSSL_NEW(RSA); + + static constexpr auto kInvalidBase64Error = "Invalid RSA key in JSON Web Key; invalid base64."_kj; + + auto nDecoded = toBignumUnowned(simdutfBase64UrlDecodeChecked(n, kInvalidBase64Error)); + auto eDecoded = toBignumUnowned(simdutfBase64UrlDecodeChecked(e, kInvalidBase64Error)); + JSG_REQUIRE(RSA_set0_key(rsa.get(), nDecoded, eDecoded, nullptr) == 1, Error, + "Invalid RSA key in JSON Web Key; failed to set key parameters"); + + if (keyType == KeyType::PRIVATE) { + auto d = JSG_REQUIRE_NONNULL(jwk.d.map([](auto& str) { return str.asPtr(); }), Error, + "Invalid RSA key in JSON Web Key; missing or invalid " + "Private Exponent parameter (\"d\")."); + auto p = JSG_REQUIRE_NONNULL(jwk.p.map([](auto& str) { return str.asPtr(); }), Error, + "Invalid RSA key in JSON Web Key; missing or invalid " + "First Prime Factor parameter (\"p\")."); + auto q = JSG_REQUIRE_NONNULL(jwk.q.map([](auto& str) { return str.asPtr(); }), Error, + "Invalid RSA key in JSON Web Key; missing or invalid " + "Second Prime Factor parameter (\"q\")."); + auto dp = JSG_REQUIRE_NONNULL(jwk.dp.map([](auto& str) { return str.asPtr(); }), Error, + "Invalid RSA key in JSON Web Key; missing or invalid " + "First Factor CRT Exponent parameter (\"dp\")."); + auto dq = JSG_REQUIRE_NONNULL(jwk.dq.map([](auto& str) { return str.asPtr(); }), Error, + "Invalid RSA key in JSON Web Key; missing or invalid " + "Second Factor CRT Exponent parameter (\"dq\")."); + auto qi = JSG_REQUIRE_NONNULL(jwk.qi.map([](auto& str) { return str.asPtr(); }), Error, + "Invalid RSA key in JSON Web Key; missing or invalid " + "First CRT Coefficient parameter (\"qi\")."); + auto dDecoded = toBignumUnowned(simdutfBase64UrlDecodeChecked(d, + "Invalid RSA key in JSON Web Key"_kj)); + auto pDecoded = toBignumUnowned(simdutfBase64UrlDecodeChecked(p, kInvalidBase64Error)); + auto qDecoded = toBignumUnowned(simdutfBase64UrlDecodeChecked(q, kInvalidBase64Error)); + auto dpDecoded = toBignumUnowned(simdutfBase64UrlDecodeChecked(dp, kInvalidBase64Error)); + auto dqDecoded = toBignumUnowned(simdutfBase64UrlDecodeChecked(dq, kInvalidBase64Error)); + auto qiDecoded = toBignumUnowned(simdutfBase64UrlDecodeChecked(qi, kInvalidBase64Error)); + + JSG_REQUIRE(RSA_set0_key(rsa.get(), nullptr, nullptr, dDecoded) == 1, Error, + "Invalid RSA key in JSON Web Key; failed to set private exponent"); + JSG_REQUIRE(RSA_set0_factors(rsa.get(), pDecoded, qDecoded) == 1, Error, + "Invalid RSA key in JSON Web Key; failed to set prime factors"); + JSG_REQUIRE(RSA_set0_crt_params(rsa.get(), dpDecoded, dqDecoded, qiDecoded) == 1, Error, + "Invalid RSA key in JSON Web Key; failed to set CRT parameters"); + } + + auto evpPkey = OSSL_NEW(EVP_PKEY); + KJ_ASSERT(EVP_PKEY_set1_RSA(evpPkey.get(), rsa.get()) == 1); + + auto usages = keyType == KeyType::PRIVATE ? CryptoKeyUsageSet::privateKeyMask() + : CryptoKeyUsageSet::publicKeyMask(); + return AsymmetricKeyData { + kj::mv(evpPkey), + keyType, + usages + }; +} + +kj::String Rsa::toPem(KeyEncoding encoding, + KeyType keyType, + kj::Maybe options) const { + ClearErrorOnReturn clearErrorOnReturn; + auto bio = OSSL_BIO_MEM(); + switch (keyType) { + case KeyType::PUBLIC: { + switch (encoding) { + case KeyEncoding::PKCS1: { + JSG_REQUIRE(PEM_write_bio_RSAPublicKey(bio.get(), rsa) == 1, Error, + "Failed to write RSA public key to PEM", tryDescribeOpensslErrors()); + break; + } + case workerd::api::KeyEncoding::SPKI: { + JSG_REQUIRE(PEM_write_bio_RSA_PUBKEY(bio.get(), rsa) == 1, Error, + "Failed to write RSA public key to PEM", tryDescribeOpensslErrors()); + break; + } + default: { + JSG_FAIL_REQUIRE(Error, "Unsupported RSA public key encoding: ", encoding); + } + } + break; + } + case KeyType::PRIVATE: { + kj::byte* passphrase = nullptr; + size_t passLen = 0; + const EVP_CIPHER* cipher = nullptr; + KJ_IF_SOME(opts, options) { + passphrase = const_cast(opts.passphrase.begin()); + passLen = opts.passphrase.size(); + cipher = opts.cipher; + } + switch (encoding) { + case KeyEncoding::PKCS1: { + JSG_REQUIRE(PEM_write_bio_RSAPrivateKey( + bio.get(), rsa, cipher, passphrase, passLen, nullptr, nullptr) == 1, Error, + "Failed to write RSA private key to PEM", tryDescribeOpensslErrors()); + break; + } + case KeyEncoding::PKCS8: { + auto evpPkey = OSSL_NEW(EVP_PKEY); + EVP_PKEY_set1_RSA(evpPkey.get(), rsa); + JSG_REQUIRE(PEM_write_bio_PKCS8PrivateKey( + bio.get(), evpPkey.get(), cipher, reinterpret_cast(passphrase), + passLen, nullptr, nullptr) == 1, Error, + "Failed to write RSA private key to PKCS8 PEM", tryDescribeOpensslErrors()); + break; + } + default: { + JSG_FAIL_REQUIRE(Error, "Unsupported RSA private key encoding: ", encoding); + } + } + break; + } + default: KJ_UNREACHABLE; + } + return kj::String(bioToArray(bio.get()).releaseAsChars()); +} + +kj::Array Rsa::toDer(KeyEncoding encoding, + KeyType keyType, + kj::Maybe options) const { + ClearErrorOnReturn clearErrorOnReturn; + auto bio = OSSL_BIO_MEM(); + switch (keyType) { + case KeyType::PUBLIC: { + switch (encoding) { + case KeyEncoding::PKCS1: { + JSG_REQUIRE(i2d_RSAPublicKey_bio(bio.get(), rsa) == 1, Error, + "Failed to write RSA public key to DER", tryDescribeOpensslErrors()); + break; + } + case workerd::api::KeyEncoding::SPKI: { + auto evpPkey = OSSL_NEW(EVP_PKEY); + EVP_PKEY_set1_RSA(evpPkey.get(), rsa); + JSG_REQUIRE(i2d_PUBKEY_bio(bio.get(), evpPkey.get()) == 1, Error, + "Failed to write RSA public key to SPKI", tryDescribeOpensslErrors()); + break; + } + default: { + JSG_FAIL_REQUIRE(Error, "Unsupported RSA public key encoding: ", encoding); + } + } + break; + } + case KeyType::PRIVATE: { + kj::byte* passphrase = nullptr; + size_t passLen = 0; + const EVP_CIPHER* cipher = nullptr; + KJ_IF_SOME(opts, options) { + passphrase = const_cast(opts.passphrase.begin()); + passLen = opts.passphrase.size(); + cipher = opts.cipher; + } + switch (encoding) { + case KeyEncoding::PKCS1: { + // Does not permit encryption + JSG_REQUIRE(i2d_RSAPrivateKey_bio(bio.get(), rsa), Error, + "Failed to write RSA private key to PEM", tryDescribeOpensslErrors()); + break; + } + case KeyEncoding::PKCS8: { + auto evpPkey = OSSL_NEW(EVP_PKEY); + EVP_PKEY_set1_RSA(evpPkey.get(), rsa); + JSG_REQUIRE(i2d_PKCS8PrivateKey_bio(bio.get(), evpPkey.get(), + cipher, reinterpret_cast(passphrase), passLen, nullptr, nullptr) == 1, Error, + "Failed to write RSA private key to PKCS8 PEM", tryDescribeOpensslErrors()); + break; + } + default: { + JSG_FAIL_REQUIRE(Error, "Unsupported RSA private key encoding: ", encoding); + } + } + break; + } + default: KJ_UNREACHABLE; + } + return bioToArray(bio.get()); +} + void Rsa::validateRsaParams(jsg::Lock& js, size_t modulusLength, kj::ArrayPtr publicExponent, @@ -260,6 +477,13 @@ void Rsa::validateRsaParams(jsg::Lock& js, } } +bool Rsa::isRSAPrivateKey(kj::ArrayPtr keyData) { + KJ_IF_SOME(rem, tryGetAsn1Sequence(keyData)) { + return rem.size() >= 3 && rem[0] == 2 && rem[1] == 1 && !(rem[2] & 0xfe); + } + return false; +} + // ====================================================================================== // Web Crypto Impl: RSASSA-PKCS1-V1_5, RSA-PSS, RSA-OEAP, RSA-RAW diff --git a/src/workerd/api/crypto/rsa.h b/src/workerd/api/crypto/rsa.h index bb3fb2d53a5..2cfe583aa05 100644 --- a/src/workerd/api/crypto/rsa.h +++ b/src/workerd/api/crypto/rsa.h @@ -22,19 +22,38 @@ class Rsa final { kj::Array getPublicExponent() KJ_WARN_UNUSED_RESULT; - CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail() const; + CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail() const KJ_WARN_UNUSED_RESULT; - kj::Array sign(const kj::ArrayPtr data) const; + kj::Array sign(const kj::ArrayPtr data) const KJ_WARN_UNUSED_RESULT; + + static kj::Maybe fromJwk( + KeyType keyType, + const SubtleCrypto::JsonWebKey& jwk) KJ_WARN_UNUSED_RESULT; SubtleCrypto::JsonWebKey toJwk(KeyType keytype, - kj::Maybe maybeHashAlgorithm) const; + kj::Maybe maybeHashAlgorithm) const + KJ_WARN_UNUSED_RESULT; + + struct CipherOptions { + const EVP_CIPHER* cipher; + kj::ArrayPtr passphrase; + }; + + kj::String toPem(KeyEncoding encoding, + KeyType keyType, + kj::Maybe options = kj::none) const KJ_WARN_UNUSED_RESULT; + + kj::Array toDer(KeyEncoding encoding, + KeyType keyType, + kj::Maybe options = kj::none) const + KJ_WARN_UNUSED_RESULT; using EncryptDecryptFunction = decltype(EVP_PKEY_encrypt); kj::Array cipher(EVP_PKEY_CTX* ctx, SubtleCrypto::EncryptAlgorithm&& algorithm, kj::ArrayPtr data, EncryptDecryptFunction encryptDecrypt, - const EVP_MD* cipher) const; + const EVP_MD* cipher) const KJ_WARN_UNUSED_RESULT; // The W3C standard itself doesn't describe any parameter validation but the conformance tests // do test "bad" exponents, likely because everyone uses OpenSSL that suffers from poor behavior @@ -45,6 +64,8 @@ class Rsa final { kj::ArrayPtr publicExponent, bool isImport = false); + static bool isRSAPrivateKey(kj::ArrayPtr keyData) KJ_WARN_UNUSED_RESULT; + private: RSA* rsa; const BIGNUM* n = nullptr;