From 942ad54e08ca0fdb4e08545c9e16002685a3f1b1 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 28 Sep 2024 10:56:05 -0700 Subject: [PATCH] src: move more key handling to ncrypto PR-URL: https://github.com/nodejs/node/pull/55108 Reviewed-By: Yagiz Nizipli Reviewed-By: Matthew Aitken --- deps/ncrypto/ncrypto.cc | 130 +++++++++++++- deps/ncrypto/ncrypto.h | 26 ++- src/crypto/crypto_keys.cc | 351 ++++++++++++-------------------------- 3 files changed, 254 insertions(+), 253 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index f4d0d406b4c84a..457bd2f6c5b189 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -33,7 +33,7 @@ ClearErrorOnReturn::~ClearErrorOnReturn() { ERR_clear_error(); } -int ClearErrorOnReturn::peeKError() { return ERR_peek_error(); } +int ClearErrorOnReturn::peekError() { return ERR_peek_error(); } MarkPopErrorOnReturn::MarkPopErrorOnReturn(CryptoErrorList* errors) : errors_(errors) { ERR_set_mark(); @@ -1570,11 +1570,66 @@ EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner( return EVPKeyPointer::ParseKeyResult(std::move(pkey)); } -EVPKeyPointer::ParseKeyResult TryParsePublicKeyPEM( +constexpr bool IsASN1Sequence(const unsigned char* data, size_t size, + size_t* data_offset, size_t* data_size) { + if (size < 2 || data[0] != 0x30) + return false; + + if (data[1] & 0x80) { + // Long form. + size_t n_bytes = data[1] & ~0x80; + if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) + return false; + size_t length = 0; + for (size_t i = 0; i < n_bytes; i++) + length = (length << 8) | data[i + 2]; + *data_offset = 2 + n_bytes; + *data_size = std::min(size - 2 - n_bytes, length); + } else { + // Short form. + *data_offset = 2; + *data_size = std::min(size - 2, data[1]); + } + + return true; +} + +constexpr bool IsEncryptedPrivateKeyInfo(const Buffer& buffer) { + // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE. + if (buffer.len == 0 || buffer.data == nullptr) return false; + size_t offset, len; + if (!IsASN1Sequence(buffer.data, buffer.len, &offset, &len)) + return false; + + // A PrivateKeyInfo sequence always starts with an integer whereas an + // EncryptedPrivateKeyInfo starts with an AlgorithmIdentifier. + return len >= 1 && buffer.data[offset] != 2; +} + +} // namespace + +bool EVPKeyPointer::IsRSAPrivateKey(const Buffer& buffer) { + // Both RSAPrivateKey and RSAPublicKey structures start with a SEQUENCE. + size_t offset, len; + if (!IsASN1Sequence(buffer.data, buffer.len, &offset, &len)) + return false; + + // An RSAPrivateKey sequence always starts with a single-byte integer whose + // value is either 0 or 1, whereas an RSAPublicKey starts with the modulus + // (which is the product of two primes and therefore at least 4), so we can + // decide the type of the structure based on the first three bytes of the + // sequence. + return len >= 3 && + buffer.data[offset] == 2 && + buffer.data[offset + 1] == 1 && + !(buffer.data[offset + 2] & 0xfe); +} + +EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM( const Buffer& buffer) { auto bp = BIOPointer::New(buffer.data, buffer.len); if (!bp) - return EVPKeyPointer::ParseKeyResult(EVPKeyPointer::PKParseError::FAILED); + return ParseKeyResult(PKParseError::FAILED); // Try parsing as SubjectPublicKeyInfo (SPKI) first. if (auto ret = TryParsePublicKeyInner(bp, "PUBLIC KEY", @@ -1601,9 +1656,8 @@ EVPKeyPointer::ParseKeyResult TryParsePublicKeyPEM( return ret; }; - return EVPKeyPointer::ParseKeyResult(EVPKeyPointer::PKParseError::NOT_RECOGNIZED); + return ParseKeyResult(PKParseError::NOT_RECOGNIZED); } -} // namespace EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKey( PKFormatType format, @@ -1634,4 +1688,70 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKey( return ParseKeyResult(PKParseError::FAILED); } +EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey( + PKFormatType format, + PKEncodingType encoding, + std::optional> maybe_passphrase, + const Buffer& buffer) { + + static auto keyOrError = [&](EVPKeyPointer pkey, bool had_passphrase = false) { + if (int err = ERR_peek_error()) { + if (ERR_GET_LIB(err) == ERR_LIB_PEM && + ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ && + !had_passphrase) { + return ParseKeyResult(PKParseError::NEED_PASSPHRASE); + } + return ParseKeyResult(PKParseError::FAILED, err); + } + if (!pkey) return ParseKeyResult(PKParseError::FAILED); + return ParseKeyResult(std::move(pkey)); + }; + + Buffer* passphrase = nullptr; + if (maybe_passphrase.has_value()) { + passphrase = &maybe_passphrase.value(); + } + + auto bio = BIOPointer::New(buffer); + if (!bio) return ParseKeyResult(PKParseError::FAILED); + + if (format == PKFormatType::PEM) { + auto key = PEM_read_bio_PrivateKey(bio.get(), nullptr, PasswordCallback, passphrase); + return keyOrError(EVPKeyPointer(key), maybe_passphrase.has_value()); + } + + if (format != PKFormatType::DER) { + return ParseKeyResult(PKParseError::FAILED); + } + + switch (encoding) { + case PKEncodingType::PKCS1: { + auto key = d2i_PrivateKey_bio(bio.get(), nullptr); + return keyOrError(EVPKeyPointer(key)); + } + case PKEncodingType::PKCS8: { + if (IsEncryptedPrivateKeyInfo(buffer)) { + auto key = d2i_PKCS8PrivateKey_bio(bio.get(), + nullptr, + PasswordCallback, + passphrase); + return keyOrError(EVPKeyPointer(key), maybe_passphrase.has_value()); + } + + PKCS8Pointer p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(bio.get(), nullptr)); + if (!p8inf) { + return ParseKeyResult(PKParseError::FAILED, ERR_peek_error()); + } + return keyOrError(EVPKeyPointer(EVP_PKCS82PKEY(p8inf.get()))); + } + case PKEncodingType::SEC1: { + auto key = d2i_PrivateKey_bio(bio.get(), nullptr); + return keyOrError(EVPKeyPointer(key)); + } + default: { + return ParseKeyResult(PKParseError::FAILED, ERR_peek_error()); + } + }; +} + } // namespace ncrypto diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 39eac0e6fb5815..20b69dc67b13fd 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -148,7 +148,7 @@ class ClearErrorOnReturn final { NCRYPTO_DISALLOW_COPY_AND_MOVE(ClearErrorOnReturn) NCRYPTO_DISALLOW_NEW_DELETE() - int peeKError(); + int peekError(); private: CryptoErrorList* errors_; @@ -178,9 +178,13 @@ template struct Result final { const bool has_value; T value; - std::optional error; + std::optional error = std::nullopt; + std::optional openssl_error = std::nullopt; Result(T&& value) : has_value(true), value(std::move(value)) {} - Result(E&& error) : has_value(false), error(std::move(error)) {} + Result(E&& error, std::optional openssl_error = std::nullopt) + : has_value(false), + error(std::move(error)), + openssl_error(std::move(openssl_error)) {} inline operator bool() const { return has_value; } }; @@ -277,6 +281,11 @@ class BIOPointer final { static BIOPointer NewFile(std::string_view filename, std::string_view mode); static BIOPointer NewFp(FILE* fd, int flags); + template + static BIOPointer New(const Buffer& buf) { + return New(buf.data, buf.len); + } + BIOPointer() = default; BIOPointer(std::nullptr_t) : bio_(nullptr) {} explicit BIOPointer(BIO* bio); @@ -398,6 +407,15 @@ class EVPKeyPointer final { PKEncodingType encoding, const Buffer& buffer); + static ParseKeyResult TryParsePublicKeyPEM( + const Buffer& buffer); + + static ParseKeyResult TryParsePrivateKey( + PKFormatType format, + PKEncodingType encoding, + std::optional> passphrase, + const Buffer& buffer); + EVPKeyPointer() = default; explicit EVPKeyPointer(EVP_PKEY* pkey); EVPKeyPointer(EVPKeyPointer&& other) noexcept; @@ -428,6 +446,8 @@ class EVPKeyPointer final { EVPKeyCtxPointer newCtx() const; + static bool IsRSAPrivateKey(const Buffer& buffer); + private: DeleteFnPtr pkey_; }; diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 58d3e26d191a4e..04e4fc390d336d 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -75,196 +75,6 @@ void GetKeyFormatAndTypeFromJs( *offset += 2; } -template -ParseKeyResult TryParsePublicKey(EVPKeyPointer* pkey, - const BIOPointer& bp, - const char* name, - F&& parse) { - unsigned char* der_data; - long der_len; // NOLINT(runtime/int) - - // This skips surrounding data and decodes PEM to DER. - { - MarkPopErrorOnReturn mark_pop_error_on_return; - if (PEM_bytes_read_bio(&der_data, &der_len, nullptr, name, - bp.get(), nullptr, nullptr) != 1) - return ParseKeyResult::kParseKeyNotRecognized; - } - - // OpenSSL might modify the pointer, so we need to make a copy before parsing. - const unsigned char* p = der_data; - pkey->reset(parse(&p, der_len)); - OPENSSL_clear_free(der_data, der_len); - - return *pkey ? ParseKeyResult::kParseKeyOk : - ParseKeyResult::kParseKeyFailed; -} - -ParseKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey, - const char* key_pem, - int key_pem_len) { - auto bp = BIOPointer::New(key_pem, key_pem_len); - if (!bp) - return ParseKeyResult::kParseKeyFailed; - - ParseKeyResult ret; - - // Try parsing as a SubjectPublicKeyInfo first. - ret = TryParsePublicKey(pkey, bp, "PUBLIC KEY", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - return d2i_PUBKEY(nullptr, p, l); - }); - if (ret != ParseKeyResult::kParseKeyNotRecognized) - return ret; - - // Maybe it is PKCS#1. - CHECK(bp.resetBio()); - ret = TryParsePublicKey(pkey, bp, "RSA PUBLIC KEY", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); - }); - if (ret != ParseKeyResult::kParseKeyNotRecognized) - return ret; - - // X.509 fallback. - CHECK(bp.resetBio()); - return TryParsePublicKey(pkey, bp, "CERTIFICATE", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - X509Pointer x509(d2i_X509(nullptr, p, l)); - return x509 ? X509_get_pubkey(x509.get()) : nullptr; - }); -} - -ParseKeyResult ParsePublicKey(EVPKeyPointer* pkey, - const PublicKeyEncodingConfig& config, - const char* key, - size_t key_len) { - auto res = EVPKeyPointer::TryParsePublicKey( - static_cast(config.format_), - static_cast(config.type_.value()), - ncrypto::Buffer{ - .data = reinterpret_cast(key), - .len = key_len, - }); - if (!res) return static_cast(res.error.value()); - - CHECK(res.has_value); - *pkey = std::move(res.value); - return ParseKeyResult::kParseKeyOk; -} - -bool IsASN1Sequence(const unsigned char* data, size_t size, - size_t* data_offset, size_t* data_size) { - if (size < 2 || data[0] != 0x30) - return false; - - if (data[1] & 0x80) { - // Long form. - size_t n_bytes = data[1] & ~0x80; - if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) - return false; - size_t length = 0; - for (size_t i = 0; i < n_bytes; i++) - length = (length << 8) | data[i + 2]; - *data_offset = 2 + n_bytes; - *data_size = std::min(size - 2 - n_bytes, length); - } else { - // Short form. - *data_offset = 2; - *data_size = std::min(size - 2, data[1]); - } - - return true; -} - -bool IsRSAPrivateKey(const unsigned char* data, size_t size) { - // Both RSAPrivateKey and RSAPublicKey structures start with a SEQUENCE. - size_t offset, len; - if (!IsASN1Sequence(data, size, &offset, &len)) - return false; - - // An RSAPrivateKey sequence always starts with a single-byte integer whose - // value is either 0 or 1, whereas an RSAPublicKey starts with the modulus - // (which is the product of two primes and therefore at least 4), so we can - // decide the type of the structure based on the first three bytes of the - // sequence. - return len >= 3 && - data[offset] == 2 && - data[offset + 1] == 1 && - !(data[offset + 2] & 0xfe); -} - -bool IsEncryptedPrivateKeyInfo(const unsigned char* data, size_t size) { - // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE. - size_t offset, len; - if (!IsASN1Sequence(data, size, &offset, &len)) - return false; - - // A PrivateKeyInfo sequence always starts with an integer whereas an - // EncryptedPrivateKeyInfo starts with an AlgorithmIdentifier. - return len >= 1 && - data[offset] != 2; -} - -ParseKeyResult ParsePrivateKey(EVPKeyPointer* pkey, - const PrivateKeyEncodingConfig& config, - const char* key, - size_t key_len) { - const ByteSource* passphrase = config.passphrase_.get(); - - if (config.format_ == kKeyFormatPEM) { - auto bio = BIOPointer::New(key, key_len); - if (!bio) - return ParseKeyResult::kParseKeyFailed; - - pkey->reset(PEM_read_bio_PrivateKey(bio.get(), - nullptr, - PasswordCallback, - &passphrase)); - } else { - CHECK_EQ(config.format_, kKeyFormatDER); - - if (config.type_.value() == kKeyEncodingPKCS1) { - const unsigned char* p = reinterpret_cast(key); - pkey->reset(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, key_len)); - } else if (config.type_.value() == kKeyEncodingPKCS8) { - auto bio = BIOPointer::New(key, key_len); - if (!bio) - return ParseKeyResult::kParseKeyFailed; - - if (IsEncryptedPrivateKeyInfo( - reinterpret_cast(key), key_len)) { - pkey->reset(d2i_PKCS8PrivateKey_bio(bio.get(), - nullptr, - PasswordCallback, - &passphrase)); - } else { - PKCS8Pointer p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(bio.get(), nullptr)); - if (p8inf) - pkey->reset(EVP_PKCS82PKEY(p8inf.get())); - } - } else { - CHECK_EQ(config.type_.value(), kKeyEncodingSEC1); - const unsigned char* p = reinterpret_cast(key); - pkey->reset(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, key_len)); - } - } - - // OpenSSL can fail to parse the key but still return a non-null pointer. - unsigned long err = ERR_peek_error(); // NOLINT(runtime/int) - if (err != 0) - pkey->reset(); - - if (*pkey) - return ParseKeyResult::kParseKeyOk; - if (ERR_GET_LIB(err) == ERR_LIB_PEM && - ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ) { - if (config.passphrase_.IsEmpty()) - return ParseKeyResult::kParseKeyNeedPassphrase; - } - return ParseKeyResult::kParseKeyFailed; -} - MaybeLocal BIOToStringOrBuffer(Environment* env, const BIOPointer& bio, PKFormatType format) { @@ -541,6 +351,40 @@ Maybe GetAsymmetricKeyDetail(Environment* env, THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); return Nothing(); } + +KeyObjectData TryParsePrivateKey( + Environment* env, + const PrivateKeyEncodingConfig& config, + const ncrypto::Buffer& buffer) { + std::optional> maybePassphrase = std::nullopt; + if (config.passphrase_.get() != nullptr) { + maybePassphrase = ncrypto::Buffer{ + .data = const_cast(config.passphrase_->data()), + .len = config.passphrase_->size(), + }; + } + + auto res = EVPKeyPointer::TryParsePrivateKey( + static_cast(config.format_), + static_cast( + config.type_.value_or(kKeyEncodingPKCS8)), + std::move(maybePassphrase), + buffer); + + if (!res) { + if (res.error.value() == EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + THROW_ERR_MISSING_PASSPHRASE(env, + "Passphrase required for encrypted key"); + return {}; + } + ThrowCryptoError( + env, res.openssl_error.value_or(0), "Failed to read private key"); + return {}; + } + + return KeyObjectData::CreateAsymmetric(KeyType::kKeyTypePrivate, + std::move(res.value)); +} } // namespace // This maps true to JustVoid and false to Nothing(). @@ -672,24 +516,23 @@ KeyObjectData KeyObjectData::GetPrivateKeyFromJs( ByteSource key = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); NonCopyableMaybe config = GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput); - if (config.IsEmpty()) return {}; - EVPKeyPointer pkey; - ParseKeyResult ret = - ParsePrivateKey(&pkey, config.Release(), key.data(), key.size()); - return GetParsedKey(KeyType::kKeyTypePrivate, - env, - std::move(pkey), - ret, - "Failed to read private key"); - } else { - CHECK(args[*offset]->IsObject() && allow_key_object); - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), KeyObjectData()); - CHECK_EQ(key->Data().GetKeyType(), kKeyTypePrivate); - (*offset) += 4; - return key->Data().addRef(); + if (config.IsEmpty()) return {}; + return TryParsePrivateKey( + env, + config.Release(), + ncrypto::Buffer{ + .data = reinterpret_cast(key.data()), + .len = key.size(), + }); } + + CHECK(args[*offset]->IsObject() && allow_key_object); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), KeyObjectData()); + CHECK_EQ(key->Data().GetKeyType(), kKeyTypePrivate); + (*offset) += 4; + return key->Data().addRef(); } KeyObjectData KeyObjectData::GetPublicOrPrivateKeyFromJs( @@ -705,59 +548,77 @@ KeyObjectData KeyObjectData::GetPublicOrPrivateKeyFromJs( KeyObjectData::GetPrivateKeyEncodingFromJs( args, offset, kKeyContextInput); if (config_.IsEmpty()) return {}; - - ParseKeyResult ret; PrivateKeyEncodingConfig config = config_.Release(); - EVPKeyPointer pkey; - KeyType type = KeyType::kKeyTypePublic; + + ncrypto::Buffer buffer = { + .data = reinterpret_cast(data.data()), + .len = data.size(), + }; + + std::optional> maybePassphrase = std::nullopt; + if (config.passphrase_.get() != nullptr) { + maybePassphrase = ncrypto::Buffer{ + .data = const_cast(config.passphrase_->data()), + .len = config.passphrase_->size(), + }; + } + if (config.format_ == kKeyFormatPEM) { // For PEM, we can easily determine whether it is a public or private key // by looking for the respective PEM tags. - ret = ParsePublicKeyPEM(&pkey, data.data(), data.size()); - if (ret == ParseKeyResult::kParseKeyNotRecognized) { - type = KeyType::kKeyTypePrivate; - ret = ParsePrivateKey(&pkey, config, data.data(), data.size()); + auto res = EVPKeyPointer::TryParsePublicKeyPEM(buffer); + if (!res) { + if (res.error.value() == EVPKeyPointer::PKParseError::NOT_RECOGNIZED) { + return TryParsePrivateKey(env, config, buffer); + } + ThrowCryptoError(env, + res.openssl_error.value_or(0), + "Failed to read asymmetric key"); + return {}; } - return GetParsedKey( - type, env, std::move(pkey), ret, "Failed to read asymmetric key"); + return CreateAsymmetric(kKeyTypePublic, std::move(res.value)); } // For DER, the type determines how to parse it. SPKI, PKCS#8 and SEC1 are // easy, but PKCS#1 can be a public key or a private key. - bool is_public; - switch (config.type_.value()) { - case kKeyEncodingPKCS1: - is_public = !IsRSAPrivateKey( - reinterpret_cast(data.data()), data.size()); - break; - case kKeyEncodingSPKI: - is_public = true; - break; - case kKeyEncodingPKCS8: - case kKeyEncodingSEC1: - is_public = false; - break; - default: - UNREACHABLE("Invalid key encoding type"); - } + bool is_public = ([&] { + switch (config.type_.value()) { + case kKeyEncodingPKCS1: + return !EVPKeyPointer::IsRSAPrivateKey(buffer); + case kKeyEncodingSPKI: + return true; + case kKeyEncodingPKCS8: + return false; + case kKeyEncodingSEC1: + return false; + default: + UNREACHABLE("Invalid key encoding type"); + } + })(); if (is_public) { - ret = ParsePublicKey(&pkey, config, data.data(), data.size()); - } else { - type = KeyType::kKeyTypePrivate; - ret = ParsePrivateKey(&pkey, config, data.data(), data.size()); + auto res = EVPKeyPointer::TryParsePublicKey( + static_cast(config.format_), + static_cast(config.type_.value()), + buffer); + if (!res) { + ThrowCryptoError(env, + res.openssl_error.value_or(0), + "Failed to read asymmetric key"); + return {}; + } + return CreateAsymmetric(KeyType::kKeyTypePublic, std::move(res.value)); } - return GetParsedKey( - type, env, std::move(pkey), ret, "Failed to read asymmetric key"); - } else { - CHECK(args[*offset]->IsObject()); - KeyObjectHandle* key = Unwrap(args[*offset].As()); - CHECK_NOT_NULL(key); - CHECK_NE(key->Data().GetKeyType(), kKeyTypeSecret); - (*offset) += 4; - return key->Data().addRef(); + return TryParsePrivateKey(env, config, buffer); } + + CHECK(args[*offset]->IsObject()); + KeyObjectHandle* key = Unwrap(args[*offset].As()); + CHECK_NOT_NULL(key); + CHECK_NE(key->Data().GetKeyType(), kKeyTypeSecret); + (*offset) += 4; + return key->Data().addRef(); } KeyObjectData KeyObjectData::GetParsedKey(KeyType type,