diff --git a/tool-openssl/CMakeLists.txt b/tool-openssl/CMakeLists.txt index 9a830e5d03..e1ef92366c 100644 --- a/tool-openssl/CMakeLists.txt +++ b/tool-openssl/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable( dhparam.cc ecparam.cc ec.cc + enc.cc genrsa.cc pass_util.cc pkcs8.cc @@ -99,6 +100,8 @@ if(BUILD_TESTING) ec_test.cc ecparam.cc ecparam_test.cc + enc.cc + enc_test.cc genrsa.cc genrsa_test.cc ordered_args.cc diff --git a/tool-openssl/enc.cc b/tool-openssl/enc.cc new file mode 100644 index 0000000000..3cfe99d873 --- /dev/null +++ b/tool-openssl/enc.cc @@ -0,0 +1,204 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include +#include "../tool/internal.h" +#include "internal.h" + +#define BUF_SIZE 1024 + +static const argument_t kArguments[] = { + // General options + {"-help", kBooleanArgument, "Display option summary"}, + {"-in", kOptionalArgument, "Input file, default stdin"}, + {"-out", kOptionalArgument, "Output file, default stdout"}, + {"-e", kBooleanArgument, "Encrypt"}, + {"-d", kBooleanArgument, "Decrypt"}, + {"-K", kOptionalArgument, "Raw key to use, in hex form"}, + {"-iv", kOptionalArgument, "IV to use, in hex form"}, + {"-aes-128-cbc", kExclusiveBooleanArgument, "Supported cipher"}, + {"", kOptionalArgument, ""}}; + +static bool HexToBinary(bssl::UniquePtr &buffer, + const std::string &hex_string, unsigned int size) { + // First validate that the string contains only valid hex characters + for (char c : hex_string) { + if (!OPENSSL_isxdigit(c)) { + return false; + } + } + + if (hex_string.size() != size * 2) { + return false; + } + + BIGNUM *raw = NULL; + if (BN_hex2bn(&raw, hex_string.c_str()) == 0) { + return false; + } + + int ret = BN_bn2binpad(raw, buffer.get(), size); + BN_free(raw); + return ret != -1; +} + +bool encTool(const args_list_t &args) { + ordered_args::ordered_args_map_t parsed_args; + args_list_t extra_args; + if (!ordered_args::ParseOrderedKeyValueArguments(parsed_args, extra_args, + args, kArguments) || + extra_args.size() > 0) { + PrintUsage(kArguments); + return false; + } + + std::string in_path, out_path, hiv, cipher_name; + bssl::UniquePtr hkey(new std::string); + bool help = false, e = false, d = false; + + 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::GetBoolArgument(&e, "-e", parsed_args); + ordered_args::GetBoolArgument(&d, "-d", parsed_args); + ordered_args::GetString(hkey.get(), "-K", "", parsed_args); + ordered_args::GetString(&hiv, "-iv", "", parsed_args); + ordered_args::GetExclusiveBoolArgument(&cipher_name, kArguments, "", + parsed_args); + + // Display enc tool option summary + if (help) { + PrintUsage(kArguments); + return true; + } + + // Since we do not implement key generation, a raw key is required + // TODO: remove/modify if we ever implement -k, -kfile, or -S + if (hkey->empty()) { + fprintf(stderr, "Error: A raw key is required\n"); + return false; + } + + if (e && d) { + fprintf(stderr, "Error: -e and -d are mutually exclusive\n"); + return false; + } + + bool enc = true; + if (d) { + enc = false; + } + + // Read from stdin if no -in path provided + ScopedFILE in_file; + if (in_path.empty()) { + in_file.reset(stdin); + } else { + in_file.reset(fopen(in_path.c_str(), "rb")); + if (!in_file) { + fprintf(stderr, "Error: unable to load data from '%s'\n", + in_path.c_str()); + return false; + } + } + + if (cipher_name.empty()) { + cipher_name = "aes-128-cbc"; + } else { + cipher_name = cipher_name.substr(1); + } + const EVP_CIPHER *cipher = EVP_get_cipherbyname(cipher_name.c_str()); + + if (cipher == nullptr) { + fprintf(stderr, "Error: Unknown cipher %s\n", cipher_name.c_str()); + return false; + } + + unsigned int iv_length = EVP_CIPHER_iv_length(cipher); + bssl::UniquePtr iv((uint8_t *)OPENSSL_zalloc(EVP_MAX_IV_LENGTH)); + + if (!hiv.empty()) { + if (iv_length == 0) { + fprintf(stderr, "Warning: IV is not used by cipher %s\n", + cipher_name.c_str()); + } else { + if (!HexToBinary(iv, hiv, iv_length)) { + fprintf(stderr, "Error: Invalid hex IV value\n"); + return false; + } + } + } else { + if (iv_length != 0) { + fprintf(stderr, "Error: IV is required for cipher %s\n", + cipher_name.c_str()); + return false; + } + } + + bssl::UniquePtr key((uint8_t *)OPENSSL_zalloc(EVP_MAX_KEY_LENGTH)); + + if (!hkey->empty()) { + if (!HexToBinary(key, *hkey, EVP_CIPHER_key_length(cipher))) { + fprintf(stderr, "Error: Invalid hex key value\n"); + return false; + } + } + + bssl::UniquePtr output_bio; + if (out_path.empty()) { + output_bio.reset(BIO_new_fp(stdout, BIO_NOCLOSE)); + } else { + output_bio.reset(BIO_new(BIO_s_file())); + if (1 != BIO_write_filename(output_bio.get(), out_path.c_str())) { + fprintf(stderr, "Error: unable to write to '%s'\n", out_path.c_str()); + return false; + } + } + + // Create and initialize cipher context + bssl::UniquePtr ctx(EVP_CIPHER_CTX_new()); + if (!EVP_CipherInit_ex(ctx.get(), cipher, nullptr, key.get(), iv.get(), + enc)) { + fprintf(stderr, "Error: Failed to initialize cipher\n"); + return false; + } + + // Process the input file + uint8_t inbuf[BUF_SIZE]; + bssl::UniquePtr outbuf( + (uint8_t *)OPENSSL_zalloc(BUF_SIZE + EVP_CIPHER_block_size(cipher))); + int inlen = 0, outlen = 0; + + for (;;) { + if (feof(in_file.get())) { + break; + } + + inlen = fread(inbuf, 1, sizeof(inbuf), in_file.get()); + + if (ferror(in_file.get())) { + fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); + return false; + } + + if (!EVP_CipherUpdate(ctx.get(), outbuf.get(), &outlen, inbuf, inlen)) { + fprintf(stderr, "Error: Cipher update failed\n"); + return false; + } + BIO_write(output_bio.get(), outbuf.get(), outlen); + } + + // Finalize + if (!EVP_CipherFinal_ex(ctx.get(), outbuf.get(), &outlen)) { + fprintf(stderr, "Error: Cipher final failed\n"); + return false; + } + + BIO_write(output_bio.get(), outbuf.get(), outlen); + + return true; +} diff --git a/tool-openssl/enc_test.cc b/tool-openssl/enc_test.cc new file mode 100644 index 0000000000..60e0d9ea16 --- /dev/null +++ b/tool-openssl/enc_test.cc @@ -0,0 +1,377 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include "internal.h" +#include "test_util.h" + +class EncTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_GT(createTempFILEpath(in_path), 0u); + ASSERT_GT(createTempFILEpath(out_path), 0u); + + // Create test input file with sample data + ScopedFILE in_file(fopen(in_path, "wb")); + ASSERT_TRUE(in_file); + const char *test_data = "Hello, World! This is test data for encryption."; + fwrite(test_data, 1, strlen(test_data), in_file.get()); + } + + void TearDown() override { + RemoveFile(in_path); + RemoveFile(out_path); + } + + char in_path[PATH_MAX]; + char out_path[PATH_MAX]; +}; + +// -------------------- Enc Basic Functionality Tests ------------------------- + +// Test help option +TEST_F(EncTest, EncToolHelpTest) { + args_list_t args = {"-help"}; + bool result = encTool(args); + ASSERT_TRUE(result); +} + +// Test basic encryption with AES-128-CBC +TEST_F(EncTest, EncToolBasicEncryptionTest) { + args_list_t args = {"-e", "-aes-128-cbc", + "-K", "0123456789abcdef0123456789abcdef", + "-iv", "0123456789abcdef0123456789abcdef", + "-in", in_path, + "-out", out_path}; + bool result = encTool(args); + ASSERT_TRUE(result); + + // Verify output file exists and has content + struct stat st; + ASSERT_EQ(stat(out_path, &st), 0); + ASSERT_GT(st.st_size, 0); +} + +// Test basic decryption with AES-128-CBC +TEST_F(EncTest, EncToolBasicDecryptionTest) { + // First encrypt + args_list_t encrypt_args = {"-e", "-aes-128-cbc", + "-K", "0123456789abcdef0123456789abcdef", + "-iv", "0123456789abcdef0123456789abcdef", + "-in", in_path, + "-out", out_path}; + bool result = encTool(encrypt_args); + ASSERT_TRUE(result); + + // Create temp file for decrypted output + char decrypt_path[PATH_MAX]; + ASSERT_GT(createTempFILEpath(decrypt_path), 0u); + + // Then decrypt + args_list_t decrypt_args = {"-d", "-aes-128-cbc", + "-K", "0123456789abcdef0123456789abcdef", + "-iv", "0123456789abcdef0123456789abcdef", + "-in", out_path, + "-out", decrypt_path}; + result = encTool(decrypt_args); + ASSERT_TRUE(result); + + // Verify decrypted content matches original + std::string original = ReadFileToString(in_path); + std::string decrypted = ReadFileToString(decrypt_path); + ASSERT_EQ(original, decrypted); + + RemoveFile(decrypt_path); +} + +// Test decryption with explicit -d flag +TEST_F(EncTest, EncToolExplicitDecryptionTest) { + // First encrypt + args_list_t encrypt_args = {"-e", "-aes-128-cbc", + "-K", "0123456789abcdef0123456789abcdef", + "-iv", "0123456789abcdef0123456789abcdef", + "-in", in_path, + "-out", out_path}; + bool result = encTool(encrypt_args); + ASSERT_TRUE(result); + + // Create temp file for decrypted output + char decrypt_path[PATH_MAX]; + ASSERT_GT(createTempFILEpath(decrypt_path), 0u); + + // Test explicit -d flag + args_list_t decrypt_args = {"-d", "-aes-128-cbc", + "-K", "0123456789abcdef0123456789abcdef", + "-iv", "0123456789abcdef0123456789abcdef", + "-in", out_path, + "-out", decrypt_path}; + result = encTool(decrypt_args); + ASSERT_TRUE(result); + + RemoveFile(decrypt_path); +} + +// Test decryption with default cipher +TEST_F(EncTest, EncToolDecryptionDefaultCipherTest) { + // First encrypt with default cipher + args_list_t encrypt_args = {"-e", + "-K", + "0123456789abcdef0123456789abcdef", + "-iv", + "0123456789abcdef0123456789abcdef", + "-in", + in_path, + "-out", + out_path}; + bool result = encTool(encrypt_args); + ASSERT_TRUE(result); + + // Create temp file for decrypted output + char decrypt_path[PATH_MAX]; + ASSERT_GT(createTempFILEpath(decrypt_path), 0u); + + // Decrypt with default cipher + args_list_t decrypt_args = {"-d", + "-K", + "0123456789abcdef0123456789abcdef", + "-iv", + "0123456789abcdef0123456789abcdef", + "-in", + out_path, + "-out", + decrypt_path}; + result = encTool(decrypt_args); + ASSERT_TRUE(result); + + RemoveFile(decrypt_path); +} + +// Test default cipher (should be aes-128-cbc) +TEST_F(EncTest, EncToolDefaultCipherTest) { + args_list_t args = {"-e", + "-K", + "0123456789abcdef0123456789abcdef", + "-iv", + "0123456789abcdef0123456789abcdef", + "-in", + in_path, + "-out", + out_path}; + bool result = encTool(args); + ASSERT_TRUE(result); +} + +// Test encryption without -e flag (should default to encrypt) +TEST_F(EncTest, EncToolDefaultEncryptTest) { + args_list_t args = {"-aes-128-cbc", + "-K", + "0123456789abcdef0123456789abcdef", + "-iv", + "0123456789abcdef0123456789abcdef", + "-in", + in_path, + "-out", + out_path}; + bool result = encTool(args); + ASSERT_TRUE(result); +} + +// -------------------- Enc Option Usage Error Tests -------------------------- + +class EncOptionUsageErrorsTest : public EncTest { + protected: + void TestOptionUsageErrors(const std::vector &args) { + args_list_t c_args; + for (const auto &arg : args) { + c_args.push_back(arg.c_str()); + } + bool result = encTool(c_args); + ASSERT_FALSE(result); + } +}; + +// Test missing required key +TEST_F(EncOptionUsageErrorsTest, MissingKeyTest) { + std::vector> testparams = { + {"-e", "-aes-128-cbc", "-iv", "0123456789abcdef0123456789abcdef", "-in", + in_path}, + {"-d", "-aes-128-cbc", "-iv", "0123456789abcdef0123456789abcdef", "-in", + in_path}, + {"-aes-128-cbc", "-iv", "0123456789abcdef0123456789abcdef", "-in", + in_path}}; + for (const auto &args : testparams) { + TestOptionUsageErrors(args); + } +} + +// Test mutually exclusive -e and -d options +TEST_F(EncOptionUsageErrorsTest, MutuallyExclusiveOptionsTest) { + std::vector> testparams = { + {"-e", "-d", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcdef", + "-iv", "0123456789abcdef0123456789abcdef", "-in", in_path}}; + for (const auto &args : testparams) { + TestOptionUsageErrors(args); + } +} + +// Test invalid hex key +TEST_F(EncOptionUsageErrorsTest, InvalidHexKeyTest) { + std::vector> testparams = { + {"-e", "-aes-128-cbc", "-K", "invalidhexkey", "-iv", + "0123456789abcdef0123456789abcdef", "-in", in_path}, + {"-e", "-aes-128-cbc", "-K", "0123456789abcdefg123456789abcdef", "-iv", + "0123456789abcdef0123456789abcdef", "-in", in_path}}; + for (const auto &args : testparams) { + TestOptionUsageErrors(args); + } +} + +// Test invalid hex IV +TEST_F(EncOptionUsageErrorsTest, InvalidHexIVTest) { + std::vector> testparams = { + {"-e", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcdef", "-iv", + "invalidhexiv", "-in", in_path}, + {"-e", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcdef", "-iv", + "0123456789abcdefg123456789abcdef", "-in", in_path}}; + for (const auto &args : testparams) { + TestOptionUsageErrors(args); + } +} + +// Test hex string size mismatch for key and IV +TEST_F(EncOptionUsageErrorsTest, HexStringSizeMismatchTest) { + std::vector> testparams = { + // Key too short (AES-128 needs 32 hex chars, providing 30) + {"-e", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcd", "-iv", + "0123456789abcdef0123456789abcdef", "-in", in_path}, + // Key too long (AES-128 needs 32 hex chars, providing 34) + {"-e", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcdef01", "-iv", + "0123456789abcdef0123456789abcdef", "-in", in_path}, + // IV too short (AES-128-CBC needs 32 hex chars, providing 30) + {"-e", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcdef", "-iv", + "0123456789abcdef0123456789abcd", "-in", in_path}, + // IV too long (AES-128-CBC needs 32 hex chars, providing 34) + {"-e", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcdef", "-iv", + "0123456789abcdef0123456789abcdef01", "-in", in_path}}; + for (const auto &args : testparams) { + TestOptionUsageErrors(args); + } +} + +// Test missing IV for cipher that requires it +TEST_F(EncOptionUsageErrorsTest, MissingIVTest) { + std::vector> testparams = { + {"-e", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcdef", "-in", + in_path}}; + for (const auto &args : testparams) { + TestOptionUsageErrors(args); + } +} + +// Test invalid input file +TEST_F(EncOptionUsageErrorsTest, InvalidInputFileTest) { + std::vector> testparams = { + {"-e", "-aes-128-cbc", "-K", "0123456789abcdef0123456789abcdef", "-iv", + "0123456789abcdef0123456789abcdef", "-in", "/nonexistent/file.txt"}}; + for (const auto &args : testparams) { + TestOptionUsageErrors(args); + } +} + +// -------------------- Enc OpenSSL Comparison Tests -------------------------- + +// Comparison tests cannot run without set up of environment variables: +// AWSLC_TOOL_PATH and OPENSSL_TOOL_PATH. + +class EncComparisonTest : public ::testing::Test { + protected: + void SetUp() override { + // Skip gtests if env variables not set + tool_executable_path = getenv("AWSLC_TOOL_PATH"); + openssl_executable_path = getenv("OPENSSL_TOOL_PATH"); + 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"; + } + + ASSERT_GT(createTempFILEpath(in_path), 0u); + ASSERT_GT(createTempFILEpath(out_path_tool), 0u); + ASSERT_GT(createTempFILEpath(out_path_openssl), 0u); + + // Create test input file + ScopedFILE in_file(fopen(in_path, "wb")); + ASSERT_TRUE(in_file); + const char *test_data = + "Hello, World! This is test data for encryption comparison."; + fwrite(test_data, 1, strlen(test_data), in_file.get()); + } + + void TearDown() override { + RemoveFile(in_path); + RemoveFile(out_path_tool); + RemoveFile(out_path_openssl); + } + + char in_path[PATH_MAX]; + char out_path_tool[PATH_MAX]; + char out_path_openssl[PATH_MAX]; + const char *tool_executable_path; + const char *openssl_executable_path; +}; + +// Test encryption comparison with OpenSSL +TEST_F(EncComparisonTest, EncryptionComparisonTest) { + std::string key = "0123456789abcdef0123456789abcdef"; + std::string iv = "0123456789abcdef0123456789abcdef"; + + std::string tool_command = std::string(tool_executable_path) + + " enc -e -aes-128-cbc -K " + key + " -iv " + iv + + " -in " + in_path + " -out " + out_path_tool; + std::string openssl_command = + std::string(openssl_executable_path) + " enc -e -aes-128-cbc -K " + key + + " -iv " + iv + " -in " + in_path + " -out " + out_path_openssl; + + std::string tool_output_str, openssl_output_str; + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, + out_path_openssl, tool_output_str, + openssl_output_str); + + // Compare encrypted outputs + ASSERT_EQ(tool_output_str, openssl_output_str); +} + +// Test decryption comparison with OpenSSL +TEST_F(EncComparisonTest, DecryptionComparisonTest) { + std::string key = "0123456789abcdef0123456789abcdef"; + std::string iv = "0123456789abcdef0123456789abcdef"; + + // First encrypt with OpenSSL to create encrypted data + char encrypted_path[PATH_MAX]; + ASSERT_GT(createTempFILEpath(encrypted_path), 0u); + + std::string openssl_encrypt_cmd = + std::string(openssl_executable_path) + " enc -e -aes-128-cbc -K " + key + + " -iv " + iv + " -in " + in_path + " -out " + encrypted_path; + ASSERT_EQ(ExecuteCommand(openssl_encrypt_cmd), 0); + + // Now test decryption comparison + std::string tool_command = + std::string(tool_executable_path) + " enc -d -aes-128-cbc -K " + key + + " -iv " + iv + " -in " + encrypted_path + " -out " + out_path_tool; + std::string openssl_command = + std::string(openssl_executable_path) + " enc -d -aes-128-cbc -K " + key + + " -iv " + iv + " -in " + encrypted_path + " -out " + out_path_openssl; + + std::string tool_output_str, openssl_output_str; + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, + out_path_openssl, tool_output_str, + openssl_output_str); + + // Compare decrypted outputs + ASSERT_EQ(tool_output_str, openssl_output_str); + + RemoveFile(encrypted_path); +} diff --git a/tool-openssl/internal.h b/tool-openssl/internal.h index f3fd1f0107..dde1a2d7b6 100644 --- a/tool-openssl/internal.h +++ b/tool-openssl/internal.h @@ -89,9 +89,10 @@ 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 dhparamTool(const args_list_t &args); +bool ecTool(const args_list_t &args); bool ecparamTool(const args_list_t &args); +bool encTool(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); @@ -105,7 +106,6 @@ bool VerifyTool(const args_list_t &args); bool VersionTool(const args_list_t &args); bool X509Tool(const args_list_t &args); - // Req Tool Utilities bssl::UniquePtr ParseSubjectName(std::string &subject_string); diff --git a/tool-openssl/tool.cc b/tool-openssl/tool.cc index 023cebc12c..1c1fe14e30 100644 --- a/tool-openssl/tool.cc +++ b/tool-openssl/tool.cc @@ -15,12 +15,13 @@ #include "./internal.h" -static const std::array kTools = {{ +static const std::array kTools = {{ {"crl", CRLTool}, {"dgst", dgstTool}, {"dhparam", dhparamTool}, {"ec", ecTool}, {"ecparam", ecparamTool}, + {"enc", encTool}, {"genrsa", genrsaTool}, {"md5", md5Tool}, {"pkcs8", pkcs8Tool},