From 57fa9d93e5099fe5fe285fcb36c54e118bf5712f Mon Sep 17 00:00:00 2001 From: Hailey Ho Date: Tue, 12 Aug 2025 22:51:02 +0000 Subject: [PATCH 1/4] Implement dgst cli --- tool-openssl/dgst.cc | 578 +++++++++++++++++++++++++++-------- tool-openssl/dgst_test.cc | 401 +++++++++++++++++------- tool-openssl/internal.h | 14 + tool-openssl/ordered_args.cc | 38 ++- tool-openssl/test_util.h | 49 +-- tool/args.cc | 39 ++- tool/internal.h | 46 +-- 7 files changed, 878 insertions(+), 287 deletions(-) diff --git a/tool-openssl/dgst.cc b/tool-openssl/dgst.cc index 55284c6050..88b6ce0ff3 100644 --- a/tool-openssl/dgst.cc +++ b/tool-openssl/dgst.cc @@ -7,214 +7,534 @@ #include #include +#include + #include "internal.h" // MD5 command currently only supports stdin static const argument_t kArguments[] = { {"-help", kBooleanArgument, "Display option summary"}, + {"-binary", kBooleanArgument, "Output in binary form"}, + {"-hex", kBooleanArgument, "Output as hex dump"}, + {"-md5", kExclusiveBooleanArgument, "Supported digest function"}, + {"-ripemd160", kExclusiveBooleanArgument, "Supported digest function"}, + {"-sha1", kExclusiveBooleanArgument, "Supported digest function"}, + {"-sha256", kExclusiveBooleanArgument, + "Supported digest function (default)"}, + {"-sha224", kExclusiveBooleanArgument, "Supported digest function"}, + {"-sha384", kExclusiveBooleanArgument, "Supported digest function"}, + {"-sha512", kExclusiveBooleanArgument, "Supported digest function"}, {"-hmac", kOptionalArgument, "Create a hashed MAC with the corresponding key"}, + {"-sigopt", kOptionalArgument, "Signature parameter in n:v form"}, + {"-passin", kOptionalArgument, "Input file pass phrase source"}, + {"-sign", kOptionalArgument, "Sign digest using private key"}, + {"-out", kOptionalArgument, "Output to filename rather than stdout"}, + {"-verify", kOptionalArgument, "Verify a signature using public key"}, + {"-signature", kOptionalArgument, "File with signature to verify"}, + {"-keyform", kOptionalArgument, "key file format (only PEM is supported)"}, {"", kOptionalArgument, ""}}; -static bool dgst_file_op(const std::string &filename, const int fd, - const EVP_MD *digest) { - static const size_t kBufSize = 8192; - std::unique_ptr buf(new uint8_t[kBufSize]); +static bool LoadPrivateKey(const std::string &key_file_path, + bssl::UniquePtr &pkey) { + ScopedFILE key_file(fopen(key_file_path.c_str(), "rb")); + + if (!key_file) { + fprintf(stderr, "Failed to open %s", key_file_path.c_str()); + return false; + } + + // TODO: use -passin to read the key, if applicable + const char *password = nullptr; + + pkey.reset(PEM_read_PrivateKey(key_file.get(), nullptr, nullptr, + const_cast(password))); + + if (!pkey) { + fprintf(stderr, "Failed to read private key from %s", + key_file_path.c_str()); + return false; + } + + return true; +} + +static bool LoadPublicKey(const std::string &key_file_path, + bssl::UniquePtr &pkey) { + ScopedFILE key_file(fopen(key_file_path.c_str(), "rb")); + + if (!key_file) { + fprintf(stderr, "Failed to open %s", key_file_path.c_str()); + return false; + } + + pkey.reset(PEM_read_PUBKEY(key_file.get(), nullptr, nullptr, nullptr)); + + if (!pkey) { + fprintf(stderr, "Failed to read public key from %s", key_file_path.c_str()); + return false; + } + + return true; +} + +static bool ApplySignatureParam(EVP_PKEY_CTX *ctx, const char *sigopt) { + char *stmp = OPENSSL_strdup(sigopt); + if (!stmp) { + return false; + } + + char *vtmp = strchr(stmp, ':'); + if (!vtmp) { + OPENSSL_free(stmp); + return false; + } + + *vtmp = 0; + vtmp++; + + OPENSSL_BEGIN_ALLOW_DEPRECATED + int result = EVP_PKEY_CTX_ctrl_str(ctx, stmp, vtmp); + OPENSSL_END_ALLOW_DEPRECATED + + OPENSSL_free(stmp); + return result == 1; +} + +static std::string GetSigName(int nid) { + switch (nid) { + case EVP_PKEY_RSA: + return "RSA"; + + case EVP_PKEY_RSA_PSS: + return "RSA-PSS"; + + case EVP_PKEY_EC: + return "ECDSA"; + default: + /* Try to output provider-registered sig alg name */ + return OBJ_nid2sn(nid); + } +} + +static bool GenerateHash(const std::string &in_path, const EVP_MD *digest, + std::vector &hash) { bssl::ScopedEVP_MD_CTX ctx; + if (!EVP_DigestInit_ex(ctx.get(), digest, nullptr)) { - fprintf(stderr, "Failed to initialize EVP_MD_CTX.\n"); + fprintf(stderr, "Failed to initialize digest context.\n"); return false; } - for (;;) { - size_t n; - if (!ReadFromFD(fd, &n, buf.get(), kBufSize)) { - fprintf(stderr, "Failed to read from %s: %s\n", filename.c_str(), - strerror(errno)); + 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; } + } + + uint8_t buf[4096]; + for (;;) { + size_t n = fread(buf, 1, sizeof(buf), in_file.get()); - if (n == 0) { + if (feof(in_file.get())) { break; } + if (ferror(in_file.get())) { + fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); + return false; + } - if (!EVP_DigestUpdate(ctx.get(), buf.get(), n)) { - fprintf(stderr, "Failed to update hash.\n"); + if (!EVP_DigestUpdate(ctx.get(), buf, n)) { + fprintf(stderr, "Failed to update signature.\n"); return false; } } - uint8_t hash[EVP_MAX_MD_SIZE]; - unsigned hash_len; - if (!EVP_DigestFinal_ex(ctx.get(), hash, &hash_len)) { - fprintf(stderr, "Failed to finish hash.\n"); + unsigned hash_len = hash.size(); + + if (!EVP_DigestFinal_ex(ctx.get(), hash.data(), &hash_len)) { + fprintf(stderr, "Failed to finish signature.\n"); return false; } - // Print digest output. OpenSSL outputs the digest name with files, but not - // with stdin. - if (fd != 0) { - fprintf(stdout, "%s(%s)= ", EVP_MD_get0_name(digest), filename.c_str()); - } else { - fprintf(stdout, "(%s)= ", filename.c_str()); - }; - for (size_t i = 0; i < hash_len; i++) { - fprintf(stdout, "%02x", hash[i]); - } - fprintf(stdout, "\n"); + hash.resize(hash_len); + return true; } -static bool hmac_file_op(const std::string &filename, const int fd, - const EVP_MD *digest, const char *hmac_key, - const size_t hmac_key_len) { - static const size_t kBufSize = 8192; - std::unique_ptr buf(new uint8_t[kBufSize]); - +static bool GenerateHMAC(const std::string &in_path, const char *hmac_key, + const size_t hmac_key_len, const EVP_MD *digest, + std::vector &mac) { bssl::ScopedHMAC_CTX ctx; if (!HMAC_Init_ex(ctx.get(), hmac_key, hmac_key_len, digest, nullptr)) { fprintf(stderr, "Failed to initialize HMAC_Init_ex.\n"); return false; } - // Update |buf| from file continuously. - for (;;) { - size_t n; - if (!ReadFromFD(fd, &n, buf.get(), kBufSize)) { - fprintf(stderr, "Failed to read from %s: %s\n", filename.c_str(), - strerror(errno)); + 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; } + } + + uint8_t buf[4096]; + for (;;) { + size_t n = fread(buf, 1, sizeof(buf), in_file.get()); - if (n == 0) { + if (feof(in_file.get())) { break; } + if (ferror(in_file.get())) { + fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); + return false; + } - if (!HMAC_Update(ctx.get(), buf.get(), n)) { + if (!HMAC_Update(ctx.get(), buf, n)) { fprintf(stderr, "Failed to update HMAC.\n"); return false; } } - const unsigned expected_mac_len = EVP_MD_size(digest); - std::unique_ptr mac(new uint8_t[expected_mac_len]); - unsigned mac_len; - if (!HMAC_Final(ctx.get(), mac.get(), &mac_len)) { + unsigned mac_len = mac.size(); + if (!HMAC_Final(ctx.get(), mac.data(), &mac_len)) { fprintf(stderr, "Failed to finalize HMAC.\n"); return false; } - // Print HMAC output. OpenSSL outputs the digest name with files, but not - // with stdin. - if (fd != 0) { - fprintf(stdout, "HMAC-%s(%s)= ", EVP_MD_get0_name(digest), - filename.c_str()); - } else { - fprintf(stdout, "(%s)= ", filename.c_str()); - }; - for (size_t i = 0; i < expected_mac_len; i++) { - fprintf(stdout, "%02x", mac[i]); - } - fprintf(stdout, "\n"); + mac.resize(mac_len); + return true; } -static bool dgst_tool_op(const args_list_t &args, const EVP_MD *digest) { - std::vector file_inputs; +static bool GenerateSignature(EVP_PKEY *pkey, const std::string &in_path, + const EVP_MD *digest, + const std::vector &sigopts, + std::vector &signature) { + bssl::ScopedEVP_MD_CTX ctx; + EVP_PKEY_CTX *pctx = nullptr; - // Default is SHA-256. - // TODO: Make this customizable when "-digest" is introduced. - if (digest == nullptr) { - digest = EVP_sha256(); - } - - // HMAC keys can be empty, but C++ std::string has no way to differentiate - // between null and empty. - const char *hmac_key = nullptr; - size_t hmac_key_len = 0; - - auto it = args.begin(); - while (it != args.end()) { - const std::string &arg = *it; - if (!arg.empty() && arg[0] != '-') { - // Any input without a '-' prefix is parsed as a file. This - // also marks the end of any option input. - while (it != args.end()) { - if (!(*it).empty()) { - file_inputs.push_back(*it); - } - it++; + if (!EVP_DigestSignInit(ctx.get(), &pctx, digest, nullptr, pkey)) { + fprintf(stderr, "Failed to initialize digest context.\n"); + return false; + } + + if (sigopts.size() > 0) { + for (const auto &sigopt : sigopts) { + if (!ApplySignatureParam(pctx, sigopt.c_str())) { + fprintf(stderr, "Signature parameter error \"%s\"\n", sigopt.c_str()); + return false; } + } + } + + 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; + } + } + + uint8_t buf[4096]; + for (;;) { + size_t n = fread(buf, 1, sizeof(buf), in_file.get()); + + if (feof(in_file.get())) { break; } + if (ferror(in_file.get())) { + fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); + return false; + } + + if (!EVP_DigestSignUpdate(ctx.get(), buf, n)) { + fprintf(stderr, "Failed to update signature.\n"); + return false; + } + } + + size_t signature_len = signature.size(); + + if (!EVP_DigestSignFinal(ctx.get(), signature.data(), &signature_len)) { + fprintf(stderr, "Failed to finish signature.\n"); + return false; + } + + signature.resize(signature_len); + + return true; +} + +static bool VerifySignature(EVP_PKEY *pkey, const std::string &in_path, + const EVP_MD *digest, + const std::vector &sigopts, + const std::string &signature_file_path, + bssl::UniquePtr &out_bio) { + bssl::ScopedEVP_MD_CTX ctx; + EVP_PKEY_CTX *pctx; + + if (!EVP_DigestVerifyInit(ctx.get(), &pctx, digest, nullptr, pkey)) { + fprintf(stderr, "Failed to initialize digest context.\n"); + return false; + } - if (!arg.empty() && arg[0] == '-') { - const std::string option = arg.substr(1); - if (option == "help") { - PrintUsage(kArguments); - return true; - } else if (option == "hmac") { - // Read next argument as key string. - it++; - // HMAC allows for empty keys. - if (it != args.end()) { - hmac_key = (*it).c_str(); - hmac_key_len = (*it).length(); - } else { - fprintf(stderr, - "dgst: Option -hmac needs a value\n" - "dgst: Use -help for summary.\n"); - return false; - } - } else { - fprintf(stderr, "Unknown option '%s'.\n", option.c_str()); + if (sigopts.size() > 0) { + for (const auto &sigopt : sigopts) { + if (!ApplySignatureParam(pctx, sigopt.c_str())) { + fprintf(stderr, "Signature parameter error \"%s\"\n", sigopt.c_str()); return false; } + } + } + + 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 opening input file '%s'\n", in_path.c_str()); + return false; + } + } + + uint8_t buf[4096]; + for (;;) { + size_t n = fread(buf, 1, sizeof(buf), in_file.get()); + + if (feof(in_file.get())) { + break; + } + if (ferror(in_file.get())) { + fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); + return false; + } + + if (!EVP_DigestVerifyUpdate(ctx.get(), buf, n)) { + fprintf(stderr, "Failed to update signature.\n"); + return false; + } + } + std::vector signature(EVP_PKEY_size(pkey)); + ScopedFILE signature_file(fopen(signature_file_path.c_str(), "rb")); + if (!signature_file) { + fprintf(stderr, "Error opening signature file %s.\n", + signature_file_path.c_str()); + return false; + } + + if (!ReadAll(&signature, signature_file.get())) { + fprintf(stderr, "Error reading from signature file %s.\n", + signature_file_path.c_str()); + return false; + } + + int result = + EVP_DigestVerifyFinal(ctx.get(), signature.data(), signature.size()); + + if (result > 0) { + BIO_printf(out_bio.get(), "Verified OK\n"); + } else if (result == 0) { + BIO_printf(out_bio.get(), "Verification failure\n"); + } else { + BIO_printf(out_bio.get(), "Error verifying data\n"); + } + + return true; +} + +static bool WriteOutput(bssl::UniquePtr &out_bio, + const std::vector &data, + const std::string &sig_name, + const std::string &digest_name, + const std::string &in_path, bool out_bin) { + if (out_bin) { + BIO_write(out_bio.get(), data.data(), data.size()); + } else { + if (!sig_name.empty()) { + BIO_printf(out_bio.get(), "%s-%s", sig_name.c_str(), digest_name.c_str()); + } else { + BIO_printf(out_bio.get(), "%s", digest_name.c_str()); + } + + if (!in_path.empty()) { + BIO_printf(out_bio.get(), "(%s)=", in_path.c_str()); } else { - // Empty input. OpenSSL continues processing the next file even when - // provided an invalid file. - fprintf(stderr, "Failed to read from empty input."); + BIO_printf(out_bio.get(), "(stdin)="); } - // Increment while loop. - it++; + for (size_t i = 0; i < data.size(); i++) { + BIO_printf(out_bio.get(), "%02x", data[i]); + } + BIO_printf(out_bio.get(), "\n"); + } + + return true; +} + +static bool dgstToolInternal(const args_list_t &args, const EVP_MD *digest) { + std::vector in_files; + + using namespace ordered_args; + ordered_args_map_t parsed_args; + + if (!ParseOrderedKeyValueArguments(parsed_args, in_files, args, kArguments)) { + PrintUsage(kArguments); + return false; + } + + std::string hmac, digest_name, passin, sign_key_file, out_path, + verify_key_file, signature_file, keyform; + std::vector sigopts; + bool out_bin = false, binary = false, hex = false; + + if (HasArgument(parsed_args, "-help")) { + PrintUsage(kArguments); + return false; + } + + GetBoolArgument(&binary, "-binary", parsed_args); + GetBoolArgument(&hex, "-hex", parsed_args); + GetString(&hmac, "-hmac", "", parsed_args); + GetString(&passin, "-passin", "", parsed_args); + GetString(&sign_key_file, "-sign", "", parsed_args); + GetString(&out_path, "-out", "", parsed_args); + GetString(&verify_key_file, "-verify", "", parsed_args); + GetString(&signature_file, "-signature", "", parsed_args); + GetString(&keyform, "-keyform", "PEM", parsed_args); + FindAll(sigopts, "-sigopt", parsed_args); + + // Validate arguments + if (HasArgument(parsed_args, "-hex") && HasArgument(parsed_args, "-binary")) { + fprintf(stderr, "Error: -hex and -binary cannot both be specified"); + return false; + } + + if ((!verify_key_file.empty() + !sign_key_file.empty() + !hmac.empty()) > 1) { + fprintf(stderr, "Error: MAC and signing key cannot both be specified\n"); + return false; + } + + if (!verify_key_file.empty() && signature_file.empty()) { + fprintf(stderr, + "Error: No signature to verify: use the -signature option\n"); + return false; + } + + if (keyform != "PEM") { + fprintf(stdout, "keyform=%s\n", keyform.c_str()); + fprintf(stderr, "Error: -keyform only accepts type PEM\n"); + return false; + } + + // Default digest is SHA-256. + if (digest == nullptr) { + GetExclusiveBoolArgument(&digest_name, kArguments, "-sha256", parsed_args); + digest = EVP_get_digestbyname(digest_name.substr(1).c_str()); + } + + if (hex) { + out_bin = false; + } else if (binary || !sign_key_file.empty() || !verify_key_file.empty()) { + out_bin = true; + } + + bssl::UniquePtr out_bio; + if (out_path.empty()) { + out_bio.reset( + BIO_new_fp(stdout, BIO_NOCLOSE | (out_bin ? 0 : BIO_FP_TEXT))); + } else { + out_bio.reset(BIO_new_file(out_path.c_str(), (out_bin ? "wb" : "w"))); + + if (!out_bio) { + fprintf(stderr, "Error: Failed to open output file: %s\n", + out_path.c_str()); + return false; + } } - // Use stdin if no files are provided. - if (file_inputs.empty()) { - // 0 denotes stdin. - std::string file_name = "stdin"; - int fd = 0; - if (hmac_key) { - if (!hmac_file_op(file_name, fd, digest, hmac_key, hmac_key_len)) { + bssl::UniquePtr pkey; + size_t i = 0; + do { + std::string in_path; + if (in_files.empty()) { + in_path = ""; + } else { + in_path = in_files[i++]; + } + + if (!hmac.empty()) { + const char *hmac_key = hmac.c_str(); + size_t hmac_key_len = hmac.length(); + + std::vector mac(EVP_MD_size(digest)); + + if (!GenerateHMAC(in_path, hmac_key, hmac_key_len, digest, mac)) { return false; } - } else { - if (!dgst_file_op(file_name, fd, digest)) { + + WriteOutput(out_bio, mac, "HMAC", EVP_MD_get0_name(digest), in_path, + out_bin); + } else if (!verify_key_file.empty()) { + if (!LoadPublicKey(verify_key_file, pkey)) { + return false; + } + + if (!VerifySignature(pkey.get(), in_path, digest, sigopts, signature_file, + out_bio)) { + return false; + } + } else if (!sign_key_file.empty()) { + if (!LoadPrivateKey(sign_key_file, pkey)) { return false; } - } - return true; - } - // Do the dgst operation on all file inputs. - for (const auto &file_name : file_inputs) { - ScopedFD scoped_fd = OpenFD(file_name.c_str(), O_RDONLY | O_BINARY); - int fd = scoped_fd.get(); - if (hmac_key) { - if (!hmac_file_op(file_name, fd, digest, hmac_key, hmac_key_len)) { + std::vector signature(EVP_PKEY_size(pkey.get())); + if (!GenerateSignature(pkey.get(), in_path, digest, sigopts, signature)) { return false; } + + WriteOutput(out_bio, signature, GetSigName(EVP_PKEY_id(pkey.get())), + EVP_MD_get0_name(digest), in_path, out_bin); } else { - if (!dgst_file_op(file_name, fd, digest)) { + std::vector hash(EVP_MAX_MD_SIZE); + + if (!GenerateHash(in_path, digest, hash)) { return false; } + + WriteOutput(out_bio, hash, "", EVP_MD_get0_name(digest), in_path, + out_bin); } - } + + if (in_files.empty()) { + break; + } + } while (i < in_files.size()); return true; } -bool dgstTool(const args_list_t &args) { return dgst_tool_op(args, nullptr); } -bool md5Tool(const args_list_t &args) { return dgst_tool_op(args, EVP_md5()); } +bool dgstTool(const args_list_t &args) { + return dgstToolInternal(args, nullptr); +} +bool md5Tool(const args_list_t &args) { + return dgstToolInternal(args, EVP_md5()); +} diff --git a/tool-openssl/dgst_test.cc b/tool-openssl/dgst_test.cc index 0ef6743fa5..6996e0b0c8 100644 --- a/tool-openssl/dgst_test.cc +++ b/tool-openssl/dgst_test.cc @@ -4,39 +4,73 @@ #include #include #include "../crypto/test/test_util.h" +#include "internal.h" #include "test_util.h" -// -------------------- MD5 OpenSSL Comparison Test --------------------------- - -// Comparison tests cannot run without set up of environment variables: -// AWSLC_TOOL_PATH and OPENSSL_TOOL_PATH. - class DgstComparisonTest : public ::testing::Test { protected: void SetUp() override { // Skip gtests if env variables not set awslc_executable_path = getenv("AWSLC_TOOL_PATH"); openssl_executable_path = getenv("OPENSSL_TOOL_PATH"); + + ASSERT_GT(createTempFILEpath(in_path), 0u); + ASSERT_GT(createTempFILEpath(out_path_awslc), 0u); + ASSERT_GT(createTempFILEpath(out_path_openssl), 0u); + ASSERT_GT(createTempFILEpath(sig_path_awslc), 0u); + ASSERT_GT(createTempFILEpath(sig_path_openssl), 0u); + ASSERT_GT(createTempFILEpath(key_path), 0u); + ASSERT_GT(createTempFILEpath(pubkey_path), 0u); + ASSERT_GT(createTempFILEpath(protected_key_path), + 0u); // TODO: update this to test passin + + // 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 test input file with some data + ScopedFILE in_file(fopen(in_path, "wb")); + ASSERT_TRUE(in_file); + const char *test_data = "AWS_LC_TEST_STRING_INPUT"; + ASSERT_EQ(fwrite(test_data, 1, strlen(test_data), in_file.get()), + strlen(test_data)); + if (awslc_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_awslc), 0u); - ASSERT_GT(createTempFILEpath(out_path_openssl), 0u); } + void TearDown() override { - if (awslc_executable_path != nullptr && - openssl_executable_path != nullptr) { - // RemoveFile(in_path); - RemoveFile(out_path_awslc); - RemoveFile(out_path_openssl); - } + RemoveFile(in_path); + RemoveFile(out_path_awslc); + RemoveFile(out_path_openssl); + RemoveFile(sig_path_awslc); + RemoveFile(sig_path_openssl); + RemoveFile(key_path); + RemoveFile(pubkey_path); + RemoveFile(protected_key_path); } + char in_path[PATH_MAX]; char out_path_awslc[PATH_MAX]; char out_path_openssl[PATH_MAX]; + char sig_path_awslc[PATH_MAX]; + char sig_path_openssl[PATH_MAX]; + char key_path[PATH_MAX]; + char pubkey_path[PATH_MAX]; + char protected_key_path[PATH_MAX]; const char *awslc_executable_path; const char *openssl_executable_path; std::string awslc_output_str; @@ -49,23 +83,30 @@ std::string GetHash(const std::string &str) { if (pos == std::string::npos) { return ""; } - return str.substr(pos + 1); -} + std::string result = str.substr(pos + 1); -// Test against OpenSSL output for "-hmac" -TEST_F(DgstComparisonTest, HMAC_default_files) { - std::string input_file = std::string(in_path); - std::ofstream ofs(input_file); - ofs << "AWS_LC_TEST_STRING_INPUT"; - ofs.close(); + // OpenSSL has inconsistent leading white space + size_t start = result.find_first_not_of(" "); + if (start == std::string::npos) { + return ""; + } - // Run -hmac against a single file. + if (start == 0) { + return result; + } + + return result.substr(start); +} + +// -------------------- Dgst Options Test --------------------------- +TEST_F(DgstComparisonTest, HMAC) { std::string awslc_command = std::string(awslc_executable_path) + - " dgst -hmac test_key_string " + input_file + - " > " + out_path_awslc; + " dgst -hmac test_key_string -out " + + out_path_awslc + " " + in_path; + std::string openssl_command = std::string(openssl_executable_path) + - " dgst -hmac test_key_string " + input_file + - " > " + out_path_openssl; + " dgst -hmac test_key_string -out " + + out_path_openssl + " " + in_path; RunCommandsAndCompareOutput(awslc_command, openssl_command, out_path_awslc, out_path_openssl, awslc_output_str, @@ -76,125 +117,275 @@ TEST_F(DgstComparisonTest, HMAC_default_files) { EXPECT_EQ(awslc_hash, openssl_hash); - // Run -hmac again against multiple files. - char in_path2[PATH_MAX]; - ASSERT_GT(createTempFILEpath(in_path2), 0u); - std::string input_file2 = std::string(in_path2); - ofs.open(input_file2); - ofs << "AWS_LC_TEST_STRING_INPUT_2"; - ofs.close(); - + // binary output awslc_command = std::string(awslc_executable_path) + - " dgst -hmac alternative_key_string " + input_file + " " + - input_file2 + " > " + out_path_awslc; + " dgst -hmac test_key_string -binary -out " + out_path_awslc + + " " + in_path; openssl_command = std::string(openssl_executable_path) + - " dgst -hmac alternative_key_string " + input_file + " " + - input_file2 + +" > " + out_path_openssl; + " dgst -hmac test_key_string -binary -out " + + out_path_openssl + " " + in_path; RunCommandsAndCompareOutput(awslc_command, openssl_command, out_path_awslc, out_path_openssl, awslc_output_str, openssl_output_str); - awslc_hash = GetHash(awslc_output_str); - openssl_hash = GetHash(openssl_output_str); + EXPECT_EQ(awslc_output_str, openssl_output_str); +} - EXPECT_EQ(awslc_hash, openssl_hash); +TEST_F(DgstComparisonTest, Digest) { + // default digest + std::string awslc_command = std::string(awslc_executable_path) + + " dgst -out " + out_path_awslc + " " + in_path; - // Run -hmac with empty key - awslc_command = std::string(awslc_executable_path) + - " dgst -hmac \"\" " - " " + - input_file + " " + input_file2 + " > " + out_path_awslc; - openssl_command = std::string(openssl_executable_path) + " dgst -hmac \"\" " + - input_file + " " + input_file2 + +" > " + out_path_openssl; + std::string openssl_command = std::string(openssl_executable_path) + + " dgst -out " + out_path_openssl + " " + + in_path; RunCommandsAndCompareOutput(awslc_command, openssl_command, out_path_awslc, out_path_openssl, awslc_output_str, openssl_output_str); - awslc_hash = GetHash(awslc_output_str); - openssl_hash = GetHash(openssl_output_str); + std::string awslc_hash = GetHash(awslc_output_str); + std::string openssl_hash = GetHash(openssl_output_str); EXPECT_EQ(awslc_hash, openssl_hash); - RemoveFile(input_file.c_str()); - RemoveFile(input_file2.c_str()); -} - - -TEST_F(DgstComparisonTest, HMAC_default_stdin) { - std::string tool_command = "echo hmac_this_string | " + - std::string(awslc_executable_path) + - " dgst -hmac key > " + out_path_awslc; - std::string openssl_command = "echo hmac_this_string | " + - std::string(openssl_executable_path) + - " dgst -hmac key > " + out_path_openssl; + // non-default digest + binary + awslc_command = std::string(awslc_executable_path) + + " dgst -sha1 -binary -out " + out_path_awslc + " " + in_path; + openssl_command = std::string(openssl_executable_path) + + " dgst -sha1 -binary -out " + out_path_openssl + " " + + in_path; - RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_awslc, + RunCommandsAndCompareOutput(awslc_command, openssl_command, out_path_awslc, out_path_openssl, awslc_output_str, openssl_output_str); - std::string tool_hash = GetHash(awslc_output_str); - std::string openssl_hash = GetHash(openssl_output_str); - - EXPECT_EQ(tool_hash, openssl_hash); + EXPECT_EQ(awslc_output_str, openssl_output_str); } -TEST_F(DgstComparisonTest, MD5_files) { - std::string input_file = std::string(in_path); - std::ofstream ofs(input_file); - ofs << "AWS_LC_TEST_STRING_INPUT"; - ofs.close(); +TEST_F(DgstComparisonTest, SignAndVerify) { + // default binary output + std::string awslc_command = std::string(awslc_executable_path) + + " dgst -sign " + key_path + " -out " + + sig_path_awslc + " " + in_path; - // Input file as pipe (stdin) - std::string tool_command = std::string(awslc_executable_path) + " md5 < " + - input_file + " > " + out_path_awslc; std::string openssl_command = std::string(openssl_executable_path) + - " md5 < " + input_file + " > " + - out_path_openssl; + " dgst -sign " + key_path + " -out " + + sig_path_openssl + " " + in_path; - RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_awslc, - out_path_openssl, awslc_output_str, + RunCommandsAndCompareOutput(awslc_command, openssl_command, sig_path_awslc, + sig_path_openssl, awslc_output_str, openssl_output_str); - std::string tool_hash = GetHash(awslc_output_str); - std::string openssl_hash = GetHash(openssl_output_str); + EXPECT_EQ(awslc_output_str, openssl_output_str); - EXPECT_EQ(tool_hash, openssl_hash); + awslc_command = std::string(awslc_executable_path) + " dgst -verify " + + pubkey_path + " -signature " + sig_path_awslc + + " -keyform PEM -out " + out_path_awslc + " " + in_path; - // Input file as regular command line option. - tool_command = std::string(awslc_executable_path) + " md5 " + input_file + - " > " + out_path_awslc; - openssl_command = std::string(openssl_executable_path) + " md5 " + - input_file + " > " + out_path_openssl; + openssl_command = std::string(openssl_executable_path) + " dgst -verify " + + pubkey_path + " -signature " + sig_path_openssl + + " -keyform PEM -out " + out_path_openssl + " " + in_path; - RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_awslc, + RunCommandsAndCompareOutput(awslc_command, openssl_command, out_path_awslc, out_path_openssl, awslc_output_str, openssl_output_str); - tool_hash = GetHash(awslc_output_str); - openssl_hash = GetHash(openssl_output_str); + EXPECT_EQ(awslc_output_str, openssl_output_str); - EXPECT_EQ(tool_hash, openssl_hash); + // sigopts + awslc_command = + std::string(awslc_executable_path) + " dgst -sign " + key_path + + " -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:0 -out " + + sig_path_awslc + " " + in_path; - RemoveFile(input_file.c_str()); -} + openssl_command = + std::string(openssl_executable_path) + " dgst -sign " + key_path + + " -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:0 -out " + + sig_path_openssl + " " + in_path; + + RunCommandsAndCompareOutput(awslc_command, openssl_command, sig_path_awslc, + sig_path_openssl, awslc_output_str, + openssl_output_str); + + EXPECT_EQ(awslc_output_str, openssl_output_str); + + awslc_command = + std::string(awslc_executable_path) + " dgst -verify " + pubkey_path + + " -signature " + sig_path_awslc + + " -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:0 -out " + + out_path_awslc + " " + in_path; -// Test against OpenSSL output with stdin. -TEST_F(DgstComparisonTest, MD5_stdin) { - std::string tool_command = "echo hash_this_string | " + - std::string(awslc_executable_path) + " md5 > " + - out_path_awslc; - std::string openssl_command = "echo hash_this_string | " + - std::string(openssl_executable_path) + - " md5 > " + out_path_openssl; + openssl_command = + std::string(openssl_executable_path) + " dgst -verify " + pubkey_path + + " -signature " + sig_path_openssl + + " -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:0 -out " + + out_path_openssl + " " + in_path; - RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_awslc, + RunCommandsAndCompareOutput(awslc_command, openssl_command, out_path_awslc, out_path_openssl, awslc_output_str, openssl_output_str); - std::string tool_hash = GetHash(awslc_output_str); + EXPECT_EQ(awslc_output_str, openssl_output_str); + + // hex output + awslc_command = std::string(awslc_executable_path) + " dgst -sign " + + key_path + " -hex -out " + sig_path_awslc + " " + in_path; + + openssl_command = std::string(openssl_executable_path) + " dgst -sign " + + key_path + " -hex -out " + sig_path_openssl + " " + in_path; + + RunCommandsAndCompareOutput(awslc_command, openssl_command, sig_path_awslc, + sig_path_openssl, awslc_output_str, + openssl_output_str); + + std::string awslc_hash = GetHash(awslc_output_str); std::string openssl_hash = GetHash(openssl_output_str); - EXPECT_EQ(tool_hash, openssl_hash); + EXPECT_EQ(awslc_hash, openssl_hash); +} + +class DgstTest : 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 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]; + std::string awslc_output_str; + std::string openssl_output_str; +}; + +TEST_F(DgstTest, HMAC) { + args_list_t args = {"-hmac", "test_key_string", in_path}; + EXPECT_TRUE(dgstTool(args)); +} + +TEST_F(DgstTest, Sign) { + args_list_t args = {"-sign", key_path, "-out", sig_path, in_path}; + EXPECT_TRUE(dgstTool(args)); +} + +TEST_F(DgstTest, Verify) { + // First create signature + args_list_t sign_args = {"-sign", key_path, "-out", sig_path, in_path}; + EXPECT_TRUE(dgstTool(sign_args)); + + // Then verify + args_list_t verify_args = {"-verify", pubkey_path, "-signature", sig_path, + in_path}; + EXPECT_TRUE(dgstTool(verify_args)); +} + +TEST_F(DgstTest, DigestSHA256) { + std::ofstream ofs(in_path); + ofs << "test data"; + ofs.close(); + + args_list_t args = {"-sha256", in_path}; + EXPECT_TRUE(dgstTool(args)); +} + +TEST_F(DgstTest, DigestSHA1) { + std::ofstream ofs(in_path); + ofs << "test data"; + ofs.close(); + + args_list_t args = {"-sha1", in_path}; + EXPECT_TRUE(dgstTool(args)); +} + +TEST_F(DgstTest, FileInput) { + // Single file input + args_list_t single_args = {in_path}; + EXPECT_TRUE(dgstTool(single_args)); + + // Multiple file inputs + char in_path2[PATH_MAX]; + ASSERT_GT(createTempFILEpath(in_path2), 0u); + std::ofstream ofs2(in_path2); + ofs2 << "AWS_LC_TEST_STRING_INPUT_2"; + ofs2.close(); + + args_list_t multi_args = {in_path, in_path2}; + EXPECT_TRUE(dgstTool(multi_args)); + + RemoveFile(in_path2); +} + +class DgstOptionUsageErrorsTest : public DgstTest { + 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 = dgstTool(c_args); + ASSERT_FALSE(result); + } +}; + +TEST_F(DgstOptionUsageErrorsTest, InvalidCombinations) { + std::vector> invalid_combos = { + // unsupported keyform + {"-verify", key_path, "-signature", sig_path, "-keyform", "ENGINE", + in_path}, + // verify without sig file + {"-verify", key_path, "-keyform", "ENGINE", in_path}, + // hmac, verify, and sign used together + {"-hmac", "test_key_string", "-verify", key_path, "-signature", sig_path, + in_path}, + {"-hmac", "test_key_string", "-sign", pubkey_path, in_path}, + {"-verify", key_path, "-sign", pubkey_path, in_path}, + // wrong use of sigopt + {"-sign", pubkey_path, "-sigopt", "abc:xyz", in_path}, + // unsupported digest + {"-sha3224", in_path}, + // hex and binary both specified + {"-hex", "-binary", in_path}}; + + for (const auto &args : invalid_combos) { + TestOptionUsageErrors(args); + } } diff --git a/tool-openssl/internal.h b/tool-openssl/internal.h index 26af3230b6..900db3fe13 100644 --- a/tool-openssl/internal.h +++ b/tool-openssl/internal.h @@ -111,6 +111,16 @@ static inline ordered_args_map_t::const_iterator FindArg( }); } +static inline void FindAll(std::vector &result, + const std::string &arg_name, + const ordered_args_map_t &args) { + for (const auto &pair : args) { + if (pair.first == arg_name) { + result.push_back(pair.second); + } + } +} + // Parse arguments in order of appearance bool ParseOrderedKeyValueArguments(ordered_args_map_t &out_args, args_list_t &extra_args, @@ -124,6 +134,10 @@ bool GetString(std::string *out, const std::string &arg_name, std::string default_value, const ordered_args_map_t &args); bool GetBoolArgument(bool *out, const std::string &arg_name, const ordered_args_map_t &args); + +bool GetExclusiveBoolArgument(std::string *out_arg, const argument_t *templates, + std::string default_out_arg, + const ordered_args_map_t &args); } // namespace ordered_args #endif // TOOL_OPENSSL_INTERNAL_H diff --git a/tool-openssl/ordered_args.cc b/tool-openssl/ordered_args.cc index 188069279a..6a06aabbe7 100644 --- a/tool-openssl/ordered_args.cc +++ b/tool-openssl/ordered_args.cc @@ -23,6 +23,8 @@ bool ParseOrderedKeyValueArguments(ordered_args_map_t &out_args, out_args.clear(); extra_args.clear(); + std::vector exclusive_boolean_args; + for (size_t i = 0; i < args.size(); i++) { const std::string &arg = args[i]; const argument_t *templ = nullptr; @@ -45,22 +47,36 @@ bool ParseOrderedKeyValueArguments(ordered_args_map_t &out_args, // Check for duplicate arguments - allowed for order preservation // but warn about it when debugging #ifndef NDEBUG - if (HasArgument(out_args, arg)) { + if (templ->type != kDuplicateArgument && HasArgument(out_args, arg)) { fprintf(stderr, "Warning: Duplicate argument: %s\n", arg.c_str()); } #endif - if (templ->type == kBooleanArgument) { + if (templ->type == kBooleanArgument || + templ->type == kExclusiveBooleanArgument) { out_args.push_back(std::pair(arg, "")); + if (templ->type == kExclusiveBooleanArgument) { + exclusive_boolean_args.push_back(arg); + } } else { if (i + 1 >= args.size()) { fprintf(stderr, "Missing argument for option: %s\n", arg.c_str()); return false; } + out_args.push_back(std::pair(arg, args[++i])); } } + if (exclusive_boolean_args.size() > 1) { + fprintf(stderr, "These arguments cannot be used together: "); + for (size_t i = 0; i < exclusive_boolean_args.size(); ++i) { + fprintf(stderr, "%s%s", exclusive_boolean_args[i].c_str(), + (i < exclusive_boolean_args.size() - 1) ? ", " : "\n"); + } + return false; + } + for (size_t j = 0; templates[j].name[0] != 0; j++) { const argument_t *templ = &templates[j]; if (templ->type == kRequiredArgument && @@ -130,4 +146,22 @@ bool GetBoolArgument(bool *out, const std::string &arg_name, return true; } +bool GetExclusiveBoolArgument(std::string *out_arg, const argument_t *templates, + std::string default_out_arg, + const ordered_args_map_t &args) { + *out_arg = default_out_arg; + + for (size_t i = 0; templates[i].name[0] != 0; i++) { + const argument_t *templ = &templates[i]; + if (templ->type == kExclusiveBooleanArgument) { + auto it = FindArg(args, templ->name); + if (it != args.end()) { + *out_arg = templ->name; + break; + } + } + } + + return true; +} } // namespace ordered_args diff --git a/tool-openssl/test_util.h b/tool-openssl/test_util.h index 26f47bc069..a96f78f4ec 100644 --- a/tool-openssl/test_util.h +++ b/tool-openssl/test_util.h @@ -19,20 +19,23 @@ // comparison output static inline std::string &trim(std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { - return !std::isspace(static_cast(ch)); - })); - s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { - return !std::isspace(static_cast(ch)); - }).base(), s.end()); + return !std::isspace(static_cast(ch)); + })); + s.erase(std::find_if(s.rbegin(), s.rend(), + [](unsigned char ch) { + return !std::isspace(static_cast(ch)); + }) + .base(), + s.end()); return s; } // Helper function to read file content into a string -inline std::string ReadFileToString(const std::string& file_path) { +inline std::string ReadFileToString(const std::string &file_path) { if (file_path.empty()) { return ""; } - + // Check if file exists first struct stat stat_buffer; if (stat(file_path.c_str(), &stat_buffer) != 0) { @@ -43,16 +46,19 @@ inline std::string ReadFileToString(const std::string& file_path) { if (!file_stream.is_open()) { return ""; } - + std::ostringstream output_buffer; output_buffer << file_stream.rdbuf(); - + return output_buffer.str(); } -inline void RunCommandsAndCompareOutput(const std::string &tool_command, const std::string &openssl_command, - const std::string &out_path_tool, const std::string &out_path_openssl, - std::string &tool_output_str, std::string &openssl_output_str) { +inline void RunCommandsAndCompareOutput(const std::string &tool_command, + const std::string &openssl_command, + const std::string &out_path_tool, + const std::string &out_path_openssl, + std::string &tool_output_str, + std::string &openssl_output_str) { int tool_result = system(tool_command.c_str()); ASSERT_EQ(tool_result, 0) << "AWS-LC tool command failed: " << tool_command; @@ -60,15 +66,20 @@ inline void RunCommandsAndCompareOutput(const std::string &tool_command, const s ASSERT_EQ(openssl_result, 0) << "OpenSSL command failed: " << openssl_command; std::ifstream tool_output(out_path_tool); - tool_output_str = std::string((std::istreambuf_iterator(tool_output)), std::istreambuf_iterator()); + 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()); + openssl_output_str = + std::string((std::istreambuf_iterator(openssl_output)), + std::istreambuf_iterator()); - std::cout << "AWS-LC tool output:" << std::endl << tool_output_str << std::endl; - std::cout << "OpenSSL output:" << std::endl << openssl_output_str << std::endl; + std::cout << "AWS-LC tool output:" << std::endl + << tool_output_str << std::endl; + std::cout << "OpenSSL output:" << std::endl + << openssl_output_str << std::endl; } -inline void RemoveFile(const char* path) { +inline void RemoveFile(const char *path) { struct stat buffer; if (path != nullptr && stat(path, &buffer) == 0) { if (remove(path) != 0) { @@ -78,6 +89,6 @@ inline void RemoveFile(const char* path) { } // OpenSSL versions 3.1.0 and later change from "(stdin)= " to "MD5(stdin) =" -std::string GetHash(const std::string& str); +std::string GetHash(const std::string &str); -#endif //TEST_UTIL_H +#endif // TEST_UTIL_H diff --git a/tool/args.cc b/tool/args.cc index 9581430cd9..6047d7b9f8 100644 --- a/tool/args.cc +++ b/tool/args.cc @@ -23,17 +23,18 @@ #include "internal.h" -bool IsFlag(const std::string& arg) { +bool IsFlag(const std::string &arg) { return arg.length() > 1 && arg[0] == '-'; } -bool ParseKeyValueArguments(args_map_t &out_args, - args_list_t &extra_args, - const args_list_t &args, - const argument_t *templates) { +bool ParseKeyValueArguments(args_map_t &out_args, args_list_t &extra_args, + const args_list_t &args, + const argument_t *templates) { out_args.clear(); extra_args.clear(); + std::vector exclusive_boolean_args; + for (size_t i = 0; i < args.size(); i++) { const std::string &arg = args[i]; const argument_t *templ = nullptr; @@ -45,7 +46,7 @@ bool ParseKeyValueArguments(args_map_t &out_args, } if (templ == nullptr) { - if(IsFlag(arg)) { + if (IsFlag(arg)) { fprintf(stderr, "Unknown flag: %s\n", arg.c_str()); return false; } @@ -58,8 +59,13 @@ bool ParseKeyValueArguments(args_map_t &out_args, return false; } - if (templ->type == kBooleanArgument) { + if (templ->type == kBooleanArgument || + templ->type == kExclusiveBooleanArgument) { out_args[arg] = ""; + + if (templ->type == kExclusiveBooleanArgument) { + exclusive_boolean_args.push_back(arg); + } } else { if (i + 1 >= args.size()) { fprintf(stderr, "Missing argument for option: %s\n", arg.c_str()); @@ -69,9 +75,18 @@ bool ParseKeyValueArguments(args_map_t &out_args, } } + if (exclusive_boolean_args.size() > 1) { + fprintf(stderr, "These arguments cannot be used together: "); + for (size_t i = 0; i < exclusive_boolean_args.size(); ++i) { + fprintf(stderr, "%s%s", exclusive_boolean_args[i].c_str(), + (i < exclusive_boolean_args.size() - 1) ? ", " : "\n"); + } + return false; + } + for (size_t j = 0; templates[j].name[0] != 0; j++) { const argument_t *templ = &templates[j]; - if (templ->type == kRequiredArgument && + if ((templ->type == kRequiredArgument) && out_args.find(templ->name) == out_args.end()) { fprintf(stderr, "Missing value for required argument: %s\n", templ->name); return false; @@ -84,13 +99,12 @@ bool ParseKeyValueArguments(args_map_t &out_args, void PrintUsage(const argument_t *templates) { for (size_t i = 0; templates[i].name[0] != 0; i++) { const argument_t *templ = &templates[i]; - fprintf(stderr, "%s\t%s\n", templ->name, templ->description); + fprintf(stderr, "%-20s%s\n", templ->name, templ->description); } } bool GetUnsigned(unsigned *out, const std::string &arg_name, - unsigned default_value, - const args_map_t &args) { + unsigned default_value, const args_map_t &args) { const auto &it = args.find(arg_name); if (it == args.end()) { *out = default_value; @@ -118,7 +132,6 @@ bool GetUnsigned(unsigned *out, const std::string &arg_name, bool GetString(std::string *out, const std::string &arg_name, std::string default_value, const args_map_t &args) { - const auto &it = args.find(arg_name); if (it == args.end()) { *out = default_value; @@ -133,7 +146,6 @@ bool GetString(std::string *out, const std::string &arg_name, bool GetBoolArgument(bool *out, const std::string &arg_name, const args_map_t &args) { - const auto &it = args.find(arg_name); if (it == args.end()) { // Boolean argument not found @@ -144,4 +156,3 @@ bool GetBoolArgument(bool *out, const std::string &arg_name, return true; } - diff --git a/tool/internal.h b/tool/internal.h index a4247a3a46..33955685e9 100644 --- a/tool/internal.h +++ b/tool/internal.h @@ -19,20 +19,18 @@ #if defined(OPENSSL_IS_AWSLC) || defined(OPENSSL_IS_BORINGSSL) #include -#include #include +#include #endif +#include +#include #include #include #include -#include -#include struct FileCloser { - void operator()(FILE *file) { - fclose(file); - } + void operator()(FILE *file) { fclose(file); } }; using ScopedFILE = std::unique_ptr; @@ -102,6 +100,8 @@ enum ArgumentType { kRequiredArgument, kOptionalArgument, kBooleanArgument, + kDuplicateArgument, + kExclusiveBooleanArgument, }; typedef struct argument_t { @@ -113,30 +113,39 @@ typedef struct argument_t { typedef std::vector args_list_t; typedef std::map args_map_t; -bool IsFlag(const std::string& arg); +bool IsFlag(const std::string &arg); -// ParseKeyValueArguments converts the list of strings |args| ["-filter", "RSA", "-Timeout", "10"] into a map in -// |out_args| of key value pairs {"-filter": "RSA", "-Timeout": "10"}. It uses |templates| to determine what arguments -// are option or required. Any extra arguments that don't look like an unknown flag argument (prefixed by "-" or "--") -// will be appended to extra_args in the order they appear in. -bool ParseKeyValueArguments(args_map_t &out_args, - args_list_t &extra_args, +// ParseKeyValueArguments converts the list of strings |args| ["-filter", "RSA", +// "-Timeout", "10"] into a map in |out_args| of key value pairs {"-filter": +// "RSA", "-Timeout": "10"}. It uses |templates| to determine what arguments are +// option or required. Any extra arguments that don't look like an unknown flag +// argument (prefixed by "-" or "--") will be appended to extra_args in the +// order they appear in. +bool ParseKeyValueArguments(args_map_t &out_args, args_list_t &extra_args, const args_list_t &args, const argument_t *templates); -// PrintUsage prints the description from the list of templates in |templates| to stderr. +// PrintUsage prints the description from the list of templates in |templates| +// to stderr. void PrintUsage(const argument_t *templates); // Get{Unsigned, String} assign |out| the value of |arg_name| from the map // |args| if it is present. If |arg_name| is not found in |args| it assigns // |out| to the |default_value|. -bool GetUnsigned(unsigned *out, const std::string &arg_name, unsigned default_value, const args_map_t &args); -bool GetString(std::string *out, const std::string &arg_name, std::string default_value, const args_map_t &args); +bool GetUnsigned(unsigned *out, const std::string &arg_name, + unsigned default_value, const args_map_t &args); +bool GetString(std::string *out, const std::string &arg_name, + std::string default_value, const args_map_t &args); // GetBoolArgument assigns |out| the value |true| if |arg_name|, of // type |kBooleanArgument|, from the map |args| is present. If |arg_name| is not // found in |args| it assigns |out| to the value |false|. -bool GetBoolArgument(bool *out, const std::string &arg_name, const args_map_t &args); +bool GetBoolArgument(bool *out, const std::string &arg_name, + const args_map_t &args); + +bool GetExclusiveBoolArgument(std::string *out_arg, const argument_t *templates, + std::string default_out_arg, + const args_map_t &args); bool ReadAll(std::vector *out, FILE *in); bool WriteToFile(const std::string &path, const uint8_t *in, size_t in_len); @@ -145,7 +154,8 @@ bool WriteToFile(const std::string &path, const uint8_t *in, size_t in_len); // bssl and openssl tools. It takes an additional parameter |tool| to indicate // which tool's s_client is being invoked. A value of true indicates openssl // and false indicates the internal bssl tool. -bool DoClient(std::map args_map, bool is_openssl_s_client); +bool DoClient(std::map args_map, + bool is_openssl_s_client); bool Ciphers(const std::vector &args); bool Client(const std::vector &args); From afd0c3ceb144fe00a5fb1fbe657f73b2e6f2cdc9 Mon Sep 17 00:00:00 2001 From: Hailey Ho Date: Tue, 26 Aug 2025 21:51:17 +0000 Subject: [PATCH 2/4] nit --- tool-openssl/dgst.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tool-openssl/dgst.cc b/tool-openssl/dgst.cc index 88b6ce0ff3..c92be03e79 100644 --- a/tool-openssl/dgst.cc +++ b/tool-openssl/dgst.cc @@ -288,7 +288,7 @@ static bool VerifySignature(EVP_PKEY *pkey, const std::string &in_path, const std::string &signature_file_path, bssl::UniquePtr &out_bio) { bssl::ScopedEVP_MD_CTX ctx; - EVP_PKEY_CTX *pctx; + EVP_PKEY_CTX *pctx = nullptr; if (!EVP_DigestVerifyInit(ctx.get(), &pctx, digest, nullptr, pkey)) { fprintf(stderr, "Failed to initialize digest context.\n"); @@ -407,7 +407,7 @@ static bool dgstToolInternal(const args_list_t &args, const EVP_MD *digest) { if (HasArgument(parsed_args, "-help")) { PrintUsage(kArguments); - return false; + return true; } GetBoolArgument(&binary, "-binary", parsed_args); From 92bc23de25ca51711282ec4f80e8ee6b56b76cc2 Mon Sep 17 00:00:00 2001 From: Hailey Ho Date: Thu, 28 Aug 2025 20:51:40 +0000 Subject: [PATCH 3/4] Incorporate feedback --- tool-openssl/dgst.cc | 162 +++++++++++++++----------------------- tool-openssl/dgst_test.cc | 20 ++--- 2 files changed, 72 insertions(+), 110 deletions(-) diff --git a/tool-openssl/dgst.cc b/tool-openssl/dgst.cc index c92be03e79..356dfbc64f 100644 --- a/tool-openssl/dgst.cc +++ b/tool-openssl/dgst.cc @@ -27,7 +27,8 @@ static const argument_t kArguments[] = { {"-hmac", kOptionalArgument, "Create a hashed MAC with the corresponding key"}, {"-sigopt", kOptionalArgument, "Signature parameter in n:v form"}, - {"-passin", kOptionalArgument, "Input file pass phrase source"}, + // TODO: Implement -passin + // {"-passin", kOptionalArgument, "Input file pass phrase source"}, {"-sign", kOptionalArgument, "Sign digest using private key"}, {"-out", kOptionalArgument, "Output to filename rather than stdout"}, {"-verify", kOptionalArgument, "Verify a signature using public key"}, @@ -118,8 +119,8 @@ static std::string GetSigName(int nid) { } } -static bool GenerateHash(const std::string &in_path, const EVP_MD *digest, - std::vector &hash) { +static bool GenerateHash(FILE *in_file, const std::string &in_path, + const EVP_MD *digest, std::vector &hash) { bssl::ScopedEVP_MD_CTX ctx; if (!EVP_DigestInit_ex(ctx.get(), digest, nullptr)) { @@ -127,26 +128,15 @@ static bool GenerateHash(const std::string &in_path, const EVP_MD *digest, return false; } - 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; - } - } - uint8_t buf[4096]; for (;;) { - size_t n = fread(buf, 1, sizeof(buf), in_file.get()); - - if (feof(in_file.get())) { + if (feof(in_file)) { break; } - if (ferror(in_file.get())) { + + size_t n = fread(buf, 1, sizeof(buf), in_file); + + if (ferror(in_file)) { fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); return false; } @@ -169,35 +159,24 @@ static bool GenerateHash(const std::string &in_path, const EVP_MD *digest, return true; } -static bool GenerateHMAC(const std::string &in_path, const char *hmac_key, - const size_t hmac_key_len, const EVP_MD *digest, - std::vector &mac) { +static bool GenerateHMAC(FILE *in_file, const std::string &in_path, + const char *hmac_key, const size_t hmac_key_len, + const EVP_MD *digest, std::vector &mac) { bssl::ScopedHMAC_CTX ctx; if (!HMAC_Init_ex(ctx.get(), hmac_key, hmac_key_len, digest, nullptr)) { fprintf(stderr, "Failed to initialize HMAC_Init_ex.\n"); return false; } - 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; - } - } - uint8_t buf[4096]; for (;;) { - size_t n = fread(buf, 1, sizeof(buf), in_file.get()); - - if (feof(in_file.get())) { + if (feof(in_file)) { break; } - if (ferror(in_file.get())) { + + size_t n = fread(buf, 1, sizeof(buf), in_file); + + if (ferror(in_file)) { fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); return false; } @@ -219,8 +198,8 @@ static bool GenerateHMAC(const std::string &in_path, const char *hmac_key, return true; } -static bool GenerateSignature(EVP_PKEY *pkey, const std::string &in_path, - const EVP_MD *digest, +static bool GenerateSignature(EVP_PKEY *pkey, FILE *in_file, + const std::string &in_path, const EVP_MD *digest, const std::vector &sigopts, std::vector &signature) { bssl::ScopedEVP_MD_CTX ctx; @@ -240,26 +219,15 @@ static bool GenerateSignature(EVP_PKEY *pkey, const std::string &in_path, } } - 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; - } - } - uint8_t buf[4096]; for (;;) { - size_t n = fread(buf, 1, sizeof(buf), in_file.get()); - - if (feof(in_file.get())) { + if (feof(in_file)) { break; } - if (ferror(in_file.get())) { + + size_t n = fread(buf, 1, sizeof(buf), in_file); + + if (ferror(in_file)) { fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); return false; } @@ -282,11 +250,11 @@ static bool GenerateSignature(EVP_PKEY *pkey, const std::string &in_path, return true; } -static bool VerifySignature(EVP_PKEY *pkey, const std::string &in_path, - const EVP_MD *digest, +static bool VerifySignature(EVP_PKEY *pkey, FILE *in_file, + const std::string &in_path, const EVP_MD *digest, const std::vector &sigopts, const std::string &signature_file_path, - bssl::UniquePtr &out_bio) { + BIO *out_bio) { bssl::ScopedEVP_MD_CTX ctx; EVP_PKEY_CTX *pctx = nullptr; @@ -304,25 +272,15 @@ static bool VerifySignature(EVP_PKEY *pkey, const std::string &in_path, } } - 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 opening input file '%s'\n", in_path.c_str()); - return false; - } - } - uint8_t buf[4096]; for (;;) { - size_t n = fread(buf, 1, sizeof(buf), in_file.get()); - - if (feof(in_file.get())) { + if (feof(in_file)) { break; } - if (ferror(in_file.get())) { + + size_t n = fread(buf, 1, sizeof(buf), in_file); + + if (ferror(in_file)) { fprintf(stderr, "Error reading from '%s'.\n", in_path.c_str()); return false; } @@ -332,7 +290,7 @@ static bool VerifySignature(EVP_PKEY *pkey, const std::string &in_path, return false; } } - std::vector signature(EVP_PKEY_size(pkey)); + std::vector signature; ScopedFILE signature_file(fopen(signature_file_path.c_str(), "rb")); if (!signature_file) { fprintf(stderr, "Error opening signature file %s.\n", @@ -350,40 +308,39 @@ static bool VerifySignature(EVP_PKEY *pkey, const std::string &in_path, EVP_DigestVerifyFinal(ctx.get(), signature.data(), signature.size()); if (result > 0) { - BIO_printf(out_bio.get(), "Verified OK\n"); + BIO_printf(out_bio, "Verified OK\n"); } else if (result == 0) { - BIO_printf(out_bio.get(), "Verification failure\n"); + BIO_printf(out_bio, "Verification failure\n"); } else { - BIO_printf(out_bio.get(), "Error verifying data\n"); + BIO_printf(out_bio, "Error verifying data\n"); } return true; } -static bool WriteOutput(bssl::UniquePtr &out_bio, - const std::vector &data, +static bool WriteOutput(BIO *out_bio, const std::vector &data, const std::string &sig_name, const std::string &digest_name, const std::string &in_path, bool out_bin) { if (out_bin) { - BIO_write(out_bio.get(), data.data(), data.size()); + BIO_write(out_bio, data.data(), data.size()); } else { if (!sig_name.empty()) { - BIO_printf(out_bio.get(), "%s-%s", sig_name.c_str(), digest_name.c_str()); + BIO_printf(out_bio, "%s-%s", sig_name.c_str(), digest_name.c_str()); } else { - BIO_printf(out_bio.get(), "%s", digest_name.c_str()); + BIO_printf(out_bio, "%s", digest_name.c_str()); } if (!in_path.empty()) { - BIO_printf(out_bio.get(), "(%s)=", in_path.c_str()); + BIO_printf(out_bio, "(%s)=", in_path.c_str()); } else { - BIO_printf(out_bio.get(), "(stdin)="); + BIO_printf(out_bio, "(stdin)="); } for (size_t i = 0; i < data.size(); i++) { - BIO_printf(out_bio.get(), "%02x", data[i]); + BIO_printf(out_bio, "%02x", data[i]); } - BIO_printf(out_bio.get(), "\n"); + BIO_printf(out_bio, "\n"); } return true; @@ -428,7 +385,8 @@ static bool dgstToolInternal(const args_list_t &args, const EVP_MD *digest) { } if ((!verify_key_file.empty() + !sign_key_file.empty() + !hmac.empty()) > 1) { - fprintf(stderr, "Error: MAC and signing key cannot both be specified\n"); + fprintf(stderr, + "Error: -verify, -sign, and -hmac cannot be used together\n"); return false; } @@ -473,11 +431,19 @@ static bool dgstToolInternal(const args_list_t &args, const EVP_MD *digest) { bssl::UniquePtr pkey; size_t i = 0; do { + ScopedFILE in_file; std::string in_path; if (in_files.empty()) { - in_path = ""; + in_path = "stdin"; + in_file.reset(stdin); } else { in_path = in_files[i++]; + 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 (!hmac.empty()) { @@ -486,19 +452,20 @@ static bool dgstToolInternal(const args_list_t &args, const EVP_MD *digest) { std::vector mac(EVP_MD_size(digest)); - if (!GenerateHMAC(in_path, hmac_key, hmac_key_len, digest, mac)) { + if (!GenerateHMAC(in_file.get(), in_path, hmac_key, hmac_key_len, digest, + mac)) { return false; } - WriteOutput(out_bio, mac, "HMAC", EVP_MD_get0_name(digest), in_path, + WriteOutput(out_bio.get(), mac, "HMAC", EVP_MD_get0_name(digest), in_path, out_bin); } else if (!verify_key_file.empty()) { if (!LoadPublicKey(verify_key_file, pkey)) { return false; } - if (!VerifySignature(pkey.get(), in_path, digest, sigopts, signature_file, - out_bio)) { + if (!VerifySignature(pkey.get(), in_file.get(), in_path, digest, sigopts, + signature_file, out_bio.get())) { return false; } } else if (!sign_key_file.empty()) { @@ -507,20 +474,21 @@ static bool dgstToolInternal(const args_list_t &args, const EVP_MD *digest) { } std::vector signature(EVP_PKEY_size(pkey.get())); - if (!GenerateSignature(pkey.get(), in_path, digest, sigopts, signature)) { + if (!GenerateSignature(pkey.get(), in_file.get(), in_path, digest, + sigopts, signature)) { return false; } - WriteOutput(out_bio, signature, GetSigName(EVP_PKEY_id(pkey.get())), + WriteOutput(out_bio.get(), signature, GetSigName(EVP_PKEY_id(pkey.get())), EVP_MD_get0_name(digest), in_path, out_bin); } else { std::vector hash(EVP_MAX_MD_SIZE); - if (!GenerateHash(in_path, digest, hash)) { + if (!GenerateHash(in_file.get(), in_path, digest, hash)) { return false; } - WriteOutput(out_bio, hash, "", EVP_MD_get0_name(digest), in_path, + WriteOutput(out_bio.get(), hash, "", EVP_MD_get0_name(digest), in_path, out_bin); } diff --git a/tool-openssl/dgst_test.cc b/tool-openssl/dgst_test.cc index 6996e0b0c8..64ec6ae153 100644 --- a/tool-openssl/dgst_test.cc +++ b/tool-openssl/dgst_test.cc @@ -318,20 +318,12 @@ TEST_F(DgstTest, Verify) { EXPECT_TRUE(dgstTool(verify_args)); } -TEST_F(DgstTest, DigestSHA256) { - std::ofstream ofs(in_path); - ofs << "test data"; - ofs.close(); - - args_list_t args = {"-sha256", in_path}; +TEST_F(DgstTest, DigestDefault) { + args_list_t args = {in_path}; EXPECT_TRUE(dgstTool(args)); } TEST_F(DgstTest, DigestSHA1) { - std::ofstream ofs(in_path); - ofs << "test data"; - ofs.close(); - args_list_t args = {"-sha1", in_path}; EXPECT_TRUE(dgstTool(args)); } @@ -344,9 +336,11 @@ TEST_F(DgstTest, FileInput) { // Multiple file inputs char in_path2[PATH_MAX]; ASSERT_GT(createTempFILEpath(in_path2), 0u); - std::ofstream ofs2(in_path2); - ofs2 << "AWS_LC_TEST_STRING_INPUT_2"; - ofs2.close(); + ScopedFILE in_file2(fopen(in_path2, "wb")); + ASSERT_TRUE(in_file2); + const char *test_data = "AWS_LC_TEST_STRING_INPUT_2"; + ASSERT_EQ(fwrite(test_data, 1, strlen(test_data), in_file2.get()), + strlen(test_data)); args_list_t multi_args = {in_path, in_path2}; EXPECT_TRUE(dgstTool(multi_args)); From f0ce443d4b36d0446c8dce6221cf42f6989900e8 Mon Sep 17 00:00:00 2001 From: Hailey Ho Date: Wed, 27 Aug 2025 22:37:32 +0000 Subject: [PATCH 4/4] Change sigopt argument type to allow duplicate arguments --- tool-openssl/dgst.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool-openssl/dgst.cc b/tool-openssl/dgst.cc index 356dfbc64f..11b70ee682 100644 --- a/tool-openssl/dgst.cc +++ b/tool-openssl/dgst.cc @@ -26,7 +26,7 @@ static const argument_t kArguments[] = { {"-sha512", kExclusiveBooleanArgument, "Supported digest function"}, {"-hmac", kOptionalArgument, "Create a hashed MAC with the corresponding key"}, - {"-sigopt", kOptionalArgument, "Signature parameter in n:v form"}, + {"-sigopt", kDuplicateArgument, "Signature parameter in n:v form"}, // TODO: Implement -passin // {"-passin", kOptionalArgument, "Input file pass phrase source"}, {"-sign", kOptionalArgument, "Sign digest using private key"},