Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement rsa toPem, toDer, and fromJwk utilities #2389

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
jasnell marked this conversation as resolved.
Show resolved Hide resolved
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));
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer note: This is a more efficient alternative to the existing base64 url decoding method we have based on kj::decodeBase64. This one follows the standard forgiving base64 decode algorithm defined by whatwg specs. Soon I expect this to be moved out of this rsa.c++ file and into a shared location.


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,
jasnell marked this conversation as resolved.
Show resolved Hide resolved
"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,
jasnell marked this conversation as resolved.
Show resolved Hide resolved
"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) {
jasnell marked this conversation as resolved.
Show resolved Hide resolved
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
Loading