Skip to content

Commit

Permalink
feat: Add Generate and SaveToPEM methods to crypto::PrivateKey
Browse files Browse the repository at this point in the history
Ticket: MEN-6657
Changelog: None

Signed-off-by: Lluis Campos <[email protected]>
  • Loading branch information
lluiscampos committed Sep 12, 2023
1 parent 61fd3f4 commit b0c035d
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
/compile
/config.guess
/config.h
/common/crypto/platform/openssl/openssl_config.h
/config.log
/config.status
/config.sub
Expand Down
9 changes: 9 additions & 0 deletions common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ if(NOT ${OpenSSL_Found})
message(FATAL_ERROR "OpenSSL not found during build")
endif()

if ("${OPENSSL_VERSION}" VERSION_LESS 3.0.0)
set(MENDER_CRYPTO_OPENSSL_LEGACY 1)
else()
set(MENDER_CRYPTO_OPENSSL_LEGACY 0)
endif()

configure_file(crypto/platform/openssl/openssl_config.h.in crypto/platform/openssl/openssl_config.h)

add_library(common_http STATIC http/http.cpp http/platform/beast/http.cpp)
# Note: Use SYSTEM include style, since Boost triggers some of our warnings.
target_include_directories(common_http SYSTEM PUBLIC ${Boost_INCLUDE_DIRS})
Expand Down Expand Up @@ -256,6 +264,7 @@ target_link_libraries(common_crypto PUBLIC
add_executable(crypto_test EXCLUDE_FROM_ALL crypto_test.cpp)
target_link_libraries(crypto_test PUBLIC
common_crypto
common_testing
main_test
gmock
)
Expand Down
3 changes: 3 additions & 0 deletions common/crypto.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class PrivateKey {
public:
static ExpectedPrivateKey LoadFromPEM(const string &private_key_path, const string &passphrase);
static ExpectedPrivateKey LoadFromPEM(const string &private_key_path);
static ExpectedPrivateKey Generate(const unsigned int bits, const unsigned int exponent);
static ExpectedPrivateKey Generate(const unsigned int bits);
error::Error SaveToPEM(const string &private_key_path);
#ifdef MENDER_CRYPTO_OPENSSL
unique_ptr<EVP_PKEY, void (*)(EVP_PKEY *)> key;

Expand Down
122 changes: 122 additions & 0 deletions common/crypto/platform/openssl/crypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@
#include <common/error.hpp>
#include <common/expected.hpp>
#include <common/common.hpp>
#include <common/crypto/platform/openssl/openssl_config.h>

#include <artifact/sha/sha.hpp>

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/rsa.h>
#include <openssl/bn.h>


namespace mender {
Expand All @@ -40,6 +42,8 @@ const size_t MENDER_DIGEST_SHA256_LENGTH = 32;

const size_t OPENSSL_SUCCESS = 1;

const int MENDER_DEFAULT_RSA_EXPONENT = 0x10001;

using namespace std;

namespace error = mender::common::error;
Expand All @@ -65,6 +69,13 @@ auto bio_free_all_func = [](BIO *bio) {
BIO_free_all(bio);
}
};
#ifdef MENDER_CRYPTO_OPENSSL_LEGACY
auto bn_free = [](BIGNUM *bn) {
if (bn) {
BN_free(bn);
}
};
#endif

// NOTE: GetOpenSSLErrorMessage should be called upon all OpenSSL errors, as
// the errors are queued, and if not harvested, the FIFO structure of the
Expand Down Expand Up @@ -139,6 +150,96 @@ ExpectedPrivateKey PrivateKey::LoadFromPEM(const string &private_key_path) {
return PrivateKey::LoadFromPEM(private_key_path, "");
}

ExpectedPrivateKey PrivateKey::Generate(const unsigned int bits, const unsigned int exponent) {
#ifdef MENDER_CRYPTO_OPENSSL_LEGACY
auto pkey_gen_ctx = unique_ptr<EVP_PKEY_CTX, void (*)(EVP_PKEY_CTX *)>(
EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr), pkey_ctx_free_func);

int ret = EVP_PKEY_keygen_init(pkey_gen_ctx.get());
if (ret != OPENSSL_SUCCESS) {
return expected::unexpected(MakeError(
SetupError,
"Failed to generate a private key. Initialization failed: "
+ GetOpenSSLErrorMessage()));
}

ret = EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_gen_ctx.get(), bits);
if (ret != OPENSSL_SUCCESS) {
return expected::unexpected(MakeError(
SetupError,
"Failed to generate a private key. Parameters setting failed: "
+ GetOpenSSLErrorMessage()));
}

auto exponent_bn = unique_ptr<BIGNUM, void (*)(BIGNUM *)>(BN_new(), bn_free);
ret = BN_set_word(exponent_bn.get(), exponent);
if (ret != OPENSSL_SUCCESS) {
return expected::unexpected(MakeError(
SetupError,
"Failed to generate a private key. Parameters setting failed: "
+ GetOpenSSLErrorMessage()));
}

ret = EVP_PKEY_CTX_set_rsa_keygen_pubexp(pkey_gen_ctx.get(), exponent_bn.get());
if (ret != OPENSSL_SUCCESS) {
return expected::unexpected(MakeError(
SetupError,
"Failed to generate a private key. Parameters setting failed: "
+ GetOpenSSLErrorMessage()));
}
exponent_bn.release();

EVP_PKEY *pkey = nullptr;
ret = EVP_PKEY_keygen(pkey_gen_ctx.get(), &pkey);
if (ret != OPENSSL_SUCCESS) {
return expected::unexpected(MakeError(
SetupError,
"Failed to generate a private key. Generation failed: " + GetOpenSSLErrorMessage()));
}
#else
auto pkey_gen_ctx = unique_ptr<EVP_PKEY_CTX, void (*)(EVP_PKEY_CTX *)>(
EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr), pkey_ctx_free_func);

