diff --git a/tool-openssl/CMakeLists.txt b/tool-openssl/CMakeLists.txt index 9732b0ccf3..e831809a29 100644 --- a/tool-openssl/CMakeLists.txt +++ b/tool-openssl/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable( crl.cc dgst.cc + ec.cc genrsa.cc pass_util.cc pkcs8.cc @@ -89,6 +90,8 @@ if(BUILD_TESTING) crl_test.cc dgst.cc dgst_test.cc + ec.cc + ec_test.cc genrsa.cc genrsa_test.cc pass_util.cc diff --git a/tool-openssl/ec.cc b/tool-openssl/ec.cc new file mode 100644 index 0000000000..d9eb60deb2 --- /dev/null +++ b/tool-openssl/ec.cc @@ -0,0 +1,115 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include +#include "internal.h" + +enum Format { + FORMAT_PEM = 1, + FORMAT_DER = 2 +}; + +static const argument_t kArguments[] = { + {"-help", kBooleanArgument, "Display this summary"}, + {"-inform", kOptionalArgument, "Input format (PEM or DER), default PEM"}, + {"-in", kOptionalArgument, "Input file, default stdin"}, + {"-pubout", kBooleanArgument, "Output public key, not private"}, + {"-out", kOptionalArgument, "Output file, default stdout"}, + {"-outform", kOptionalArgument, "Output format (PEM or DER), default PEM"}, + {"", kOptionalArgument, ""}}; + +bool ecTool(const args_list_t &args) { + ordered_args::ordered_args_map_t parsed_args; + args_list_t extra_args; + std::string in_path, out_path, inform_str, outform_str; + bool help = false, pubout = false; + int input_format = FORMAT_PEM, output_format = FORMAT_PEM; + bssl::UniquePtr input_bio, output_bio; + bssl::UniquePtr ec_key; + + if (!ordered_args::ParseOrderedKeyValueArguments(parsed_args, extra_args, + args, kArguments)) { + PrintUsage(kArguments); + goto err; + } + + ordered_args::GetBoolArgument(&help, "-help", parsed_args); + ordered_args::GetString(&in_path, "-in", "", parsed_args); + ordered_args::GetString(&out_path, "-out", "", parsed_args); + ordered_args::GetString(&inform_str, "-inform", "PEM", parsed_args); + ordered_args::GetString(&outform_str, "-outform", "PEM", parsed_args); + ordered_args::GetBoolArgument(&pubout, "-pubout", parsed_args); + + if (help) { + PrintUsage(kArguments); + return true; + } + + if (isStringUpperCaseEqual(inform_str, "DER")) { + input_format = FORMAT_DER; + } else if (isStringUpperCaseEqual(inform_str, "PEM")) { + input_format = FORMAT_PEM; + } else { + fprintf(stderr, "Error: Invalid input format '%s'. Must be PEM or DER\n", inform_str.c_str()); + goto err; + } + + if (isStringUpperCaseEqual(outform_str, "DER")) { + output_format = FORMAT_DER; + } else if (isStringUpperCaseEqual(outform_str, "PEM")) { + output_format = FORMAT_PEM; + } else { + fprintf(stderr, "Error: Invalid output format '%s'. Must be PEM or DER\n", outform_str.c_str()); + goto err; + } + + input_bio.reset(in_path.empty() ? BIO_new_fp(stdin, BIO_NOCLOSE) + : BIO_new_file(in_path.c_str(), "rb")); + if (!input_bio) { + fprintf(stderr, "Error: Could not open input\n"); + goto err; + } + + ec_key.reset(input_format == FORMAT_DER + ? d2i_ECPrivateKey_bio(input_bio.get(), nullptr) + : PEM_read_bio_ECPrivateKey(input_bio.get(), nullptr, + nullptr, nullptr)); + if (!ec_key) { + fprintf(stderr, "Error: Could not read EC key in %s format\n", + input_format == FORMAT_DER ? "DER" : "PEM"); + goto err; + } + + output_bio.reset(out_path.empty() ? BIO_new_fp(stdout, BIO_NOCLOSE) + : BIO_new_file(out_path.c_str(), "wb")); + if (!output_bio) { + fprintf(stderr, "Error: Could not open output\n"); + goto err; + } + + if (pubout) { + if (!(output_format == FORMAT_DER + ? i2d_EC_PUBKEY_bio(output_bio.get(), ec_key.get()) + : PEM_write_bio_EC_PUBKEY(output_bio.get(), ec_key.get()))) { + goto err; + } + } else { + if (!(output_format == FORMAT_DER + ? i2d_ECPrivateKey_bio(output_bio.get(), ec_key.get()) + : PEM_write_bio_ECPrivateKey(output_bio.get(), ec_key.get(), + nullptr, nullptr, 0, nullptr, + nullptr))) { + goto err; + } + } + + return true; + +err: + ERR_print_errors_fp(stderr); + return false; +} diff --git a/tool-openssl/ec_test.cc b/tool-openssl/ec_test.cc new file mode 100644 index 0000000000..814a8045a3 --- /dev/null +++ b/tool-openssl/ec_test.cc @@ -0,0 +1,249 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include "internal.h" +#include "test_util.h" +#include "../crypto/test/test_util.h" + +static EC_KEY* CreateTestECKey() { + bssl::UniquePtr ec_key(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + if (!ec_key || !EC_KEY_generate_key(ec_key.get())) { + return nullptr; + } + return ec_key.release(); +} + +class ECTest : public ::testing::Test { +protected: + void SetUp() override { + ASSERT_GT(createTempFILEpath(pem_key_path), 0u); + ASSERT_GT(createTempFILEpath(der_key_path), 0u); + ASSERT_GT(createTempFILEpath(out_path), 0u); + + tool_executable_path = getenv("AWSLC_TOOL_PATH"); + openssl_executable_path = getenv("OPENSSL_TOOL_PATH"); + + if (tool_executable_path != nullptr && openssl_executable_path != nullptr) { + ASSERT_GT(createTempFILEpath(out_path_openssl), 0u); + + // Use OpenSSL to generate test keys for better cross-compatibility + std::string pem_cmd = std::string(openssl_executable_path) + " ecparam -genkey -name prime256v1 -out " + pem_key_path; + std::string der_cmd = std::string(openssl_executable_path) + " ecparam -genkey -name prime256v1 | " + + std::string(openssl_executable_path) + " ec -outform DER -out " + der_key_path; + + ASSERT_EQ(system(pem_cmd.c_str()), 0) << "Failed to generate PEM key with OpenSSL"; + ASSERT_EQ(system(der_cmd.c_str()), 0) << "Failed to generate DER key with OpenSSL"; + } else { + // Fallback to AWS-LC key generation + ec_key.reset(CreateTestECKey()); + ASSERT_TRUE(ec_key); + + bssl::UniquePtr pem_bio(BIO_new_file(pem_key_path, "wb")); + ASSERT_TRUE(pem_bio); + ASSERT_TRUE(PEM_write_bio_ECPrivateKey(pem_bio.get(), ec_key.get(), nullptr, nullptr, 0, nullptr, nullptr)); + BIO_flush(pem_bio.get()); + + bssl::UniquePtr der_bio(BIO_new_file(der_key_path, "wb")); + ASSERT_TRUE(der_bio); + ASSERT_TRUE(i2d_ECPrivateKey_bio(der_bio.get(), ec_key.get())); + BIO_flush(der_bio.get()); + } + } + + void TearDown() override { + RemoveFile(pem_key_path); + RemoveFile(der_key_path); + RemoveFile(out_path); + if (tool_executable_path != nullptr && openssl_executable_path != nullptr) { + RemoveFile(out_path_openssl); + } + } + + char pem_key_path[PATH_MAX]; + char der_key_path[PATH_MAX]; + char out_path[PATH_MAX]; + char out_path_openssl[PATH_MAX]; + const char* tool_executable_path; + const char* openssl_executable_path; + bssl::UniquePtr ec_key; +}; + +TEST_F(ECTest, ReadPEMOutputPEM) { + args_list_t args = {"-in", pem_key_path, "-out", out_path}; + ASSERT_TRUE(ecTool(args)); + + bssl::UniquePtr out_bio(BIO_new_file(out_path, "rb")); + ASSERT_TRUE(out_bio); + bssl::UniquePtr parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr)); + ASSERT_TRUE(parsed_key); +} + +TEST_F(ECTest, ReadPEMOutputDER) { + args_list_t args = {"-in", pem_key_path, "-outform", "DER", "-out", out_path}; + ASSERT_TRUE(ecTool(args)); + + bssl::UniquePtr out_bio(BIO_new_file(out_path, "rb")); + ASSERT_TRUE(out_bio); + bssl::UniquePtr parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr)); + ASSERT_TRUE(parsed_key); +} + +TEST_F(ECTest, ReadDEROutputPEM) { + args_list_t args = {"-in", der_key_path, "-inform", "DER", "-out", out_path}; + ASSERT_TRUE(ecTool(args)); + + bssl::UniquePtr out_bio(BIO_new_file(out_path, "rb")); + ASSERT_TRUE(out_bio); + bssl::UniquePtr parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr)); + ASSERT_TRUE(parsed_key); +} + +TEST_F(ECTest, ReadDEROutputDER) { + args_list_t args = {"-in", der_key_path, "-inform", "DER", "-outform", "DER", "-out", out_path}; + ASSERT_TRUE(ecTool(args)); + + bssl::UniquePtr out_bio(BIO_new_file(out_path, "rb")); + ASSERT_TRUE(out_bio); + bssl::UniquePtr parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr)); + ASSERT_TRUE(parsed_key); +} + +TEST_F(ECTest, PublicKeyExtractionPEM) { + args_list_t args = {"-in", pem_key_path, "-pubout", "-out", out_path}; + ASSERT_TRUE(ecTool(args)); + + bssl::UniquePtr out_bio(BIO_new_file(out_path, "rb")); + ASSERT_TRUE(out_bio); + bssl::UniquePtr parsed_key(PEM_read_bio_EC_PUBKEY(out_bio.get(), nullptr, nullptr, nullptr)); + ASSERT_TRUE(parsed_key); +} + +TEST_F(ECTest, PublicKeyExtractionDER) { + args_list_t args = {"-in", der_key_path, "-inform", "DER", "-pubout", "-outform", "DER", "-out", out_path}; + ASSERT_TRUE(ecTool(args)); + + bssl::UniquePtr out_bio(BIO_new_file(out_path, "rb")); + ASSERT_TRUE(out_bio); + bssl::UniquePtr parsed_key(d2i_EC_PUBKEY_bio(out_bio.get(), nullptr)); + ASSERT_TRUE(parsed_key); +} + +TEST_F(ECTest, RoundTripPEMtoDERtoPEM) { + char temp_der[PATH_MAX]; + ASSERT_GT(createTempFILEpath(temp_der), 0u); + + // Load original key for comparison + bssl::UniquePtr orig_bio(BIO_new_file(pem_key_path, "rb")); + ASSERT_TRUE(orig_bio); + bssl::UniquePtr orig_key(PEM_read_bio_ECPrivateKey(orig_bio.get(), nullptr, nullptr, nullptr)); + ASSERT_TRUE(orig_key); + + args_list_t args1 = {"-in", pem_key_path, "-outform", "DER", "-out", temp_der}; + ASSERT_TRUE(ecTool(args1)); + + args_list_t args2 = {"-in", temp_der, "-inform", "DER", "-out", out_path}; + ASSERT_TRUE(ecTool(args2)); + + bssl::UniquePtr out_bio(BIO_new_file(out_path, "rb")); + ASSERT_TRUE(out_bio); + bssl::UniquePtr parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr)); + ASSERT_TRUE(parsed_key); + + // Validate key content matches + const BIGNUM *orig_priv = EC_KEY_get0_private_key(orig_key.get()); + const BIGNUM *parsed_priv = EC_KEY_get0_private_key(parsed_key.get()); + ASSERT_EQ(BN_cmp(orig_priv, parsed_priv), 0); + + RemoveFile(temp_der); +} + +TEST_F(ECTest, RoundTripDERtoPEMtoDER) { + char temp_pem[PATH_MAX]; + ASSERT_GT(createTempFILEpath(temp_pem), 0u); + + // Load original key for comparison + bssl::UniquePtr orig_bio(BIO_new_file(der_key_path, "rb")); + ASSERT_TRUE(orig_bio); + bssl::UniquePtr orig_key(d2i_ECPrivateKey_bio(orig_bio.get(), nullptr)); + ASSERT_TRUE(orig_key); + + args_list_t args1 = {"-in", der_key_path, "-inform", "DER", "-out", temp_pem}; + ASSERT_TRUE(ecTool(args1)); + + args_list_t args2 = {"-in", temp_pem, "-outform", "DER", "-out", out_path}; + ASSERT_TRUE(ecTool(args2)); + + bssl::UniquePtr out_bio(BIO_new_file(out_path, "rb")); + ASSERT_TRUE(out_bio); + bssl::UniquePtr parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr)); + ASSERT_TRUE(parsed_key); + + // Validate key content matches + const BIGNUM *orig_priv = EC_KEY_get0_private_key(orig_key.get()); + const BIGNUM *parsed_priv = EC_KEY_get0_private_key(parsed_key.get()); + ASSERT_EQ(BN_cmp(orig_priv, parsed_priv), 0); + + RemoveFile(temp_pem); +} + +TEST_F(ECTest, HelpOption) { + args_list_t args = {"-help"}; + ASSERT_TRUE(ecTool(args)); +} + +TEST_F(ECTest, InvalidInputFile) { + args_list_t args = {"-in", "/nonexistent/file.pem", "-out", out_path}; + ASSERT_FALSE(ecTool(args)); +} + +TEST_F(ECTest, InvalidOutputPath) { + args_list_t args = {"-in", pem_key_path, "-out", "/nonexistent/dir/output.pem"}; + ASSERT_FALSE(ecTool(args)); +} + +TEST_F(ECTest, CompareWithOpenSSLPEMOutput) { + if (tool_executable_path == nullptr || openssl_executable_path == nullptr) { + GTEST_SKIP() << "Skipping test: AWSLC_TOOL_PATH and/or OPENSSL_TOOL_PATH environment variables are not set"; + } + + std::string tool_cmd = std::string(tool_executable_path) + " ec -in " + pem_key_path + " -out " + out_path; + std::string openssl_cmd = std::string(openssl_executable_path) + " ec -in " + pem_key_path + " -out " + out_path_openssl; + + ASSERT_EQ(system(tool_cmd.c_str()), 0); + ASSERT_EQ(system(openssl_cmd.c_str()), 0); + + bssl::UniquePtr tool_bio(BIO_new_file(out_path, "rb")); + bssl::UniquePtr openssl_bio(BIO_new_file(out_path_openssl, "rb")); + ASSERT_TRUE(tool_bio); + ASSERT_TRUE(openssl_bio); + + bssl::UniquePtr tool_key(PEM_read_bio_ECPrivateKey(tool_bio.get(), nullptr, nullptr, nullptr)); + bssl::UniquePtr openssl_key(PEM_read_bio_ECPrivateKey(openssl_bio.get(), nullptr, nullptr, nullptr)); + ASSERT_TRUE(tool_key); + ASSERT_TRUE(openssl_key); +} + +TEST_F(ECTest, CompareWithOpenSSLDEROutput) { + if (tool_executable_path == nullptr || openssl_executable_path == nullptr) { + GTEST_SKIP() << "Skipping test: AWSLC_TOOL_PATH and/or OPENSSL_TOOL_PATH environment variables are not set"; + } + + std::string tool_cmd = std::string(tool_executable_path) + " ec -in " + pem_key_path + " -outform DER -out " + out_path; + std::string openssl_cmd = std::string(openssl_executable_path) + " ec -in " + pem_key_path + " -outform DER -out " + out_path_openssl; + + ASSERT_EQ(system(tool_cmd.c_str()), 0); + ASSERT_EQ(system(openssl_cmd.c_str()), 0); + + bssl::UniquePtr tool_bio(BIO_new_file(out_path, "rb")); + bssl::UniquePtr openssl_bio(BIO_new_file(out_path_openssl, "rb")); + ASSERT_TRUE(tool_bio); + ASSERT_TRUE(openssl_bio); + + bssl::UniquePtr tool_key(d2i_ECPrivateKey_bio(tool_bio.get(), nullptr)); + bssl::UniquePtr openssl_key(d2i_ECPrivateKey_bio(openssl_bio.get(), nullptr)); + ASSERT_TRUE(tool_key); + ASSERT_TRUE(openssl_key); +} diff --git a/tool-openssl/internal.h b/tool-openssl/internal.h index fae0e88656..058cabfbfd 100644 --- a/tool-openssl/internal.h +++ b/tool-openssl/internal.h @@ -92,6 +92,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 ecTool(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 76718feb92..ab2514dc70 100644 --- a/tool-openssl/tool.cc +++ b/tool-openssl/tool.cc @@ -15,10 +15,11 @@ #include "./internal.h" -static const std::array kTools = {{ +static const std::array kTools = {{ {"crl", CRLTool}, {"dgst", dgstTool}, {"genrsa", genrsaTool}, + {"ec", ecTool}, {"md5", md5Tool}, {"pkcs8", pkcs8Tool}, {"pkey", pkeyTool},