diff --git a/.github/workflows/windows-alt.yml b/.github/workflows/windows-alt.yml index fa7b43f548..7b8446d4ac 100644 --- a/.github/workflows/windows-alt.yml +++ b/.github/workflows/windows-alt.yml @@ -226,7 +226,7 @@ jobs: echo "GOPATH=${SYSROOT}" >> $GITHUB_ENV echo "GOROOT=${SYSROOT}/lib/go" >> $GITHUB_ENV echo "CMAKE_GENERATOR=${{ matrix.generator }}" >> $GITHUB_ENV - cygpath -w ${SYSROOT}/bin >> $GITHUB_PATH + cygpath -m ${SYSROOT}/bin >> $GITHUB_PATH - name: Checkout uses: actions/checkout@v4 - name: Setup CMake @@ -242,10 +242,5 @@ jobs: CMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \ CMAKE_BUILD_TYPE=Release \ - name: Run tests - if: matrix.generator != 'MSYS Makefiles' - shell: 'bash' - run: cmake --build ./build --target run_tests - - name: Run tests - if: matrix.generator == 'MSYS Makefiles' shell: 'msys2 {0}' run: cmake --build ./build --target run_tests diff --git a/tool-openssl/CMakeLists.txt b/tool-openssl/CMakeLists.txt index 5834ad284b..f355f0b518 100644 --- a/tool-openssl/CMakeLists.txt +++ b/tool-openssl/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable( dgst.cc pkcs8.cc pkey.cc + pkeyutl.cc rehash.cc req.cc ordered_args.cc @@ -91,6 +92,8 @@ if(BUILD_TESTING) pkcs8_test.cc pkey.cc pkey_test.cc + pkeyutl.cc + pkeyutl_test.cc rehash.cc rehash_test.cc req.cc diff --git a/tool-openssl/internal.h b/tool-openssl/internal.h index 26af3230b6..e2ac567894 100644 --- a/tool-openssl/internal.h +++ b/tool-openssl/internal.h @@ -40,6 +40,7 @@ bool dgstTool(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); +bool pkeyutlTool(const args_list_t &args); bool RehashTool(const args_list_t &args); bool reqTool(const args_list_t &args); bool rsaTool(const args_list_t &args); diff --git a/tool-openssl/pkeyutl.cc b/tool-openssl/pkeyutl.cc new file mode 100644 index 0000000000..6584d29e4f --- /dev/null +++ b/tool-openssl/pkeyutl.cc @@ -0,0 +1,314 @@ +// 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" +#include + +#define KEY_NONE 0 +#define KEY_PRIVKEY 1 +#define KEY_PUBKEY 2 + +static const argument_t kArguments[] = { + { "-help", kBooleanArgument, "Display option summary" }, + { "-in", kOptionalArgument, "Input file - default stdin" }, + { "-out", kOptionalArgument, "Output file - default stdout" }, + { "-sign", kBooleanArgument, "Sign input data with private key" }, + { "-verify", kBooleanArgument, "Verify with public key" }, + { "-sigfile", kOptionalArgument, "Signature file, required for verify operations only" }, + { "-inkey", kOptionalArgument, "Input private key file" }, + { "-pubin", kBooleanArgument, "Input is a public key" }, + { "-passin", kOptionalArgument, "Input file pass phrase source" }, + { "", kOptionalArgument, "" } +}; + +static bool LoadPrivateKey(const std::string &keyfile, const std::string &passin_arg, + bssl::UniquePtr &pkey) { + ScopedFILE key_file; + if (keyfile.empty()) { + fprintf(stderr, "Error: no private key given (-inkey parameter)\n"); + return false; + } + + key_file.reset(fopen(keyfile.c_str(), "rb")); + if (!key_file) { + fprintf(stderr, "Error: unable to load private key from '%s'\n", keyfile.c_str()); + return false; + } + + // For simplicity, we'll handle basic password-protected keys + // In a full implementation, we'd parse passin_arg for different password sources + const char *password = nullptr; + if (!passin_arg.empty()) { + // Simple case: assume it's a direct password (not a file or other source) + password = passin_arg.c_str(); + } + + pkey.reset(PEM_read_PrivateKey(key_file.get(), nullptr, nullptr, + const_cast(password))); + if (!pkey) { + fprintf(stderr, "Error: error reading private key from '%s'\n", keyfile.c_str()); + ERR_print_errors_fp(stderr); + return false; + } + + return true; +} + +static bool LoadPublicKey(const std::string &keyfile, bssl::UniquePtr &pkey) { + ScopedFILE key_file; + if (keyfile.empty()) { + fprintf(stderr, "Error: no public key given (-inkey parameter)\n"); + return false; + } + + key_file.reset(fopen(keyfile.c_str(), "rb")); + if (!key_file) { + fprintf(stderr, "Error: unable to load public key from '%s'\n", keyfile.c_str()); + return false; + } + + pkey.reset(PEM_read_PUBKEY(key_file.get(), nullptr, nullptr, nullptr)); + if (!pkey) { + fprintf(stderr, "Error: error reading public key from '%s'\n", keyfile.c_str()); + ERR_print_errors_fp(stderr); + return false; + } + + return true; +} + +static bool ReadInputData(const std::string &in_path, std::vector &data) { + 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 open input file '%s'\n", in_path.c_str()); + return false; + } + } + + if (!ReadAll(&data, in_file.get())) { + fprintf(stderr, "Error: error reading input data\n"); + return false; + } + + return true; +} + +static bool DoSign(EVP_PKEY *pkey, const std::vector &input_data, + std::vector &signature) { + bssl::UniquePtr ctx(EVP_PKEY_CTX_new(pkey, nullptr)); + if (!ctx) { + fprintf(stderr, "Error: failed to create signing context\n"); + return false; + } + + if (EVP_PKEY_sign_init(ctx.get()) <= 0) { + fprintf(stderr, "Error: failed to initialize signing context\n"); + ERR_print_errors_fp(stderr); + return false; + } + + size_t sig_len = 0; + if (EVP_PKEY_sign(ctx.get(), nullptr, &sig_len, input_data.data(), input_data.size()) <= 0) { + fprintf(stderr, "Error: failed to determine signature length\n"); + ERR_print_errors_fp(stderr); + return false; + } + + signature.resize(sig_len); + if (EVP_PKEY_sign(ctx.get(), signature.data(), &sig_len, + input_data.data(), input_data.size()) <= 0) { + fprintf(stderr, "Error: failed to sign data\n"); + ERR_print_errors_fp(stderr); + return false; + } + + signature.resize(sig_len); + return true; +} + +static bool DoVerify(EVP_PKEY *pkey, const std::vector &input_data, + const std::vector &signature) { + bssl::UniquePtr ctx(EVP_PKEY_CTX_new(pkey, nullptr)); + if (!ctx) { + fprintf(stderr, "Error: failed to create verification context\n"); + return false; + } + + if (EVP_PKEY_verify_init(ctx.get()) <= 0) { + fprintf(stderr, "Error: failed to initialize verification context\n"); + ERR_print_errors_fp(stderr); + return false; + } + + int result = EVP_PKEY_verify(ctx.get(), signature.data(), signature.size(), + input_data.data(), input_data.size()); + if (result == 1) { + return true; + } else if (result == 0) { + return false; // Verification failed + } else { + fprintf(stderr, "Error: verification operation failed\n"); + ERR_print_errors_fp(stderr); + return false; + } +} + +static bool WriteOutput(const std::vector &data, const std::string &out_path) { + bssl::UniquePtr output_bio; + if (out_path.empty()) { + output_bio.reset(BIO_new_fp(stdout, BIO_CLOSE)); + } else { + output_bio.reset(BIO_new(BIO_s_file())); + if (BIO_write_filename(output_bio.get(), out_path.c_str()) <= 0) { + fprintf(stderr, "Error: failed to open output file '%s'\n", out_path.c_str()); + return false; + } + } + + if (!output_bio) { + fprintf(stderr, "Error: unable to create output BIO\n"); + return false; + } + + BIO_write(output_bio.get(), data.data(), data.size()); + return true; +} + +bool pkeyutlTool(const args_list_t &args) { + using namespace ordered_args; + ordered_args_map_t parsed_args; + args_list_t extra_args; + + if (!ParseOrderedKeyValueArguments(parsed_args, extra_args, args, kArguments) || + extra_args.size() > 0) { + PrintUsage(kArguments); + return false; + } + + std::string in_path, out_path, inkey_path, passin_arg, sigfile_path; + bool sign = false, verify = false, pubin = false; + + GetString(&in_path, "-in", "", parsed_args); + GetString(&out_path, "-out", "", parsed_args); + GetString(&inkey_path, "-inkey", "", parsed_args); + GetString(&passin_arg, "-passin", "", parsed_args); + GetString(&sigfile_path, "-sigfile", "", parsed_args); + GetBoolArgument(&sign, "-sign", parsed_args); + GetBoolArgument(&verify, "-verify", parsed_args); + GetBoolArgument(&pubin, "-pubin", parsed_args); + + // Display help + if (HasArgument(parsed_args, "-help")) { + PrintUsage(kArguments); + return true; + } + + // Validate arguments + if (!sign && !verify) { + fprintf(stderr, "Error: must specify either -sign or -verify\n"); + return false; + } + + if (sign && verify) { + fprintf(stderr, "Error: cannot specify both -sign and -verify\n"); + return false; + } + + if (verify && sigfile_path.empty()) { + fprintf(stderr, "Error: No signature file specified for verify (-sigfile parameter)\n"); + return false; + } + + if (!verify && !sigfile_path.empty()) { + fprintf(stderr, "Error: Signature file specified for non-verify operation\n"); + return false; + } + + if (inkey_path.empty()) { + fprintf(stderr, "Error: no key given (-inkey parameter)\n"); + return false; + } + + // Load the key + bssl::UniquePtr pkey; + if (pubin || verify) { + if (!LoadPublicKey(inkey_path, pkey)) { + return false; + } + } else { + if (!LoadPrivateKey(inkey_path, passin_arg, pkey)) { + return false; + } + } + + if (sign) { + std::vector signature; + std::vector input_data; + if (!ReadInputData(in_path, input_data)) { + return false; + } + + // Sanity check for non-raw input + if (input_data.size() > EVP_MAX_MD_SIZE) { + fprintf(stderr, "Error: input data looks too long to be a hash\n"); + return false; + } + + if (!DoSign(pkey.get(), input_data, signature)) { + return false; + } + + if (!WriteOutput(signature, out_path)) { + return false; + } + } else if (verify) { + // Read signature from sigfile + std::vector signature; + ScopedFILE sig_file; + sig_file.reset(fopen(sigfile_path.c_str(), "rb")); + if (!sig_file) { + fprintf(stderr, "Error: unable to open signature file '%s'\n", sigfile_path.c_str()); + return false; + } + + if (!ReadAll(&signature, sig_file.get())) { + fprintf(stderr, "Error: error reading signature data\n"); + return false; + } + + std::vector input_data; + if (!ReadInputData(in_path, input_data)) { + return false; + } + + bool success = DoVerify(pkey.get(), input_data, signature); + + bssl::UniquePtr output_bio; + if (out_path.empty()) { + output_bio.reset(BIO_new_fp(stdout, BIO_CLOSE)); + } else { + output_bio.reset(BIO_new(BIO_s_file())); + if (BIO_write_filename(output_bio.get(), out_path.c_str()) <= 0) { + fprintf(stderr, "Error: failed to open output file '%s'\n", out_path.c_str()); + return false; + } + } + + if (success) { + BIO_puts(output_bio.get(), "Signature Verified Successfully\n"); + } else { + BIO_puts(output_bio.get(), "Signature Verification Failure\n"); + } + } + + return true; +} diff --git a/tool-openssl/pkeyutl_test.cc b/tool-openssl/pkeyutl_test.cc new file mode 100644 index 0000000000..4542fbeeca --- /dev/null +++ b/tool-openssl/pkeyutl_test.cc @@ -0,0 +1,252 @@ +// 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" +#include "../crypto/test/test_util.h" +#include + +class PKeyUtlTest : public ::testing::Test { +protected: + void SetUp() override { + ASSERT_GT(createTempFILEpath(in_path), 0u); + ASSERT_GT(createTempFILEpath(out_path), 0u); + ASSERT_GT(createTempFILEpath(sig_path), 0u); + ASSERT_GT(createTempFILEpath(key_path), 0u); + ASSERT_GT(createTempFILEpath(pubkey_path), 0u); + ASSERT_GT(createTempFILEpath(protected_key_path), 0u); + + // Create and save a private key in PEM format + bssl::UniquePtr pkey(CreateTestKey(2048)); + ASSERT_TRUE(pkey); + + ScopedFILE key_file(fopen(key_path, "wb")); + ASSERT_TRUE(key_file); + ASSERT_TRUE(PEM_write_PrivateKey(key_file.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr)); + + // Create a public key file + ScopedFILE pubkey_file(fopen(pubkey_path, "wb")); + ASSERT_TRUE(pubkey_file); + ASSERT_TRUE(PEM_write_PUBKEY(pubkey_file.get(), pkey.get())); + + // Create a password-protected private key + ScopedFILE protected_key_file(fopen(protected_key_path, "wb")); + ASSERT_TRUE(protected_key_file); + ASSERT_TRUE(PEM_write_PrivateKey(protected_key_file.get(), pkey.get(), + EVP_aes_256_cbc(), + (unsigned char*)"testpassword", 12, + nullptr, nullptr)); + + // Create a test input file with some data + ScopedFILE in_file(fopen(in_path, "wb")); + ASSERT_TRUE(in_file); + const char* test_data = "Test data for signing and verification"; + ASSERT_EQ(fwrite(test_data, 1, strlen(test_data), in_file.get()), strlen(test_data)); + } + + void TearDown() override { + RemoveFile(in_path); + RemoveFile(out_path); + RemoveFile(sig_path); + RemoveFile(key_path); + RemoveFile(pubkey_path); + RemoveFile(protected_key_path); + } + + char in_path[PATH_MAX]; + char out_path[PATH_MAX]; + char sig_path[PATH_MAX]; + char key_path[PATH_MAX]; + char pubkey_path[PATH_MAX]; + char protected_key_path[PATH_MAX]; +}; + +// ----------------------------- PKeyUtl Option Tests ----------------------------- + +// Test basic signing operation +TEST_F(PKeyUtlTest, SignTest) { + args_list_t args = {"-sign", "-inkey", key_path, "-in", in_path, "-out", out_path}; + bool result = pkeyutlTool(args); + ASSERT_TRUE(result); + + // Verify the signature file was created and has content + struct stat st; + ASSERT_EQ(stat(out_path, &st), 0); + ASSERT_GT(st.st_size, 0); +} + +// Test basic verification operation +TEST_F(PKeyUtlTest, VerifyTest) { + // First sign the data + { + args_list_t args = {"-sign", "-inkey", key_path, "-in", in_path, "-out", sig_path}; + bool result = pkeyutlTool(args); + ASSERT_TRUE(result); + } + + // Then verify the signature + { + args_list_t args = {"-verify", "-pubin", "-inkey", pubkey_path, "-in", in_path, + "-sigfile", sig_path, "-out", out_path}; + bool result = pkeyutlTool(args); + ASSERT_TRUE(result); + + // Check that the output contains "Signature Verified Successfully" + std::string output = ReadFileToString(out_path); + ASSERT_NE(output.find("Signature Verified Successfully"), std::string::npos); + } +} + + + +// Test with password-protected key +TEST_F(PKeyUtlTest, PassinTest) { + args_list_t args = {"-sign", "-inkey", protected_key_path, "-passin", "testpassword", "-in", in_path, "-out", out_path}; + bool result = pkeyutlTool(args); + ASSERT_TRUE(result); + + // Verify the signature file was created and has content + struct stat st; + ASSERT_EQ(stat(out_path, &st), 0); + ASSERT_GT(st.st_size, 0); +} + +// -------------------- PKeyUtl Option Usage Error Tests -------------------------- + +class PKeyUtlOptionUsageErrorsTest : public PKeyUtlTest { +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 = pkeyutlTool(c_args); + ASSERT_FALSE(result); + } +}; + +// Test invalid option combinations +TEST_F(PKeyUtlOptionUsageErrorsTest, InvalidOptionCombinationsTest) { + std::vector> testparams = { + // Both sign and verify specified + {"-sign", "-verify", "-inkey", key_path, "-in", in_path}, + // Missing inkey + {"-sign", "-in", in_path}, + // Verify without sigfile + {"-verify", "-inkey", key_path, "-in", in_path}, + // Sigfile with sign operation + {"-sign", "-inkey", key_path, "-in", in_path, "-sigfile", sig_path}, + }; + + for (const auto& args : testparams) { + TestOptionUsageErrors(args); + } +} + +// -------------------- PKeyUtl OpenSSL Comparison Tests -------------------------- + +// Comparison tests cannot run without set up of environment variables: +// AWSLC_TOOL_PATH and OPENSSL_TOOL_PATH. + +class PKeyUtlComparisonTest : 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); + ASSERT_GT(createTempFILEpath(sig_path_tool), 0u); + ASSERT_GT(createTempFILEpath(sig_path_openssl), 0u); + ASSERT_GT(createTempFILEpath(key_path), 0u); + ASSERT_GT(createTempFILEpath(pubkey_path), 0u); + + // Create and save a private key + pkey.reset(CreateTestKey(2048)); + ASSERT_TRUE(pkey); + + ScopedFILE key_file(fopen(key_path, "wb")); + ASSERT_TRUE(key_file); + ASSERT_TRUE(PEM_write_PrivateKey(key_file.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr)); + + // Create a public key file + ScopedFILE pubkey_file(fopen(pubkey_path, "wb")); + ASSERT_TRUE(pubkey_file); + ASSERT_TRUE(PEM_write_PUBKEY(pubkey_file.get(), pkey.get())); + + // Create a test input file with some data + ScopedFILE in_file(fopen(in_path, "wb")); + ASSERT_TRUE(in_file); + const char* test_data = "Test data for signing and verification"; + ASSERT_EQ(fwrite(test_data, 1, strlen(test_data), in_file.get()), strlen(test_data)); + } + + void TearDown() override { + if (tool_executable_path != nullptr && openssl_executable_path != nullptr) { + RemoveFile(in_path); + RemoveFile(out_path_tool); + RemoveFile(out_path_openssl); + RemoveFile(sig_path_tool); + RemoveFile(sig_path_openssl); + RemoveFile(key_path); + RemoveFile(pubkey_path); + } + } + + char in_path[PATH_MAX]; + char out_path_tool[PATH_MAX]; + char out_path_openssl[PATH_MAX]; + char sig_path_tool[PATH_MAX]; + char sig_path_openssl[PATH_MAX]; + char key_path[PATH_MAX]; + char pubkey_path[PATH_MAX]; + bssl::UniquePtr pkey; + const char* tool_executable_path; + const char* openssl_executable_path; + std::string tool_output_str; + std::string openssl_output_str; +}; + +// Test signing operation against OpenSSL +TEST_F(PKeyUtlComparisonTest, SignCompareOpenSSL) { + std::string tool_command = std::string(tool_executable_path) + " pkeyutl -sign -inkey " + + key_path + " -in " + in_path + " -out " + sig_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " pkeyutl -sign -inkey " + + key_path + " -in " + in_path + " -out " + sig_path_openssl; + + int tool_result = system(tool_command.c_str()); + ASSERT_EQ(tool_result, 0) << "AWS-LC tool command failed: " << tool_command; + + int openssl_result = system(openssl_command.c_str()); + ASSERT_EQ(openssl_result, 0) << "OpenSSL command failed: " << openssl_command; + + // Verify both signatures with the public key + std::string tool_verify_cmd = std::string(tool_executable_path) + " pkeyutl -verify -pubin -inkey " + + pubkey_path + " -in " + in_path + " -sigfile " + sig_path_tool + + " > " + out_path_tool; + std::string openssl_verify_cmd = std::string(openssl_executable_path) + " pkeyutl -verify -pubin -inkey " + + pubkey_path + " -in " + in_path + " -sigfile " + sig_path_openssl + + " > " + out_path_openssl; + + ASSERT_EQ(system(tool_verify_cmd.c_str()), 0); + ASSERT_EQ(system(openssl_verify_cmd.c_str()), 0); + + // Read verification results + std::ifstream tool_output(out_path_tool); + tool_output_str = std::string((std::istreambuf_iterator(tool_output)), std::istreambuf_iterator()); + std::ifstream openssl_output(out_path_openssl); + openssl_output_str = std::string((std::istreambuf_iterator(openssl_output)), std::istreambuf_iterator()); + + // Both should verify successfully + ASSERT_NE(tool_output_str.find("Signature Verified Successfully"), std::string::npos); + ASSERT_NE(openssl_output_str.find("Signature Verified Successfully"), std::string::npos); +} diff --git a/tool-openssl/tool.cc b/tool-openssl/tool.cc index 4ef0fa14cd..c7ad54cd1c 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}, {"md5", md5Tool}, {"pkcs8", pkcs8Tool}, {"pkey", pkeyTool}, + {"pkeyutl", pkeyutlTool}, {"rehash", RehashTool}, {"req", reqTool}, {"rsa", rsaTool},