Skip to content

Commit

Permalink
Merge pull request #2389 from cloudflare/jsnell/crypto-rsa-updates
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell authored Jul 22, 2024
2 parents d46550a + 4ab6dab commit c46c662
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 4 deletions.
24 changes: 24 additions & 0 deletions src/workerd/api/crypto/impl.c++
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,28 @@ bool CSPRNG(kj::ArrayPtr<kj::byte> buffer) {

return false;
}


kj::Maybe<kj::ArrayPtr<const kj::byte>> tryGetAsn1Sequence(kj::ArrayPtr<const kj::byte> 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
2 changes: 2 additions & 0 deletions src/workerd/api/crypto/impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ kj::Own<CryptoKey::Impl> fromRsaKey(kj::Own<EVP_PKEY> key);
kj::Own<CryptoKey::Impl> fromEcKey(kj::Own<EVP_PKEY> key);
kj::Own<CryptoKey::Impl> fromEd25519Key(kj::Own<EVP_PKEY> key);

// If the input bytes are a valid ASN.1 sequence, return them minus the prefix.
kj::Maybe<kj::ArrayPtr<const kj::byte>> tryGetAsn1Sequence(kj::ArrayPtr<const kj::byte> data);
} // namespace workerd::api

KJ_DECLARE_NON_POLYMORPHIC(DH);
Expand Down
23 changes: 23 additions & 0 deletions src/workerd/api/crypto/keys.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
224 changes: 224 additions & 0 deletions src/workerd/api/crypto/rsa.c++
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
#include <openssl/bn.h>
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <map>
#include "simdutf.h"

namespace workerd::api {

Expand All @@ -28,6 +30,32 @@ kj::Maybe<T> fromBignum(kj::ArrayPtr<kj::byte> value) {

return asUnsigned;
}

kj::Array<kj::byte> bioToArray(BIO* bio) {
BUF_MEM* bptr;
BIO_get_mem_ptr(bio, &bptr);
auto buf = kj::heapArray<char>(bptr->length);
auto aptr = kj::arrayPtr(bptr->data, bptr->length);
buf.asPtr().copyFrom(aptr);
return buf.releaseAsBytes();
}

kj::Maybe<kj::Array<kj::byte>> simdutfBase64UrlDecode(kj::StringPtr input) {
auto size = simdutf::maximal_binary_length_from_base64(input.begin(), input.size());
auto buf = kj::heapArray<kj::byte>(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<kj::byte> simdutfBase64UrlDecodeChecked(kj::StringPtr input,
kj::StringPtr error) {
return JSG_REQUIRE_NONNULL(simdutfBase64UrlDecode(input), Error, error);
}
} // namespace

kj::Maybe<Rsa> Rsa::tryGetRsa(const EVP_PKEY* key) {
Expand Down Expand Up @@ -225,6 +253,195 @@ SubtleCrypto::JsonWebKey Rsa::toJwk(KeyType keyType,
return jwk;
}

kj::Maybe<AsymmetricKeyData> 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<CipherOptions> 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<kj::byte*>(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<char*>(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<const kj::byte> Rsa::toDer(KeyEncoding encoding,
KeyType keyType,
kj::Maybe<CipherOptions> 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<kj::byte*>(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<char*>(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<kj::byte> publicExponent,
Expand Down Expand Up @@ -260,6 +477,13 @@ void Rsa::validateRsaParams(jsg::Lock& js,
}
}

bool Rsa::isRSAPrivateKey(kj::ArrayPtr<const kj::byte> 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

Expand Down
29 changes: 25 additions & 4 deletions src/workerd/api/crypto/rsa.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,38 @@ class Rsa final {

kj::Array<kj::byte> getPublicExponent() KJ_WARN_UNUSED_RESULT;

CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail() const;
CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail() const KJ_WARN_UNUSED_RESULT;

kj::Array<kj::byte> sign(const kj::ArrayPtr<const kj::byte> data) const;
kj::Array<kj::byte> sign(const kj::ArrayPtr<const kj::byte> data) const KJ_WARN_UNUSED_RESULT;

static kj::Maybe<AsymmetricKeyData> fromJwk(
KeyType keyType,
const SubtleCrypto::JsonWebKey& jwk) KJ_WARN_UNUSED_RESULT;

SubtleCrypto::JsonWebKey toJwk(KeyType keytype,
kj::Maybe<kj::String> maybeHashAlgorithm) const;
kj::Maybe<kj::String> maybeHashAlgorithm) const
KJ_WARN_UNUSED_RESULT;

struct CipherOptions {
const EVP_CIPHER* cipher;
kj::ArrayPtr<const kj::byte> passphrase;
};

kj::String toPem(KeyEncoding encoding,
KeyType keyType,
kj::Maybe<CipherOptions> options = kj::none) const KJ_WARN_UNUSED_RESULT;

kj::Array<const kj::byte> toDer(KeyEncoding encoding,
KeyType keyType,
kj::Maybe<CipherOptions> options = kj::none) const
KJ_WARN_UNUSED_RESULT;

using EncryptDecryptFunction = decltype(EVP_PKEY_encrypt);
kj::Array<kj::byte> cipher(EVP_PKEY_CTX* ctx,
SubtleCrypto::EncryptAlgorithm&& algorithm,
kj::ArrayPtr<const kj::byte> 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
Expand All @@ -45,6 +64,8 @@ class Rsa final {
kj::ArrayPtr<kj::byte> publicExponent,
bool isImport = false);

static bool isRSAPrivateKey(kj::ArrayPtr<const kj::byte> keyData) KJ_WARN_UNUSED_RESULT;

private:
RSA* rsa;
const BIGNUM* n = nullptr;
Expand Down

0 comments on commit c46c662

Please sign in to comment.