int ret = EVP_PKEY_keygen_init(pkey_gen_ctx.get());
if (ret != OPENSSL_SUCCESS) {
return expected::unexpected(MakeError(
SetupError,
"Failed to generate a private key. Initialization failed: "
+ GetOpenSSLErrorMessage()));
}

OSSL_PARAM params[3];
auto bits_buffer = bits;
auto exponent_buffer = exponent;
params[0] = OSSL_PARAM_construct_uint("bits", &bits_buffer);
params[1] = OSSL_PARAM_construct_uint("e", &exponent_buffer);
params[2] = OSSL_PARAM_construct_end();

ret = EVP_PKEY_CTX_set_params(pkey_gen_ctx.get(), params);
if (ret != OPENSSL_SUCCESS) {
return expected::unexpected(MakeError(
SetupError,
"Failed to generate a private key. Parameters setting failed: "
+ GetOpenSSLErrorMessage()));
}

EVP_PKEY *pkey = nullptr;
ret = EVP_PKEY_generate(pkey_gen_ctx.get(), &pkey);
if (ret != OPENSSL_SUCCESS) {
return expected::unexpected(MakeError(
SetupError,
"Failed to generate a private key. Generation failed: " + GetOpenSSLErrorMessage()));
}
#endif

auto private_key = unique_ptr<EVP_PKEY, void (*)(EVP_PKEY *)>(pkey, pkey_free_func);
return unique_ptr<PrivateKey>(new PrivateKey(std::move(private_key)));
}

ExpectedPrivateKey PrivateKey::Generate(const unsigned int bits) {
return PrivateKey::Generate(bits, MENDER_DEFAULT_RSA_EXPONENT);
}

expected::ExpectedString EncodeBase64(vector<uint8_t> to_encode) {
// Predict the len of the decoded for later verification. From man page:
// For every 3 bytes of input provided 4 bytes of output
Expand Down Expand Up @@ -380,6 +481,27 @@ expected::ExpectedBool VerifySign(
return VerifySignData(public_key_path, shasum, decoded_signature);
}

error::Error PrivateKey::SaveToPEM(const string &private_key_path) {
auto bio_key = unique_ptr<BIO, void (*)(BIO *)>(
BIO_new_file(private_key_path.c_str(), "w"), bio_free_func);
if (bio_key == nullptr) {
return MakeError(
SetupError, "Failed to open the private key file: " + GetOpenSSLErrorMessage());
}

// PEM_write_bio_PrivateKey_traditional will use the key-specific PKCS1
// format if one is available for that key type, otherwise it will encode
// to a PKCS8 key.
auto ret = PEM_write_bio_PrivateKey_traditional(
bio_key.get(), key.get(), nullptr, nullptr, 0, nullptr, nullptr);
if (ret != OPENSSL_SUCCESS) {
return MakeError(
SetupError, "Failed to save the private key to file: " + GetOpenSSLErrorMessage());
}

return error::NoError;
}

} // namespace crypto
} // namespace common
} // namespace mender
1 change: 1 addition & 0 deletions common/crypto/platform/openssl/openssl_config.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#cmakedefine MENDER_CRYPTO_OPENSSL_LEGACY @MENDER_CRYPTO_OPENSSL_LEGACY@
39 changes: 39 additions & 0 deletions common/crypto/testdata/private-key.rsa.traditional.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
41 changes: 34 additions & 7 deletions common/crypto_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <common/testing.hpp>
#include <common/path.hpp>

