diff --git a/tool-openssl/CMakeLists.txt b/tool-openssl/CMakeLists.txt index 5834ad284b..7d7493b443 100644 --- a/tool-openssl/CMakeLists.txt +++ b/tool-openssl/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable( crl.cc dgst.cc + genrsa.cc pkcs8.cc pkey.cc rehash.cc @@ -87,6 +88,8 @@ if(BUILD_TESTING) crl_test.cc dgst.cc dgst_test.cc + genrsa.cc + genrsa_test.cc pkcs8.cc pkcs8_test.cc pkey.cc diff --git a/tool-openssl/genrsa.cc b/tool-openssl/genrsa.cc new file mode 100644 index 0000000000..78fcde66f9 --- /dev/null +++ b/tool-openssl/genrsa.cc @@ -0,0 +1,181 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include +#include +#include +#include "internal.h" + +static const unsigned kDefaultKeySize = 2048; +static const unsigned kMinKeySize = 1024; +static const unsigned kRecommendedMaxKeySize = 16384; +static const char kKeyArgName[] = "key_size"; + +static const argument_t kArguments[] = { + {"-help", kBooleanArgument, "Display this summary"}, + {"-out", kOptionalArgument, "Output file to write the key to"}, + {"", kOptionalArgument, ""}}; + +static void DisplayHelp(BIO *bio) { + BIO_printf(bio, "Usage: genrsa [options] numbits\n\n"); + BIO_printf(bio, "Options:\n"); + + for (size_t i = 0; kArguments[i].name[0] != '\0'; i++) { + BIO_printf(bio, " %-20s %s\n", kArguments[i].name, + kArguments[i].description); + } + BIO_printf(bio, "\n numbits Size of key in bits (default: %u)\n", + kDefaultKeySize); +} + +static bool ParseKeySize(const args_list_t &extra_args, unsigned &KeySizeBits) { + KeySizeBits = kDefaultKeySize; + + if (extra_args.empty()) { + return true; + } + + if (extra_args.size() > 1) { + fprintf(stderr, "Error: Only one key size argument allowed\n"); + return false; + } + + ordered_args::ordered_args_map_t temp_args; + temp_args.push_back(std::make_pair(kKeyArgName, extra_args[0])); + + if (!ordered_args::GetUnsigned(&KeySizeBits, kKeyArgName, 0, temp_args)) { + fprintf(stderr, "Error: Invalid key size '%s'\n", extra_args[0].c_str()); + return false; + } + + if (KeySizeBits < kMinKeySize) { + fprintf(stderr, "Error: Key size must be at least %u bits\n", kMinKeySize); + return false; + } + + if (KeySizeBits > kRecommendedMaxKeySize) { + fprintf(stderr, "Warning: It is not recommended to use more than %u bits for RSA keys.\n", kRecommendedMaxKeySize); + fprintf(stderr, " Your key size is %u! Larger key sizes may not behave as expected.\n", KeySizeBits); + } + + return true; +} + +static bssl::UniquePtr GenerateRSAKey(unsigned bits) { + bssl::UniquePtr pkey; + EVP_PKEY *raw_pkey = nullptr; + bssl::UniquePtr ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)); + if (!ctx || !EVP_PKEY_keygen_init(ctx.get()) || + !EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), bits)) { + return pkey; + } + + if (!EVP_PKEY_keygen(ctx.get(), &raw_pkey)) { + return pkey; + } + + pkey.reset(raw_pkey); + return pkey; +} + +static bssl::UniquePtr CreateOutputBIO(const std::string &out_path) { + bssl::UniquePtr bio; + if (out_path.empty()) { + bio.reset(BIO_new_fp(stdout, BIO_NOCLOSE)); + if (!bio) { + fprintf(stderr, "Error: Could not create BIO for stdout\n"); + return nullptr; + } + } else { + bio.reset(BIO_new_file(out_path.c_str(), "wb")); + if (!bio) { + fprintf(stderr, "Error: Could not open output file '%s'\n", + out_path.c_str()); + return nullptr; + } + } + return bio; +} + +bool genrsaTool(const args_list_t &args) { + ordered_args::ordered_args_map_t parsed_args; + args_list_t extra_args{}; + std::string out_path; + bool help = false; + bssl::UniquePtr bio; + bssl::UniquePtr pkey; + unsigned KeySizeBits = 0; + + // Parse command line arguments + if (!ordered_args::ParseOrderedKeyValueArguments(parsed_args, extra_args, + args, kArguments)) { + bio.reset(BIO_new_fp(stderr, BIO_NOCLOSE)); + if (bio) { + DisplayHelp(bio.get()); + } + goto err; + } + + ordered_args::GetBoolArgument(&help, "-help", parsed_args); + ordered_args::GetString(&out_path, "-out", "", parsed_args); + + // Parse and validate key size first (catches multiple key sizes) + if (!ParseKeySize(extra_args, KeySizeBits)) { + goto err; + } + + // Simple validation that numbits is the last argument + if (!extra_args.empty() && args[args.size()-1] != extra_args[0]) { + fprintf(stderr, + "Error: Key size must be specified after all options\n"); + fprintf(stderr, "Usage: genrsa [options] numbits\n"); + goto err; + } + + // Handle help request + if (help) { + bio.reset(BIO_new_fp(stdout, BIO_NOCLOSE)); + if (!bio) { + goto err; + } + DisplayHelp(bio.get()); + return true; // Help display is a successful exit + } + + // Set up output BIO + bio = CreateOutputBIO(out_path); + if (!bio) { + goto err; + } + + // Generate RSA key + pkey = GenerateRSAKey(KeySizeBits); + if (!pkey) { + fprintf(stderr, "Error: Failed to generate RSA key\n"); + goto err; + } + + // Write the key + if (!PEM_write_bio_PrivateKey(bio.get(), pkey.get(), NULL, NULL, 0, NULL, + NULL)) { + goto err; + } + + // Flush output + if (!BIO_flush(bio.get())) { + goto err; + } + + return true; + +err: + ERR_print_errors_fp(stderr); + if (bio) { + BIO_flush(bio.get()); + } + return false; +} diff --git a/tool-openssl/genrsa_test.cc b/tool-openssl/genrsa_test.cc new file mode 100644 index 0000000000..bf6e6ea651 --- /dev/null +++ b/tool-openssl/genrsa_test.cc @@ -0,0 +1,226 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../crypto/test/test_util.h" +#include "internal.h" +#include "test_util.h" + + +const std::vector kStandardKeySizes = {1024, 2048, 3072, 4096}; + + +class GenRSATestBase : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_GT(createTempFILEpath(out_path_tool), 0u) + << "Failed to create temporary file path for tool output"; + ASSERT_GT(createTempFILEpath(out_path_openssl), 0u) + << "Failed to create temporary file path for OpenSSL output"; + + awslc_executable_path = getenv("AWSLC_TOOL_PATH"); + openssl_executable_path = getenv("OPENSSL_TOOL_PATH"); + } + + void TearDown() override { + RemoveFile(out_path_tool); + RemoveFile(out_path_openssl); + } + + bool ValidateKeyFile(const char *path, unsigned expected_bits = 0) { + if (!path) { + ADD_FAILURE() << "Path parameter is null"; + return false; + } + + bssl::UniquePtr bio(BIO_new_file(path, "rb")); + if (!bio) { + ADD_FAILURE() << "Failed to open key file: " << path; + return false; + } + + bssl::UniquePtr rsa( + PEM_read_bio_RSAPrivateKey(bio.get(), nullptr, nullptr, nullptr)); + if (!rsa) { + ADD_FAILURE() << "Failed to parse RSA key from PEM file"; + return false; + } + + if (RSA_check_key(rsa.get()) != 1) { + ADD_FAILURE() << "RSA key failed consistency check"; + return false; + } + + if (expected_bits > 0) { + unsigned actual_bits = RSA_bits(rsa.get()); + if (actual_bits != expected_bits) { + ADD_FAILURE() << "Key size mismatch. Expected: " << expected_bits + << " bits, Got: " << actual_bits << " bits"; + return false; + } + } + + return true; + } + + bool GenerateKey(unsigned key_size, const char *output_path = nullptr) { + args_list_t args; + if (output_path) { + args = {"-out", output_path, std::to_string(key_size)}; + } else { + args = {std::to_string(key_size)}; + } + return genrsaTool(args); + } + + bool HasCrossCompatibilityTools() { + return awslc_executable_path != nullptr && + openssl_executable_path != nullptr; + } + + char out_path_tool[PATH_MAX]; + char out_path_openssl[PATH_MAX]; + const char *awslc_executable_path = nullptr; + const char *openssl_executable_path = nullptr; +}; + + +class GenRSATest : public GenRSATestBase {}; + + +class GenRSAParamTest : public GenRSATestBase, + public ::testing::WithParamInterface {}; + + +TEST_P(GenRSAParamTest, GeneratesKeyFile) { + unsigned key_size = GetParam(); + + // FIPS builds require 2048-bit minimum - check both compile-time and runtime + bool is_fips = false; +#if defined(BORINGSSL_FIPS) + is_fips = true; +#endif + is_fips = is_fips || FIPS_mode(); + + if (is_fips && key_size < 2048) { + GTEST_SKIP() << "Skipping " << key_size << "-bit key test in FIPS build/mode (minimum 2048 bits required)"; + } + + EXPECT_TRUE(GenerateKey(key_size, out_path_tool)) << "Key generation failed"; + EXPECT_TRUE(ValidateKeyFile(out_path_tool, key_size)) + << "Generated key file validation failed"; +} + + +TEST_P(GenRSAParamTest, OpenSSLCompatibility) { + unsigned key_size = GetParam(); + + // FIPS builds require 2048-bit minimum - check both compile-time and runtime + bool is_fips = false; +#if defined(BORINGSSL_FIPS) + is_fips = true; +#endif + is_fips = is_fips || FIPS_mode(); + + if (is_fips && key_size < 2048) { + GTEST_SKIP() << "Skipping " << key_size << "-bit key test in FIPS build/mode (minimum 2048 bits required)"; + } + + if (!HasCrossCompatibilityTools()) { + GTEST_SKIP() << "Skipping test: AWSLC_TOOL_PATH and/or OPENSSL_TOOL_PATH " + "environment variables are not set"; + return; + } + + EXPECT_TRUE(GenerateKey(key_size, out_path_tool)) + << "AWS-LC key generation failed"; + + std::string verify_cmd = std::string(openssl_executable_path) + " rsa -in " + + out_path_tool + " -check -noout"; + EXPECT_EQ(system(verify_cmd.c_str()), 0) << "OpenSSL verification failed"; +} + +INSTANTIATE_TEST_SUITE_P(StandardKeySizes, GenRSAParamTest, + ::testing::ValuesIn(kStandardKeySizes)); + +TEST_F(GenRSATest, DefaultKeyGeneration) { + args_list_t args{"-out", out_path_tool}; + EXPECT_TRUE(genrsaTool(args)) << "Default key generation failed"; + EXPECT_TRUE(ValidateKeyFile(out_path_tool)) + << "Default key file validation failed"; +} + +TEST_F(GenRSATest, HelpOption) { + args_list_t args{"-help"}; + EXPECT_TRUE(genrsaTool(args)) << "Help command failed"; +} + +TEST_F(GenRSATest, StdoutOutput) { + // This test verifies the CLI can output to stdout + // We can't easily capture stdout in this test framework, + // but we can verify the command succeeds + EXPECT_TRUE(GenerateKey(2048)) << "Stdout key generation failed"; +} + +TEST_F(GenRSATest, FileOutput) { + // Test file output + args_list_t file_args{"-out", out_path_tool, "2048"}; + EXPECT_TRUE(genrsaTool(file_args)) << "File output failed"; + EXPECT_TRUE(ValidateKeyFile(out_path_tool)) + << "File output validation failed"; +} + +TEST_F(GenRSATest, ArgumentParsingErrors) { + // Test incorrect argument order + { + args_list_t args{"2048", "-out", out_path_tool}; + EXPECT_FALSE(genrsaTool(args)) + << "Command should fail with incorrect argument order"; + } + + // Test invalid key size + { + args_list_t args{"-out", out_path_tool, "invalid"}; + EXPECT_FALSE(genrsaTool(args)) + << "Command should fail with invalid key size"; + } + + // Test zero key size + { + args_list_t args{"-out", out_path_tool, "0"}; + EXPECT_FALSE(genrsaTool(args)) << "Command should fail with zero key size"; + } +} + +TEST_F(GenRSATest, FileIOErrors) { + // Test invalid output path + { + args_list_t args{"-out", "/nonexistent/directory/key.pem", "2048"}; + EXPECT_FALSE(genrsaTool(args)) + << "Command should fail with invalid output path"; + } +} + +TEST_F(GenRSATest, ArgumentValidation) { + // Test missing key size (should use default) + { + args_list_t args{"-out", out_path_tool}; + EXPECT_TRUE(genrsaTool(args)) << "Default key size should work"; + EXPECT_TRUE(ValidateKeyFile(out_path_tool)) + << "Default key should be valid"; + } + + // Test help takes precedence + { + args_list_t args{"-help", "-out", out_path_tool, "2048"}; + EXPECT_TRUE(genrsaTool(args)) << "Help should work even with other args"; + } +} diff --git a/tool-openssl/internal.h b/tool-openssl/internal.h index 26af3230b6..c8212205c5 100644 --- a/tool-openssl/internal.h +++ b/tool-openssl/internal.h @@ -37,6 +37,7 @@ tool_func_t FindTool(int argc, char **argv, int &starting_arg); bool CRLTool(const args_list_t &args); bool dgstTool(const args_list_t &args); +bool genrsaTool(const args_list_t &args); bool md5Tool(const args_list_t &args); bool pkcs8Tool(const args_list_t &args); bool pkeyTool(const args_list_t &args); diff --git a/tool-openssl/tool.cc b/tool-openssl/tool.cc index 4ef0fa14cd..76718feb92 100644 --- a/tool-openssl/tool.cc +++ b/tool-openssl/tool.cc @@ -15,9 +15,10 @@ #include "./internal.h" -static const std::array kTools = {{ +static const std::array kTools = {{ {"crl", CRLTool}, {"dgst", dgstTool}, + {"genrsa", genrsaTool}, {"md5", md5Tool}, {"pkcs8", pkcs8Tool}, {"pkey", pkeyTool}, @@ -104,7 +105,7 @@ int main(int argc, char **argv) { return 1; } - args_list_t args; + args_list_t args{}; for (int i = starting_arg; i < argc; i++) { args.emplace_back(argv[i]); }