diff --git a/crypto/fipsmodule/evp/evp.c b/crypto/fipsmodule/evp/evp.c index 6c7c6dfa3b..dd6eeef57a 100644 --- a/crypto/fipsmodule/evp/evp.c +++ b/crypto/fipsmodule/evp/evp.c @@ -171,7 +171,7 @@ int EVP_read_pw_string_min(char *buf, int min_length, int length, int ret = -1; char verify_buf[1024]; - if (!buf || min_length <= 0 || min_length >= length) { + if (!buf || min_length < 0 || min_length >= length) { return -1; } diff --git a/crypto/pem/pem_pkey.c b/crypto/pem/pem_pkey.c index 7a3ec7461c..f11fc00747 100644 --- a/crypto/pem/pem_pkey.c +++ b/crypto/pem/pem_pkey.c @@ -109,7 +109,7 @@ EVP_PKEY *PEM_read_bio_PrivateKey(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, cb = PEM_def_callback; } int pass_len = cb(psbuf, PEM_BUFSIZE, 0, u); - if (pass_len <= 0) { + if (pass_len < 0) { OPENSSL_PUT_ERROR(PEM, PEM_R_BAD_PASSWORD_READ); X509_SIG_free(p8); goto err; diff --git a/tool-openssl/CMakeLists.txt b/tool-openssl/CMakeLists.txt index 5834ad284b..525f4ef718 100644 --- a/tool-openssl/CMakeLists.txt +++ b/tool-openssl/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable( crl.cc dgst.cc + pass_util.cc pkcs8.cc pkey.cc rehash.cc @@ -87,6 +88,8 @@ if(BUILD_TESTING) crl_test.cc dgst.cc dgst_test.cc + pass_util.cc + pass_util_test.cc pkcs8.cc pkcs8_test.cc pkey.cc diff --git a/tool-openssl/internal.h b/tool-openssl/internal.h index 26af3230b6..ba676ab536 100644 --- a/tool-openssl/internal.h +++ b/tool-openssl/internal.h @@ -5,8 +5,8 @@ #define TOOL_OPENSSL_INTERNAL_H #include -#include #include +#include #include #include #include @@ -24,13 +24,67 @@ struct Tool { bool IsNumeric(const std::string &str); -X509* CreateAndSignX509Certificate(); -X509_CRL* createTestCRL(); +X509 *CreateAndSignX509Certificate(); +X509_CRL *createTestCRL(); bool isStringUpperCaseEqual(const std::string &a, const std::string &b); +// Password extracting utility for -passin and -passout options +namespace pass_util { +// Password source types for handling different input methods +enum class Source : uint8_t { + kNone, // Empty or invalid source + kPass, // Direct password with pass: prefix + kFile, // Password from file with file: prefix + kEnv, // Password from environment with env: prefix + kStdin, // Password from stdin +#ifndef _WIN32 + kFd, // Password from file descriptor with fd: prefix (Unix only) +#endif +}; + +// Custom deleter for sensitive strings that securely clears memory before +// deletion. This ensures passwords are securely removed from memory when no +// longer needed, preventing potential exposure in memory dumps or swap files. +void SensitiveStringDeleter(std::string *str); + +// Extracts password from a source string, modifying it in place if successful. +// source: Password source string in one of the following formats: +// - pass:password (direct password, e.g., "pass:mypassword") +// - file:/path/to/file (password from file) +// - env:VAR_NAME (password from environment variable) +// - stdin (password from standard input) +// - fd:N (password from file descriptor N, Unix only) +// The source string will be replaced with the extracted password if successful. +// Returns bool indicating success or failure: +// - true: Password was successfully extracted and stored in source +// - false: Error occurred, error message printed to stderr +// Error cases: +// - Invalid format string (missing or unknown prefix) +// - File access errors (file not found, permission denied) +// - Environment variable not set +// - Memory allocation failures +bool ExtractPassword(bssl::UniquePtr &source); + +// Same process as ExtractPassword but used for -passin and -passout within same +// tool. Special handling: +// - If same file is used for both passwords, reads first line for passin +// and second line for passout in a single file operation matching OpenSSL +// behavior +// - If stdin is used for both passwords, reads first line for passin +// and second line for passout from standard input matching OpenSSL behavior +bool ExtractPasswords(bssl::UniquePtr &passin, + bssl::UniquePtr &passout); + +} // namespace pass_util + +// Custom deleter used for -passin -passout options +BSSL_NAMESPACE_BEGIN +BORINGSSL_MAKE_DELETER(std::string, pass_util::SensitiveStringDeleter) +BSSL_NAMESPACE_END + bool LoadPrivateKeyAndSignCertificate(X509 *x509, const std::string &signkey_path); -EVP_PKEY* CreateTestKey(int key_bits); +EVP_PKEY *CreateTestKey(int key_bits); tool_func_t FindTool(const std::string &name); tool_func_t FindTool(int argc, char **argv, int &starting_arg); diff --git a/tool-openssl/pass_util.cc b/tool-openssl/pass_util.cc new file mode 100644 index 0000000000..992ba2c7e8 --- /dev/null +++ b/tool-openssl/pass_util.cc @@ -0,0 +1,334 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include +#include +#include "internal.h" + +// Use PEM_BUFSIZE (defined in openssl/pem.h) for password buffer size to ensure +// compatibility with PEM functions and password callbacks throughout AWS-LC + +// Detect the type of password source +static pass_util::Source DetectSource( + const bssl::UniquePtr &source) { + if (!source || source->empty()) { + return pass_util::Source::kNone; + } + if (source->compare(0, 5, "pass:") == 0) { + return pass_util::Source::kPass; + } + if (source->compare(0, 5, "file:") == 0) { + return pass_util::Source::kFile; + } + if (source->compare(0, 4, "env:") == 0) { + return pass_util::Source::kEnv; + } + if (source->compare("stdin") == 0) { + return pass_util::Source::kStdin; + } +#ifndef _WIN32 + if (source->compare(0, 3, "fd:") == 0) { + return pass_util::Source::kFd; + } +#endif + return pass_util::Source::kNone; +} + +// Helper function to validate password sources and detect same-file case +static bool ValidateSource(bssl::UniquePtr &passin, + bssl::UniquePtr *passout = nullptr, + bool *same_file = nullptr) { + // Validate passin + if (!passin) { + fprintf(stderr, "Invalid password format (use pass:, file:, env:, or stdin)\n"); + return false; + } + + // Validate passout if provided + if (passout && !*passout) { + fprintf(stderr, "Invalid password format (use pass:, file:, env:, or stdin)\n"); + return false; + } + + // Validate passin format (if not empty) + if (!passin->empty()) { + pass_util::Source passin_type = DetectSource(passin); + if (passin_type == pass_util::Source::kNone) { + fprintf(stderr, "Invalid password format (use pass:, file:, env:, or stdin)\n"); + return false; + } + } + + // Validate passout format (if provided and not empty) + if (passout && *passout && !(*passout)->empty()) { + pass_util::Source passout_type = DetectSource(*passout); + if (passout_type == pass_util::Source::kNone) { + fprintf(stderr, "Invalid password format (use pass:, file:, env:, or stdin)\n"); + return false; + } + + // Detect same-file case if requested + if (same_file && !passin->empty()) { + pass_util::Source passin_type = DetectSource(passin); + *same_file = + (passin_type == pass_util::Source::kFile && + passout_type == pass_util::Source::kFile && *passin == **passout) || + (passin_type == pass_util::Source::kStdin && + passout_type == pass_util::Source::kStdin); + } + } + + // Initialize same_file to false if not detected + if (same_file && (!passout || !*passout)) { + *same_file = false; + } + + return true; +} + +static bool ExtractDirectPassword(bssl::UniquePtr &source) { + // Check for additional colons in password portion after prefix + if (source->find(':', 5) != std::string::npos) { + fprintf(stderr, "Invalid password format (use pass:, file:, env:, or stdin)\n"); + return false; + } + + // Check length before modification + if (source->length() - 5 > PEM_BUFSIZE) { + fprintf(stderr, "Password exceeds maximum allowed length (%d bytes)\n", + PEM_BUFSIZE); + return false; + } + + // Remove "pass:" prefix by shifting the remaining content to the beginning + source->erase(0, 5); + return true; +} + +static bool ExtractPasswordFromStream(bssl::UniquePtr &source, + pass_util::Source source_type, + bool skip_first_line = false, + bssl::UniquePtr *passout = nullptr) { + char buf[PEM_BUFSIZE] = {}; + bssl::UniquePtr bio; + + // Initialize BIO based on source type + if (source_type == pass_util::Source::kStdin) { + bio.reset(BIO_new_fp(stdin, BIO_NOCLOSE)); + } else if (source_type == pass_util::Source::kFile) { + source->erase(0, 5); // Remove "file:" prefix + bio.reset(BIO_new_file(source->c_str(), "r")); +#ifndef _WIN32 + } else if (source_type == pass_util::Source::kFd) { + source->erase(0, 3); // Remove "fd:" prefix + int fd = atoi(source->c_str()); + if (fd < 0) { + fprintf(stderr, "Invalid file descriptor: %s\n", source->c_str()); + return false; + } + bio.reset(BIO_new_fd(fd, BIO_NOCLOSE)); +#endif + } else { + fprintf(stderr, "Unsupported source type for stream extraction\n"); + return false; + } + + if (!bio) { + if (source_type == pass_util::Source::kStdin) { + fprintf(stderr, "Cannot open stdin\n"); +#ifndef _WIN32 + } else if (source_type == pass_util::Source::kFd) { + fprintf(stderr, "Cannot open file descriptor\n"); +#endif + } else { + fprintf(stderr, "Cannot open password file\n"); + } + return false; + } + + auto read_password_line = [&](std::string& target) -> bool { + int len = BIO_gets(bio.get(), buf, sizeof(buf)); + if (len <= 0) { + OPENSSL_cleanse(buf, sizeof(buf)); + if (source_type == pass_util::Source::kStdin) { + fprintf(stderr, "Failed to read password from stdin\n"); +#ifndef _WIN32 + } else if (source_type == pass_util::Source::kFd) { + fprintf(stderr, "Failed to read password from file descriptor\n"); +#endif + } else { + fprintf(stderr, "Cannot read password file\n"); + } + return false; + } + + // Check for possible truncation + if (static_cast(len) == PEM_BUFSIZE - 1 && + buf[len - 1] != '\n' && buf[len - 1] != '\r') { + OPENSSL_cleanse(buf, sizeof(buf)); + if (source_type == pass_util::Source::kStdin) { + fprintf(stderr, "Password from stdin too long (maximum %d bytes)\n", PEM_BUFSIZE); +#ifndef _WIN32 + } else if (source_type == pass_util::Source::kFd) { + fprintf(stderr, "Password from file descriptor too long (maximum %d bytes)\n", PEM_BUFSIZE); +#endif + } else { + fprintf(stderr, "Password file content too long (maximum %d bytes)\n", PEM_BUFSIZE); + } + return false; + } + + // Trim trailing newlines + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + len--; + } + + target.assign(buf, len); + return true; + }; + + // Handle same-file case (read both passwords) + if (passout) { + if (!read_password_line(*source) || !read_password_line(**passout)) { + return false; + } + } else { + // Handle skip_first_line if needed + if (skip_first_line) { + std::string dummy; + if (!read_password_line(dummy)) { + return false; + } + } + + // Read single password + if (!read_password_line(*source)) { + return false; + } + } + + OPENSSL_cleanse(buf, sizeof(buf)); + return true; +} + +static bool ExtractPasswordFromEnv(bssl::UniquePtr &source) { + // Remove "env:" prefix + source->erase(0, 4); + + if (source->empty()) { + fprintf(stderr, "Empty environment variable name\n"); + return false; + } + + const char *env_val = getenv(source->c_str()); + if (!env_val) { + fprintf(stderr, "Environment variable '%s' not set\n", source->c_str()); + return false; + } + + size_t env_val_len = strlen(env_val); + if (env_val_len == 0) { + fprintf(stderr, "Environment variable '%s' is empty\n", source->c_str()); + return false; + } + if (env_val_len > PEM_BUFSIZE) { + fprintf(stderr, "Environment variable value too long (maximum %d bytes)\n", + PEM_BUFSIZE); + return false; + } + + // Replace source content with environment value + *source = std::string(env_val); + return true; +} + +// Internal helper to extract password based on source type +static bool ExtractPasswordFromSource(bssl::UniquePtr &source, + pass_util::Source type, + bool skip_first_line = false, + bssl::UniquePtr *passout = nullptr) { + switch (type) { + case pass_util::Source::kPass: + return ExtractDirectPassword(source); + case pass_util::Source::kFile: + return ExtractPasswordFromStream(source, type, skip_first_line, passout); + case pass_util::Source::kEnv: + return ExtractPasswordFromEnv(source); + case pass_util::Source::kStdin: + return ExtractPasswordFromStream(source, type, skip_first_line, passout); +#ifndef _WIN32 + case pass_util::Source::kFd: + return ExtractPasswordFromStream(source, type, skip_first_line, passout); +#endif + default: +#ifndef _WIN32 + fprintf(stderr, "Invalid password format (use pass:, file:, env:, fd:, or stdin)\n"); +#else + fprintf(stderr, "Invalid password format (use pass:, file:, env:, or stdin)\n"); +#endif + return false; + } +} + +namespace pass_util { + +void SensitiveStringDeleter(std::string *str) { + if (str && !str->empty()) { + OPENSSL_cleanse(&(*str)[0], str->size()); + } + delete str; +} + +bool ExtractPassword(bssl::UniquePtr &source) { + if (!ValidateSource(source)) { + return false; + } + + if (source->empty()) { + fprintf(stderr, "Invalid password format (use pass:, file:, env:, or stdin)\n"); + return false; + } + + pass_util::Source type = DetectSource(source); + return ExtractPasswordFromSource(source, type); +} + +bool ExtractPasswords(bssl::UniquePtr &passin, + bssl::UniquePtr &passout) { + // Use ValidateSource for all validation and same-file detection + bool same_file = false; + if (!ValidateSource(passin, &passout, &same_file)) { + return false; + } + + // Handle same_file case with single extraction call + if (same_file && !passin->empty() && !passout->empty()) { + pass_util::Source source_type = DetectSource(passin); + return ExtractPasswordFromSource(passin, source_type, same_file, &passout); + } + + // Extract passin (always from first line) + if (!passin->empty()) { + pass_util::Source passin_type = DetectSource(passin); + if (!ExtractPasswordFromSource(passin, passin_type, false)) { + return false; + } + } + + // Extract passout (from first line if different files, second line if same + // file) + if (!passout->empty()) { + pass_util::Source passout_type = DetectSource(passout); + if (!ExtractPasswordFromSource(passout, passout_type, same_file)) { + return false; + } + } + + return true; +} + +} // namespace pass_util diff --git a/tool-openssl/pass_util_test.cc b/tool-openssl/pass_util_test.cc new file mode 100644 index 0000000000..7efbeee6cc --- /dev/null +++ b/tool-openssl/pass_util_test.cc @@ -0,0 +1,462 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include // for _putenv_s +#endif +#include "internal.h" +#include "test_util.h" + +// Use PEM_BUFSIZE (defined in openssl/pem.h) for password buffer size testing +// to match the implementation in pass_util.cc + +namespace { +// Helper functions to encapsulate common operations +void WriteTestFile(const char *path, const char *content, + bool preserve_newlines = false) { + ScopedFILE file(fopen(path, "wb")); + ASSERT_TRUE(file) << "Failed to open file: " << path; + if (content && strlen(content) > 0) { + if (preserve_newlines) { + // Write content exactly as provided, including newlines + ASSERT_GT(fprintf(file.get(), "%s", content), 0) + << "Failed to write to file: " << path; + } else { + // Write content without trailing newline + size_t bytes_written = fwrite(content, 1, strlen(content), file.get()); + ASSERT_GT(bytes_written, 0u) // Compare with unsigned 0 + << "Failed to write to file: " << path; + } + } + // If content is NULL or empty, we just create an empty file (no assertion + // needed) +} + +void SetTestEnvVar(const char *name, const char *value) { +#ifdef _WIN32 + _putenv_s(name, value); +#else + setenv(name, value, 1); +#endif +} + +void UnsetTestEnvVar(const char *name) { +#ifdef _WIN32 + _putenv_s(name, ""); +#else + unsetenv(name); +#endif +} +} // namespace + +// Base test fixture for pass_util tests +class PassUtilTest : public ::testing::Test { + protected: + void SetUp() override { + // Create temporary files for testing using utility from + // crypto/test/test_util.h + ASSERT_GT(createTempFILEpath(pass_path), 0u) + << "Failed to create first temp file path"; + ASSERT_GT(createTempFILEpath(pass_path2), 0u) + << "Failed to create second temp file path"; + + // Write test passwords using helper function + WriteTestFile(pass_path, "testpassword"); + WriteTestFile(pass_path2, "anotherpassword"); + + // Set up environment variable using helper function + SetTestEnvVar("TEST_PASSWORD_ENV", "envpassword"); + } + + void TearDown() override { + // Use RemoveFile from tool-openssl/test_util.h + RemoveFile(pass_path); + RemoveFile(pass_path2); + UnsetTestEnvVar("TEST_PASSWORD_ENV"); + } + + char pass_path[PATH_MAX] = {}; + char pass_path2[PATH_MAX] = {}; +}; + + +TEST_F(PassUtilTest, FileEdgeCases) { + // Test file truncation - exactly at buffer size - 1 without newline + { + std::string truncated_pass(PEM_BUFSIZE - 1, 'A'); + WriteTestFile(pass_path, truncated_pass.c_str()); + } + + bssl::UniquePtr source( + new std::string(std::string("file:") + pass_path)); + EXPECT_FALSE(pass_util::ExtractPassword(source)) + << "Should fail on truncated file"; + + // Test file exceeding maximum length + { + std::string long_pass(PEM_BUFSIZE + 10, 'B'); + WriteTestFile(pass_path, long_pass.c_str()); + } + + source.reset(new std::string(std::string("file:") + pass_path)); + EXPECT_FALSE(pass_util::ExtractPassword(source)) + << "Should fail on too long file content"; + + // Test empty file + { + WriteTestFile(pass_path, ""); + } + + source.reset(new std::string(std::string("file:") + pass_path)); + EXPECT_FALSE(pass_util::ExtractPassword(source)) + << "Should fail on empty file"; + + // Test file with only newlines - should succeed with empty password (like + // OpenSSL) + { + WriteTestFile(pass_path, "\n\n\n", true); // preserve_newlines = true + } + + source.reset(new std::string(std::string("file:") + pass_path)); + bool result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) << "Should succeed on newline-only file"; + EXPECT_TRUE(source->empty()) + << "Password should be empty from newline-only file"; + + // Test file at buffer size - 1 with newline (should not trigger truncation) + { + std::string non_truncated_pass(PEM_BUFSIZE - 2, 'C'); + std::string content = non_truncated_pass + "\n"; + WriteTestFile(pass_path, content.c_str(), + true); // preserve_newlines = true + } + + source.reset(new std::string(std::string("file:") + pass_path)); + result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) + << "Should succeed when file is at max length but has newline"; + EXPECT_EQ(source->length(), static_cast(PEM_BUFSIZE - 2)) + << "Password should not include newline and should be max length - 2"; + + // Test Windows carriage return behavior (CRLF) + { + WriteTestFile(pass_path, "windowspassword\r\n", true); + } + + source.reset(new std::string(std::string("file:") + pass_path)); + result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) << "Should succeed with Windows CRLF line ending"; + EXPECT_EQ(*source, "windowspassword") << "Should trim both \\r and \\n from Windows CRLF"; + + // Test old Mac carriage return behavior (CR only) + { + WriteTestFile(pass_path, "macpassword\r", true); + } + + source.reset(new std::string(std::string("file:") + pass_path)); + result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) << "Should succeed with old Mac CR line ending"; + EXPECT_EQ(*source, "macpassword") << "Should trim \\r from old Mac line ending"; + + // Test mixed trailing line endings + { + WriteTestFile(pass_path, "mixedpassword\r\n\r", true); + } + + source.reset(new std::string(std::string("file:") + pass_path)); + result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) << "Should succeed with mixed trailing line endings"; + EXPECT_EQ(*source, "mixedpassword") << "Should trim multiple trailing \\r and \\n characters"; + + // Test password with embedded carriage return (should be preserved) + { + WriteTestFile(pass_path, "pass\rwith\rembedded\r\n", true); + } + + source.reset(new std::string(std::string("file:") + pass_path)); + result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) << "Should succeed with embedded carriage returns"; + EXPECT_EQ(*source, "pass\rwith\rembedded") << "Embedded \\r should be preserved, only trailing trimmed"; + + // Test file with only CRLF + { + WriteTestFile(pass_path, "\r\n", true); + } + + source.reset(new std::string(std::string("file:") + pass_path)); + result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) << "Should succeed on CRLF-only file"; + EXPECT_TRUE(source->empty()) << "CRLF-only file should result in empty password"; + + // Test file with multiple CRLF lines + { + WriteTestFile(pass_path, "\r\n\r\n\r\n", true); + } + + source.reset(new std::string(std::string("file:") + pass_path)); + result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) << "Should succeed on multiple CRLF-only lines"; + EXPECT_TRUE(source->empty()) << "Multiple CRLF-only lines should result in empty password"; +} + + +TEST_F(PassUtilTest, EnvVarEdgeCases) { + // Test empty environment variable + SetTestEnvVar("TEST_EMPTY_PASSWORD", ""); + + bssl::UniquePtr source( + new std::string("env:TEST_EMPTY_PASSWORD")); + bool result = pass_util::ExtractPassword(source); + EXPECT_FALSE(result) << "Should fail on empty environment variable"; + + // Test maximum length environment variable + std::string long_pass(PEM_BUFSIZE + 10, 'B'); + SetTestEnvVar("TEST_LONG_PASSWORD", long_pass.c_str()); + + source.reset(new std::string("env:TEST_LONG_PASSWORD")); + EXPECT_FALSE(pass_util::ExtractPassword(source)) + << "Should fail on too long environment variable"; + + // Test non-existent environment variable + source.reset(new std::string("env:NON_EXISTENT_VAR_NAME_12345")); + EXPECT_FALSE(pass_util::ExtractPassword(source)) + << "Should fail on non-existent environment variable"; + + UnsetTestEnvVar("TEST_EMPTY_PASSWORD"); + UnsetTestEnvVar("TEST_LONG_PASSWORD"); +} + +TEST_F(PassUtilTest, DirectPasswordEdgeCases) { + // Test maximum length direct password + std::string long_pass = "pass:" + std::string(PEM_BUFSIZE + 10, 'C'); + bssl::UniquePtr source(new std::string(long_pass)); + EXPECT_FALSE(pass_util::ExtractPassword(source)) + << "Should fail on too long direct password"; + + // Test empty direct password + source.reset(new std::string("pass:")); + bool result = pass_util::ExtractPassword(source); + EXPECT_TRUE(result) << "Should succeed with empty direct password"; + EXPECT_TRUE(source->empty()) << "Password should be empty"; + + // Test invalid format strings + const char *invalid_formats[] = { + "pass", // Missing colon + "pass:test:123", // Multiple colons + ":password", // Missing prefix + "invalid:pass", // Invalid prefix + "file:", // Empty file path + "env:", // Empty environment variable + "", // Empty string + }; + + for (const char *fmt : invalid_formats) { + source.reset(new std::string(fmt)); + EXPECT_FALSE(pass_util::ExtractPassword(source)) + << "Should fail on invalid format: " << fmt; + } +} + +TEST_F(PassUtilTest, SensitiveStringDeleter) { + const char *test_password = "sensitive_data_to_be_cleared"; + + // Test the actual usage pattern with smart pointer + { + bssl::UniquePtr source(new std::string(test_password)); + + // Verify password is initially there + ASSERT_EQ(*source, test_password); + + // Let smart pointer go out of scope here - deleter should be called + } + + // Test that OPENSSL_cleanse works (verifies deleter functionality) + std::string test_str(test_password); + const char *buffer = test_str.data(); + size_t len = test_str.length(); + + ASSERT_EQ(memcmp(buffer, test_password, len), 0); + OPENSSL_cleanse(const_cast(buffer), len); + EXPECT_NE(memcmp(buffer, test_password, len), 0) + << "OPENSSL_cleanse should clear memory"; +} + +TEST_F(PassUtilTest, ExtractPasswordsDifferentFiles) { + bssl::UniquePtr passin( + new std::string(std::string("file:") + pass_path)); + bssl::UniquePtr passout( + new std::string(std::string("file:") + pass_path2)); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "testpassword"); + EXPECT_EQ(*passout, "anotherpassword"); +} + +TEST_F(PassUtilTest, ExtractPasswordsSameFile) { + // Create file with two lines + WriteTestFile(pass_path, "firstpassword\nsecondpassword\n", true); + + bssl::UniquePtr passin( + new std::string(std::string("file:") + pass_path)); + bssl::UniquePtr passout( + new std::string(std::string("file:") + pass_path)); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "firstpassword"); + EXPECT_EQ(*passout, "secondpassword"); + + // Test same-file functionality with Windows CRLF + WriteTestFile(pass_path, "firstpass\r\nsecondpass\r\n", true); + + passin.reset(new std::string(std::string("file:") + pass_path)); + passout.reset(new std::string(std::string("file:") + pass_path)); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "firstpass") << "First line should have CRLF trimmed"; + EXPECT_EQ(*passout, "secondpass") << "Second line should have CRLF trimmed"; + + // Test mixed line endings in same-file scenario + WriteTestFile(pass_path, "unixpass\nsecondpass\r\n", true); + + passin.reset(new std::string(std::string("file:") + pass_path)); + passout.reset(new std::string(std::string("file:") + pass_path)); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "unixpass") << "Unix LF should be trimmed"; + EXPECT_EQ(*passout, "secondpass") << "Windows CRLF should be trimmed"; +} + +TEST_F(PassUtilTest, ExtractPasswordsMixedSources) { + // Test file + environment variable + bssl::UniquePtr passin( + new std::string(std::string("file:") + pass_path)); + bssl::UniquePtr passout( + new std::string("env:TEST_PASSWORD_ENV")); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "testpassword"); + EXPECT_EQ(*passout, "envpassword"); + + // Test direct password + file + passin.reset(new std::string("pass:directpass")); + passout.reset(new std::string(std::string("file:") + pass_path2)); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "directpass"); + EXPECT_EQ(*passout, "anotherpassword"); +} + +TEST_F(PassUtilTest, ExtractPasswordsEmptyPasswords) { + // Both empty + bssl::UniquePtr passin(new std::string("")); + bssl::UniquePtr passout(new std::string("")); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_TRUE(passin->empty()); + EXPECT_TRUE(passout->empty()); + + // One empty, one with password + passin.reset(new std::string("")); + passout.reset(new std::string("pass:onlypassout")); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_TRUE(passin->empty()); + EXPECT_EQ(*passout, "onlypassout"); + + // Reverse: one with password, one empty + passin.reset(new std::string("pass:onlypassin")); + passout.reset(new std::string("")); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "onlypassin"); + EXPECT_TRUE(passout->empty()); +} + +TEST_F(PassUtilTest, ExtractPasswordsErrorCases) { + // Invalid passin format + bssl::UniquePtr passin(new std::string("invalid:format")); + bssl::UniquePtr passout(new std::string("pass:validpass")); + + EXPECT_FALSE(pass_util::ExtractPasswords(passin, passout)); + + // Invalid passout format + passin.reset(new std::string("pass:validpass")); + passout.reset(new std::string("invalid:format")); + + EXPECT_FALSE(pass_util::ExtractPasswords(passin, passout)); + + // Both invalid formats + passin.reset(new std::string("invalid1:format")); + passout.reset(new std::string("invalid2:format")); + + EXPECT_FALSE(pass_util::ExtractPasswords(passin, passout)); + + // Null UniquePtr objects + bssl::UniquePtr null_passin; + bssl::UniquePtr null_passout; + + EXPECT_FALSE(pass_util::ExtractPasswords(null_passin, null_passout)); + + // One null, one valid + passin.reset(new std::string("pass:valid")); + EXPECT_FALSE(pass_util::ExtractPasswords(passin, null_passout)); +} + +TEST_F(PassUtilTest, ExtractPasswordsFileErrors) { + // Non-existent file for passin + bssl::UniquePtr passin( + new std::string("file:/non/existent/file1")); + bssl::UniquePtr passout(new std::string("pass:validpass")); + + EXPECT_FALSE(pass_util::ExtractPasswords(passin, passout)); + + // Non-existent file for passout + passin.reset(new std::string("pass:validpass")); + passout.reset(new std::string("file:/non/existent/file2")); + + EXPECT_FALSE(pass_util::ExtractPasswords(passin, passout)); + + // Same non-existent file for both + passin.reset(new std::string("file:/non/existent/samefile")); + passout.reset(new std::string("file:/non/existent/samefile")); + + EXPECT_FALSE(pass_util::ExtractPasswords(passin, passout)); +} + +TEST_F(PassUtilTest, ExtractPasswordsSameFileEdgeCases) { + // File with only one line (passout should fail) + WriteTestFile(pass_path, "onlyoneline", false); + + bssl::UniquePtr passin( + new std::string(std::string("file:") + pass_path)); + bssl::UniquePtr passout( + new std::string(std::string("file:") + pass_path)); + + EXPECT_FALSE(pass_util::ExtractPasswords(passin, passout)); + + // File with empty second line + WriteTestFile(pass_path, "firstline\n\n", true); + + passin.reset(new std::string(std::string("file:") + pass_path)); + passout.reset(new std::string(std::string("file:") + pass_path)); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "firstline"); + EXPECT_TRUE(passout->empty()); + + // File with multiple lines (should only read first two) + WriteTestFile(pass_path, "line1\nline2\nline3\nline4\n", true); + + passin.reset(new std::string(std::string("file:") + pass_path)); + passout.reset(new std::string(std::string("file:") + pass_path)); + + EXPECT_TRUE(pass_util::ExtractPasswords(passin, passout)); + EXPECT_EQ(*passin, "line1"); + EXPECT_EQ(*passout, "line2"); +} diff --git a/tool-openssl/pkcs8.cc b/tool-openssl/pkcs8.cc index 5b61c6b99f..bd6149febd 100644 --- a/tool-openssl/pkcs8.cc +++ b/tool-openssl/pkcs8.cc @@ -2,80 +2,73 @@ // SPDX-License-Identifier: Apache-2.0 OR ISC #include +#include +#include #include -#include #include -#include -#include +#include +#include #include #include -#include #include "internal.h" -// Maximum size for crypto files to prevent loading excessively large files (1MB) +// Maximum size for crypto files to prevent loading excessively large files +// (1MB) static constexpr long DEFAULT_MAX_CRYPTO_FILE_SIZE = 1024 * 1024; -// Maximum length limit for sensitive strings like passwords (4KB) -static constexpr size_t DEFAULT_MAX_SENSITIVE_STRING_LENGTH = 4096; - // Checks if BIO size is within allowed limits -static bool validate_bio_size(BIO* bio, long max_size = DEFAULT_MAX_CRYPTO_FILE_SIZE) { - if (!bio) { - return false; - } - const long current_pos = BIO_tell(bio); - if (current_pos < 0) { - return false; - } - if (BIO_seek(bio, 0) < 0) { - return false; - } - long size = 0; - char buffer[4096] = {}; - int bytes_read = 0; - while ((bytes_read = BIO_read(bio, buffer, sizeof(buffer))) > 0) { - size += bytes_read; - if (size > max_size) { - BIO_seek(bio, current_pos); - fprintf(stderr, "File exceeds maximum allowed size\n"); - return false; - } - } - if (BIO_seek(bio, current_pos) < 0) { - return false; - } - return true; +static bool validate_bio_size(BIO &bio, + long max_size = DEFAULT_MAX_CRYPTO_FILE_SIZE) { + const long current_pos = BIO_tell(&bio); + if (current_pos < 0) { + return false; + } + if (BIO_seek(&bio, 0) < 0) { + return false; + } + long size = 0; + char buffer[4096] = {}; + int bytes_read = 0; + while ((bytes_read = BIO_read(&bio, buffer, sizeof(buffer))) > 0) { + size += bytes_read; + if (size > max_size) { + BIO_seek(&bio, current_pos); + fprintf(stderr, "File exceeds maximum allowed size\n"); + return false; + } + } + if (BIO_seek(&bio, current_pos) < 0) { + return false; + } + return true; } // Validates input/output format is PEM or DER -static bool validate_format(const std::string& format) { - if (format != "PEM" && format != "DER") { - fprintf(stderr, "Format must be PEM or DER\n"); - return false; - } - return true; +static bool validate_format(const std::string &format) { + if (format != "PEM" && format != "DER") { + fprintf(stderr, "Format must be PEM or DER\n"); + return false; + } + return true; } // Supported cipher algorithms for key encryption static const std::unordered_set kSupportedCiphers = { - "aes-128-cbc", "aes-192-cbc", "aes-256-cbc", - "des-ede3-cbc", - "des-cbc" -}; + "aes-128-cbc", "aes-192-cbc", "aes-256-cbc", "des-ede3-cbc", "des-cbc"}; // Checks if the cipher name is supported and can be initialized -static bool validate_cipher(const std::string& cipher_name) { - if (kSupportedCiphers.find(cipher_name) == kSupportedCiphers.end()) { - fprintf(stderr, "Unsupported cipher algorithm\n"); - return false; - } - const EVP_CIPHER* cipher = EVP_get_cipherbyname(cipher_name.c_str()); - if (!cipher) { - fprintf(stderr, "Cannot initialize cipher\n"); - return false; - } - - return true; +static bool validate_cipher(const std::string &cipher_name) { + if (kSupportedCiphers.find(cipher_name) == kSupportedCiphers.end()) { + fprintf(stderr, "Unsupported cipher algorithm\n"); + return false; + } + const EVP_CIPHER *cipher = EVP_get_cipherbyname(cipher_name.c_str()); + if (!cipher) { + fprintf(stderr, "Cannot initialize cipher\n"); + return false; + } + + return true; } // Supported PRF algorithms for PKCS#5 v2.0 @@ -84,141 +77,41 @@ static const std::unordered_set kSupportedPRFs = { }; // Checks if the PRF algorithm name is supported -static bool validate_prf(const std::string& prf_name) { - if (kSupportedPRFs.find(prf_name) == kSupportedPRFs.end()) { - fprintf(stderr, "Only hmacWithSHA1 PRF is supported\n"); - return false; - } - return true; -} - -// Extracts password from various sources (direct input, file, environment) -// Updates source string to contain the actual password -static bool extract_password(std::string& source) { - // TODO Prompt user for password via EVP_read_pw_string - if (source.empty()) { - return true; - } - - if (source.compare(0, 5, "pass:") == 0) { - std::string password = source.substr(5); - if (password.length() > DEFAULT_MAX_SENSITIVE_STRING_LENGTH) { - fprintf(stderr, "Password exceeds maximum allowed length\n"); - return false; - } - source = password; - return true; - } - - if (source.compare(0, 5, "file:") == 0) { - std::string path = source.substr(5); - bssl::UniquePtr bio(BIO_new_file(path.c_str(), "r")); - if (!bio) { - fprintf(stderr, "Cannot open password file\n"); - return false; - } - char buf[DEFAULT_MAX_SENSITIVE_STRING_LENGTH] = {}; - int len = BIO_gets(bio.get(), buf, sizeof(buf)); - if (len <= 0) { - OPENSSL_cleanse(buf, sizeof(buf)); - fprintf(stderr, "Cannot read password file\n"); - return false; - } - const bool possible_truncation = (static_cast(len) == DEFAULT_MAX_SENSITIVE_STRING_LENGTH - 1 && - buf[len - 1] != '\n' && buf[len - 1] != '\r'); - if (possible_truncation) { - OPENSSL_cleanse(buf, sizeof(buf)); - fprintf(stderr, "Password file content too long\n"); - return false; - } - size_t buf_len = len; - while (buf_len > 0 && (buf[buf_len-1] == '\n' || buf[buf_len-1] == '\r')) { - buf[--buf_len] = '\0'; - } - source = buf; - OPENSSL_cleanse(buf, sizeof(buf)); - return true; - } - - if (source.compare(0, 4, "env:") == 0) { - std::string env_var = source.substr(4); - if (env_var.empty()) { - fprintf(stderr, "Empty environment variable name\n"); - return false; - } - const char* env_val = getenv(env_var.c_str()); - if (!env_val) { - fprintf(stderr, "Environment variable '%s' not set\n", env_var.c_str()); - return false; - } - size_t env_val_len = strlen(env_val); - if (env_val_len == 0) { - fprintf(stderr, "Environment variable '%s' is empty\n", env_var.c_str()); - return false; - } - if (env_val_len > DEFAULT_MAX_SENSITIVE_STRING_LENGTH) { - fprintf(stderr, "Environment variable value too long\n"); - return false; - } - source = std::string(env_val); - return true; - } - fprintf(stderr, "Invalid password format (use pass:, file:, or env:)\n"); +static bool validate_prf(const std::string &prf_name) { + if (kSupportedPRFs.find(prf_name) == kSupportedPRFs.end()) { + fprintf(stderr, "Only hmacWithSHA1 PRF is supported\n"); return false; + } + return true; } -// Custom deleter for sensitive strings that clears memory before deletion -static void SensitiveStringDeleter(std::string* str) { - if (str && !str->empty()) { - OPENSSL_cleanse(&(*str)[0], str->size()); - str->clear(); - } - delete str; -} - -BSSL_NAMESPACE_BEGIN -BORINGSSL_MAKE_DELETER(std::string, SensitiveStringDeleter) -BSSL_NAMESPACE_END +// Reads a private key from BIO in DER format with optional password +static bssl::UniquePtr read_private_der(BIO *in_bio, const char *passin) { + if (!in_bio) { + return nullptr; + } -// Reads a private key from BIO in the specified format with optional password -static bssl::UniquePtr read_private_key(BIO* in_bio, const char* passin, - const std::string& format) { - if (!in_bio) { - return nullptr; - } - - bssl::UniquePtr pkey; - - if (passin) { - if (format == "DER") { - BIO_reset(in_bio); - pkey.reset(d2i_PKCS8PrivateKey_bio(in_bio, nullptr, nullptr, const_cast(passin))); - return pkey; - } else { - BIO_reset(in_bio); - pkey.reset(PEM_read_bio_PrivateKey(in_bio, nullptr, nullptr, const_cast(passin))); - return pkey; - } - } - - // If no password provided, try unencrypted paths - if (format == "DER") { - BIO_reset(in_bio); - bssl::UniquePtr p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(in_bio, nullptr)); - if (p8inf) { - pkey.reset(EVP_PKCS82PKEY(p8inf.get())); - if (pkey) { - return pkey; - } - } - BIO_reset(in_bio); - pkey.reset(d2i_PrivateKey_bio(in_bio, nullptr)); - return pkey; - } else { - BIO_reset(in_bio); - pkey.reset(PEM_read_bio_PrivateKey(in_bio, nullptr, nullptr, nullptr)); - return pkey; - } + BIO_reset(in_bio); + + if (passin) { + // Try encrypted PKCS8 DER + return bssl::UniquePtr( + d2i_PKCS8PrivateKey_bio(in_bio, nullptr, nullptr, const_cast(passin))); + } + + // Try unencrypted DER formats in order of preference + // 1. Try unencrypted PKCS8 DER + bssl::UniquePtr p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(in_bio, nullptr)); + if (p8inf) { + bssl::UniquePtr pkey(EVP_PKCS82PKEY(p8inf.get())); + if (pkey) { + return pkey; + } + } + + // 2. Fall back to traditional DER (auto-detect RSA/DSA/EC) + BIO_reset(in_bio); + return bssl::UniquePtr(d2i_PrivateKey_bio(in_bio, nullptr)); } static const argument_t kArguments[] = { @@ -230,11 +123,11 @@ static const argument_t kArguments[] = { {"-topk8", kBooleanArgument, "Convert traditional format to PKCS#8"}, {"-nocrypt", kBooleanArgument, "Use unencrypted private key"}, {"-v2", kOptionalArgument, "Use PKCS#5 v2.0 and specified cipher"}, - {"-v2prf", kOptionalArgument, "Use specified PRF algorithm with PKCS#5 v2.0"}, + {"-v2prf", kOptionalArgument, + "Use specified PRF algorithm with PKCS#5 v2.0"}, {"-passin", kOptionalArgument, "Input file passphrase source"}, {"-passout", kOptionalArgument, "Output file passphrase source"}, - {"", kOptionalArgument, ""} -}; + {"", kOptionalArgument, ""}}; bool pkcs8Tool(const args_list_t& args) { using namespace ordered_args; @@ -254,6 +147,7 @@ bool pkcs8Tool(const args_list_t& args) { bssl::UniquePtr pkey; const EVP_CIPHER* cipher = nullptr; bssl::UniquePtr p8inf; + if (!ParseOrderedKeyValueArguments(parsed_args, extra_args, args, kArguments)) { PrintUsage(kArguments); @@ -293,31 +187,32 @@ bool pkcs8Tool(const args_list_t& args) { if (!v2_prf.empty() && !validate_prf(v2_prf)) { return false; } - + GetString(passin_arg.get(), "-passin", "", parsed_args); GetString(passout_arg.get(), "-passout", "", parsed_args); - if (!extract_password(*passin_arg)) { - return false; - } - if (!extract_password(*passout_arg)) { + + // Extract passwords (handles same-file case where both passwords are in one file) + if (!pass_util::ExtractPasswords(passin_arg, passout_arg)) { + fprintf(stderr, "Error extracting passwords\n"); return false; } - + // Check for contradictory arguments if (nocrypt && !passin_arg->empty() && !passout_arg->empty()) { - fprintf(stderr, "Error: -nocrypt cannot be used with both -passin and -passout\n"); + fprintf(stderr, + "Error: -nocrypt cannot be used with both -passin and -passout\n"); return false; } - + in.reset(BIO_new_file(in_path.c_str(), "rb")); if (!in) { fprintf(stderr, "Cannot open input file\n"); return false; } - if (!validate_bio_size(in.get())) { + if (!validate_bio_size(*in)) { return false; } - + if (!out_path.empty()) { out.reset(BIO_new_file(out_path.c_str(), "wb")); } else { @@ -327,12 +222,11 @@ bool pkcs8Tool(const args_list_t& args) { fprintf(stderr, "Cannot open output file\n"); return false; } - - pkey = read_private_key( - in.get(), - passin_arg->empty() ? nullptr : passin_arg->c_str(), - inform - ); + + pkey.reset((inform == "PEM") + ? PEM_read_bio_PrivateKey(in.get(), nullptr, nullptr, + passin_arg->empty() ? nullptr : const_cast(passin_arg->c_str())) + : read_private_der(in.get(), passin_arg->empty() ? nullptr : passin_arg->c_str()).release()); if (!pkey) { fprintf(stderr, "Unable to load private key\n"); return false; @@ -340,40 +234,27 @@ bool pkcs8Tool(const args_list_t& args) { bool result = false; if (!topk8) { - result = (outform == "PEM") ? - PEM_write_bio_PrivateKey(out.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr) : - i2d_PrivateKey_bio(out.get(), pkey.get()); + // Default behavior: output unencrypted PKCS#8 format + // (AWS-LC doesn't support -traditional option) + result = (outform == "PEM") + ? PEM_write_bio_PKCS8PrivateKey(out.get(), pkey.get(), nullptr, + nullptr, 0, nullptr, nullptr) + : i2d_PKCS8PrivateKey_bio(out.get(), pkey.get(), nullptr, + nullptr, 0, nullptr, nullptr); } else { - // If passout is provided, always encrypt the output regardless of nocrypt - if (nocrypt && passout_arg->empty()) { - p8inf.reset(EVP_PKEY2PKCS8(pkey.get())); - if (!p8inf) { - fprintf(stderr, "Error converting to PKCS#8\n"); - return false; - } - - result = (outform == "PEM") ? - PEM_write_bio_PKCS8_PRIV_KEY_INFO(out.get(), p8inf.get()) : - i2d_PKCS8_PRIV_KEY_INFO_bio(out.get(), p8inf.get()); - } else { - if (passout_arg->empty()) { - fprintf(stderr, "Password required for encryption\n"); - return false; - } - cipher = v2_cipher.empty() ? - EVP_aes_256_cbc() : EVP_get_cipherbyname(v2_cipher.c_str()); - if (outform == "PEM") { - result = PEM_write_bio_PKCS8PrivateKey( - out.get(), pkey.get(), cipher, - passout_arg->c_str(), passout_arg->length(), - nullptr, nullptr); - } else { - result = i2d_PKCS8PrivateKey_bio( - out.get(), pkey.get(), cipher, - passout_arg->c_str(), passout_arg->length(), - nullptr, nullptr); - } - } + // -topk8: output PKCS#8 format (encrypted by default unless -nocrypt) + cipher = (nocrypt) ? nullptr : + (v2_cipher.empty() ? EVP_aes_256_cbc() : EVP_get_cipherbyname(v2_cipher.c_str())); + + result = (outform == "PEM") + ? PEM_write_bio_PKCS8PrivateKey(out.get(), pkey.get(), cipher, + passout_arg->empty() ? nullptr : passout_arg->c_str(), + passout_arg->empty() ? 0 : passout_arg->length(), + nullptr, nullptr) + : i2d_PKCS8PrivateKey_bio(out.get(), pkey.get(), cipher, + passout_arg->empty() ? nullptr : passout_arg->c_str(), + passout_arg->empty() ? 0 : passout_arg->length(), + nullptr, nullptr); } if (!result) {