using namespace std;

namespace mtesting = mender::common::testing;
using testing::HasSubstr;
using testing::StartsWith;

namespace error = mender::common::error;
namespace path = mender::common::path;

namespace mender {
namespace common {
Expand Down Expand Up @@ -149,8 +155,7 @@ TEST(CryptoTest, TestVerifySignInvalid) {
expected_verify_signature =
crypto::VerifySign("non-existing.key", shasum, good_signature_encoded);
ASSERT_FALSE(expected_verify_signature);
EXPECT_THAT(
expected_verify_signature.error().message, testing::HasSubstr("No such file or directory"));
EXPECT_THAT(expected_verify_signature.error().message, HasSubstr("No such file or directory"));
}

TEST(CryptoTest, TestPrivateKeyLoadFromPEMValidRSA) {
Expand All @@ -171,30 +176,29 @@ TEST(CryptoTest, TestPrivateKeyLoadFromPEMFileNotFound) {
// Load non-exsistent private key from PEM
string private_key_file = "./private-non-existent.pem";
auto expected_private_key = PrivateKey::LoadFromPEM(private_key_file);
EXPECT_THAT(
expected_private_key.error().message, testing::HasSubstr("No such file or directory"));
EXPECT_THAT(expected_private_key.error().message, HasSubstr("No such file or directory"));
}

TEST(CryptoTest, TestPrivateKeyLoadFromPEMInvalid) {
// Load corrupted/unsupported private key from PEM
string private_key_file = "./private-corrupted.pem";
auto expected_private_key = PrivateKey::LoadFromPEM(private_key_file);
EXPECT_THAT(expected_private_key.error().message, testing::HasSubstr("Failed to load the key"));
EXPECT_THAT(expected_private_key.error().message, HasSubstr("Failed to load the key"));
}

TEST(CryptoTest, TestPrivateKeyLoadFromPEMNoPassphrase) {
// Load encrypted private key with no password
string private_key_file = "./private-encrypted.pem";
auto expected_private_key = PrivateKey::LoadFromPEM(private_key_file);
EXPECT_THAT(expected_private_key.error().message, testing::HasSubstr("Failed to load the key"));
EXPECT_THAT(expected_private_key.error().message, HasSubstr("Failed to load the key"));
}

TEST(CryptoTest, TestPrivateKeyLoadFromPEMWrongPassphrase) {
// Load encrypted private key with wrong password
string private_key_file = "./private-encrypted.pem";
string passphrase = "dunno";
auto expected_private_key = PrivateKey::LoadFromPEM(private_key_file, passphrase);
EXPECT_THAT(expected_private_key.error().message, testing::HasSubstr("Failed to load the key"));
EXPECT_THAT(expected_private_key.error().message, HasSubstr("Failed to load the key"));
}

TEST(CryptoTest, TestPrivateKeyLoadFromPEMCorrectPassphrase) {
Expand All @@ -205,6 +209,29 @@ TEST(CryptoTest, TestPrivateKeyLoadFromPEMCorrectPassphrase) {
ASSERT_TRUE(expected_private_key) << "Unexpected: " << expected_private_key.error();
}

TEST(CryptoTest, TestPrivateKeyGenerate) {
auto expected_private_key = PrivateKey::Generate(3072);
EXPECT_TRUE(expected_private_key) << "Unexpected: " << expected_private_key.error();

auto expected_private_key_exponent = PrivateKey::Generate(3072, 65539);
EXPECT_TRUE(expected_private_key_exponent)
<< "Unexpected: " << expected_private_key_exponent.error();
}

TEST(CryptoTest, TestPrivateKeySaveToPEM) {
string private_key_file = "./private-key.rsa.traditional.pem";
auto expected_private_key = PrivateKey::LoadFromPEM(private_key_file);
ASSERT_TRUE(expected_private_key) << "Unexpected: " << expected_private_key.error();
auto private_key = std::move(expected_private_key.value());

mtesting::TemporaryDirectory tmpdir;
string tmpfile = path::Join(tmpdir.Path(), "private.key");
auto err = private_key.get()->SaveToPEM(tmpfile);
EXPECT_EQ(error::NoError, err);

EXPECT_TRUE(mtesting::FilesEqual(private_key_file, tmpfile));
}

} // namespace crypto
} // namespace common
} // namespace mender

0 comments on commit b0c035d

Please sign in to comment.