generated from amazon-archives/__template_Apache-2.0
-
Notifications
You must be signed in to change notification settings - Fork 143
Add minimal EC CLI tool implementation #2640
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
fdbfe68
Add minimal EC CLI tool implementation
kingstjo fa072cc
Add comprehensive EC CLI tool tests
kingstjo 10cfb95
Improve EC tool: enum for type safety, format validation, specific er…
kingstjo c14994a
Add content validation to EC round-trip tests
kingstjo cd66ebf
Improve format validation in EC tool
kingstjo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 OR ISC | ||
|
||
#include <openssl/bio.h> | ||
#include <openssl/ec.h> | ||
#include <openssl/err.h> | ||
#include <openssl/pem.h> | ||
#include <string> | ||
#include "internal.h" | ||
|
||
enum Format { | ||
FORMAT_PEM = 1, | ||
FORMAT_DER = 2 | ||
}; | ||
|
||
static const argument_t kArguments[] = { | ||
{"-help", kBooleanArgument, "Display this summary"}, | ||
{"-inform", kOptionalArgument, "Input format (PEM or DER), default PEM"}, | ||
{"-in", kOptionalArgument, "Input file, default stdin"}, | ||
{"-pubout", kBooleanArgument, "Output public key, not private"}, | ||
{"-out", kOptionalArgument, "Output file, default stdout"}, | ||
{"-outform", kOptionalArgument, "Output format (PEM or DER), default PEM"}, | ||
{"", kOptionalArgument, ""}}; | ||
|
||
bool ecTool(const args_list_t &args) { | ||
ordered_args::ordered_args_map_t parsed_args; | ||
args_list_t extra_args; | ||
std::string in_path, out_path, inform_str, outform_str; | ||
bool help = false, pubout = false; | ||
int input_format = FORMAT_PEM, output_format = FORMAT_PEM; | ||
bssl::UniquePtr<BIO> input_bio, output_bio; | ||
bssl::UniquePtr<EC_KEY> ec_key; | ||
|
||
if (!ordered_args::ParseOrderedKeyValueArguments(parsed_args, extra_args, | ||
args, kArguments)) { | ||
PrintUsage(kArguments); | ||
goto err; | ||
} | ||
|
||
ordered_args::GetBoolArgument(&help, "-help", parsed_args); | ||
ordered_args::GetString(&in_path, "-in", "", parsed_args); | ||
ordered_args::GetString(&out_path, "-out", "", parsed_args); | ||
ordered_args::GetString(&inform_str, "-inform", "PEM", parsed_args); | ||
ordered_args::GetString(&outform_str, "-outform", "PEM", parsed_args); | ||
ordered_args::GetBoolArgument(&pubout, "-pubout", parsed_args); | ||
|
||
if (help) { | ||
PrintUsage(kArguments); | ||
return true; | ||
} | ||
|
||
if (isStringUpperCaseEqual(inform_str, "DER")) { | ||
input_format = FORMAT_DER; | ||
} else if (isStringUpperCaseEqual(inform_str, "PEM")) { | ||
input_format = FORMAT_PEM; | ||
} else { | ||
fprintf(stderr, "Error: Invalid input format '%s'. Must be PEM or DER\n", inform_str.c_str()); | ||
goto err; | ||
} | ||
|
||
if (isStringUpperCaseEqual(outform_str, "DER")) { | ||
output_format = FORMAT_DER; | ||
} else if (isStringUpperCaseEqual(outform_str, "PEM")) { | ||
output_format = FORMAT_PEM; | ||
} else { | ||
fprintf(stderr, "Error: Invalid output format '%s'. Must be PEM or DER\n", outform_str.c_str()); | ||
goto err; | ||
} | ||
|
||
input_bio.reset(in_path.empty() ? BIO_new_fp(stdin, BIO_NOCLOSE) | ||
: BIO_new_file(in_path.c_str(), "rb")); | ||
if (!input_bio) { | ||
fprintf(stderr, "Error: Could not open input\n"); | ||
goto err; | ||
} | ||
|
||
ec_key.reset(input_format == FORMAT_DER | ||
? d2i_ECPrivateKey_bio(input_bio.get(), nullptr) | ||
: PEM_read_bio_ECPrivateKey(input_bio.get(), nullptr, | ||
nullptr, nullptr)); | ||
if (!ec_key) { | ||
fprintf(stderr, "Error: Could not read EC key in %s format\n", | ||
input_format == FORMAT_DER ? "DER" : "PEM"); | ||
goto err; | ||
} | ||
|
||
output_bio.reset(out_path.empty() ? BIO_new_fp(stdout, BIO_NOCLOSE) | ||
: BIO_new_file(out_path.c_str(), "wb")); | ||
if (!output_bio) { | ||
fprintf(stderr, "Error: Could not open output\n"); | ||
goto err; | ||
} | ||
|
||
if (pubout) { | ||
if (!(output_format == FORMAT_DER | ||
? i2d_EC_PUBKEY_bio(output_bio.get(), ec_key.get()) | ||
: PEM_write_bio_EC_PUBKEY(output_bio.get(), ec_key.get()))) { | ||
goto err; | ||
} | ||
kingstjo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
if (!(output_format == FORMAT_DER | ||
? i2d_ECPrivateKey_bio(output_bio.get(), ec_key.get()) | ||
: PEM_write_bio_ECPrivateKey(output_bio.get(), ec_key.get(), | ||
nullptr, nullptr, 0, nullptr, | ||
nullptr))) { | ||
goto err; | ||
} | ||
} | ||
|
||
return true; | ||
|
||
err: | ||
ERR_print_errors_fp(stderr); | ||
return false; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 OR ISC | ||
|
||
#include <gtest/gtest.h> | ||
#include <openssl/pem.h> | ||
#include <openssl/ec.h> | ||
#include "internal.h" | ||
#include "test_util.h" | ||
#include "../crypto/test/test_util.h" | ||
|
||
static EC_KEY* CreateTestECKey() { | ||
bssl::UniquePtr<EC_KEY> ec_key(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); | ||
if (!ec_key || !EC_KEY_generate_key(ec_key.get())) { | ||
return nullptr; | ||
} | ||
return ec_key.release(); | ||
} | ||
|
||
class ECTest : public ::testing::Test { | ||
protected: | ||
void SetUp() override { | ||
ASSERT_GT(createTempFILEpath(pem_key_path), 0u); | ||
ASSERT_GT(createTempFILEpath(der_key_path), 0u); | ||
ASSERT_GT(createTempFILEpath(out_path), 0u); | ||
|
||
tool_executable_path = getenv("AWSLC_TOOL_PATH"); | ||
openssl_executable_path = getenv("OPENSSL_TOOL_PATH"); | ||
|
||
if (tool_executable_path != nullptr && openssl_executable_path != nullptr) { | ||
ASSERT_GT(createTempFILEpath(out_path_openssl), 0u); | ||
|
||
// Use OpenSSL to generate test keys for better cross-compatibility | ||
std::string pem_cmd = std::string(openssl_executable_path) + " ecparam -genkey -name prime256v1 -out " + pem_key_path; | ||
std::string der_cmd = std::string(openssl_executable_path) + " ecparam -genkey -name prime256v1 | " + | ||
std::string(openssl_executable_path) + " ec -outform DER -out " + der_key_path; | ||
|
||
ASSERT_EQ(system(pem_cmd.c_str()), 0) << "Failed to generate PEM key with OpenSSL"; | ||
ASSERT_EQ(system(der_cmd.c_str()), 0) << "Failed to generate DER key with OpenSSL"; | ||
} else { | ||
// Fallback to AWS-LC key generation | ||
ec_key.reset(CreateTestECKey()); | ||
ASSERT_TRUE(ec_key); | ||
|
||
bssl::UniquePtr<BIO> pem_bio(BIO_new_file(pem_key_path, "wb")); | ||
ASSERT_TRUE(pem_bio); | ||
ASSERT_TRUE(PEM_write_bio_ECPrivateKey(pem_bio.get(), ec_key.get(), nullptr, nullptr, 0, nullptr, nullptr)); | ||
BIO_flush(pem_bio.get()); | ||
|
||
bssl::UniquePtr<BIO> der_bio(BIO_new_file(der_key_path, "wb")); | ||
ASSERT_TRUE(der_bio); | ||
ASSERT_TRUE(i2d_ECPrivateKey_bio(der_bio.get(), ec_key.get())); | ||
BIO_flush(der_bio.get()); | ||
} | ||
} | ||
|
||
void TearDown() override { | ||
RemoveFile(pem_key_path); | ||
RemoveFile(der_key_path); | ||
RemoveFile(out_path); | ||
if (tool_executable_path != nullptr && openssl_executable_path != nullptr) { | ||
RemoveFile(out_path_openssl); | ||
} | ||
} | ||
|
||
char pem_key_path[PATH_MAX]; | ||
char der_key_path[PATH_MAX]; | ||
char out_path[PATH_MAX]; | ||
char out_path_openssl[PATH_MAX]; | ||
const char* tool_executable_path; | ||
const char* openssl_executable_path; | ||
bssl::UniquePtr<EC_KEY> ec_key; | ||
}; | ||
|
||
TEST_F(ECTest, ReadPEMOutputPEM) { | ||
args_list_t args = {"-in", pem_key_path, "-out", out_path}; | ||
ASSERT_TRUE(ecTool(args)); | ||
|
||
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb")); | ||
ASSERT_TRUE(out_bio); | ||
bssl::UniquePtr<EC_KEY> parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr)); | ||
ASSERT_TRUE(parsed_key); | ||
} | ||
|
||
TEST_F(ECTest, ReadPEMOutputDER) { | ||
args_list_t args = {"-in", pem_key_path, "-outform", "DER", "-out", out_path}; | ||
ASSERT_TRUE(ecTool(args)); | ||
|
||
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb")); | ||
ASSERT_TRUE(out_bio); | ||
bssl::UniquePtr<EC_KEY> parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr)); | ||
ASSERT_TRUE(parsed_key); | ||
} | ||
|
||
TEST_F(ECTest, ReadDEROutputPEM) { | ||
args_list_t args = {"-in", der_key_path, "-inform", "DER", "-out", out_path}; | ||
ASSERT_TRUE(ecTool(args)); | ||
|
||
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb")); | ||
ASSERT_TRUE(out_bio); | ||
bssl::UniquePtr<EC_KEY> parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr)); | ||
ASSERT_TRUE(parsed_key); | ||
} | ||
|
||
TEST_F(ECTest, ReadDEROutputDER) { | ||
args_list_t args = {"-in", der_key_path, "-inform", "DER", "-outform", "DER", "-out", out_path}; | ||
ASSERT_TRUE(ecTool(args)); | ||
|
||
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb")); | ||
ASSERT_TRUE(out_bio); | ||
bssl::UniquePtr<EC_KEY> parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr)); | ||
ASSERT_TRUE(parsed_key); | ||
} | ||
|
||
TEST_F(ECTest, PublicKeyExtractionPEM) { | ||
args_list_t args = {"-in", pem_key_path, "-pubout", "-out", out_path}; | ||
ASSERT_TRUE(ecTool(args)); | ||
|
||
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb")); | ||
ASSERT_TRUE(out_bio); | ||
bssl::UniquePtr<EC_KEY> parsed_key(PEM_read_bio_EC_PUBKEY(out_bio.get(), nullptr, nullptr, nullptr)); | ||
ASSERT_TRUE(parsed_key); | ||
} | ||
|
||
TEST_F(ECTest, PublicKeyExtractionDER) { | ||
args_list_t args = {"-in", der_key_path, "-inform", "DER", "-pubout", "-outform", "DER", "-out", out_path}; | ||
ASSERT_TRUE(ecTool(args)); | ||
|
||
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb")); | ||
ASSERT_TRUE(out_bio); | ||
bssl::UniquePtr<EC_KEY> parsed_key(d2i_EC_PUBKEY_bio(out_bio.get(), nullptr)); | ||
ASSERT_TRUE(parsed_key); | ||
} | ||
|
||
TEST_F(ECTest, RoundTripPEMtoDERtoPEM) { | ||
char temp_der[PATH_MAX]; | ||
ASSERT_GT(createTempFILEpath(temp_der), 0u); | ||
|
||
// Load original key for comparison | ||
bssl::UniquePtr<BIO> orig_bio(BIO_new_file(pem_key_path, "rb")); | ||
ASSERT_TRUE(orig_bio); | ||
bssl::UniquePtr<EC_KEY> orig_key(PEM_read_bio_ECPrivateKey(orig_bio.get(), nullptr, nullptr, nullptr)); | ||
ASSERT_TRUE(orig_key); | ||
|
||
args_list_t args1 = {"-in", pem_key_path, "-outform", "DER", "-out", temp_der}; | ||
ASSERT_TRUE(ecTool(args1)); | ||
|
||
args_list_t args2 = {"-in", temp_der, "-inform", "DER", "-out", out_path}; | ||
ASSERT_TRUE(ecTool(args2)); | ||
|
||
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb")); | ||
ASSERT_TRUE(out_bio); | ||
bssl::UniquePtr<EC_KEY> parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr)); | ||
ASSERT_TRUE(parsed_key); | ||
kingstjo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Validate key content matches | ||
const BIGNUM *orig_priv = EC_KEY_get0_private_key(orig_key.get()); | ||
const BIGNUM *parsed_priv = EC_KEY_get0_private_key(parsed_key.get()); | ||
ASSERT_EQ(BN_cmp(orig_priv, parsed_priv), 0); | ||
|
||
RemoveFile(temp_der); | ||
} | ||
|
||
TEST_F(ECTest, RoundTripDERtoPEMtoDER) { | ||
char temp_pem[PATH_MAX]; | ||
ASSERT_GT(createTempFILEpath(temp_pem), 0u); | ||
|
||
// Load original key for comparison | ||
bssl::UniquePtr<BIO> orig_bio(BIO_new_file(der_key_path, "rb")); | ||
ASSERT_TRUE(orig_bio); | ||
bssl::UniquePtr<EC_KEY> orig_key(d2i_ECPrivateKey_bio(orig_bio.get(), nullptr)); | ||
ASSERT_TRUE(orig_key); | ||
|
||
args_list_t args1 = {"-in", der_key_path, "-inform", "DER", "-out", temp_pem}; | ||
ASSERT_TRUE(ecTool(args1)); | ||
|
||
args_list_t args2 = {"-in", temp_pem, "-outform", "DER", "-out", out_path}; | ||
ASSERT_TRUE(ecTool(args2)); | ||
|
||
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb")); | ||
ASSERT_TRUE(out_bio); | ||
bssl::UniquePtr<EC_KEY> parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr)); | ||
ASSERT_TRUE(parsed_key); | ||
|
||
// Validate key content matches | ||
const BIGNUM *orig_priv = EC_KEY_get0_private_key(orig_key.get()); | ||
const BIGNUM *parsed_priv = EC_KEY_get0_private_key(parsed_key.get()); | ||
ASSERT_EQ(BN_cmp(orig_priv, parsed_priv), 0); | ||
|
||
RemoveFile(temp_pem); | ||
} | ||
|
||
TEST_F(ECTest, HelpOption) { | ||
args_list_t args = {"-help"}; | ||
ASSERT_TRUE(ecTool(args)); | ||
} | ||
|
||
TEST_F(ECTest, InvalidInputFile) { | ||
args_list_t args = {"-in", "/nonexistent/file.pem", "-out", out_path}; | ||
ASSERT_FALSE(ecTool(args)); | ||
} | ||
|
||
TEST_F(ECTest, InvalidOutputPath) { | ||
args_list_t args = {"-in", pem_key_path, "-out", "/nonexistent/dir/output.pem"}; | ||
ASSERT_FALSE(ecTool(args)); | ||
} | ||
|
||
TEST_F(ECTest, CompareWithOpenSSLPEMOutput) { | ||
if (tool_executable_path == nullptr || openssl_executable_path == nullptr) { | ||
GTEST_SKIP() << "Skipping test: AWSLC_TOOL_PATH and/or OPENSSL_TOOL_PATH environment variables are not set"; | ||
} | ||
|
||
std::string tool_cmd = std::string(tool_executable_path) + " ec -in " + pem_key_path + " -out " + out_path; | ||
std::string openssl_cmd = std::string(openssl_executable_path) + " ec -in " + pem_key_path + " -out " + out_path_openssl; | ||
|
||
ASSERT_EQ(system(tool_cmd.c_str()), 0); | ||
ASSERT_EQ(system(openssl_cmd.c_str()), 0); | ||
|
||
bssl::UniquePtr<BIO> tool_bio(BIO_new_file(out_path, "rb")); | ||
bssl::UniquePtr<BIO> openssl_bio(BIO_new_file(out_path_openssl, "rb")); | ||
ASSERT_TRUE(tool_bio); | ||
ASSERT_TRUE(openssl_bio); | ||
|
||
bssl::UniquePtr<EC_KEY> tool_key(PEM_read_bio_ECPrivateKey(tool_bio.get(), nullptr, nullptr, nullptr)); | ||
bssl::UniquePtr<EC_KEY> openssl_key(PEM_read_bio_ECPrivateKey(openssl_bio.get(), nullptr, nullptr, nullptr)); | ||
ASSERT_TRUE(tool_key); | ||
ASSERT_TRUE(openssl_key); | ||
} | ||
|
||
TEST_F(ECTest, CompareWithOpenSSLDEROutput) { | ||
if (tool_executable_path == nullptr || openssl_executable_path == nullptr) { | ||
GTEST_SKIP() << "Skipping test: AWSLC_TOOL_PATH and/or OPENSSL_TOOL_PATH environment variables are not set"; | ||
} | ||
|
||
std::string tool_cmd = std::string(tool_executable_path) + " ec -in " + pem_key_path + " -outform DER -out " + out_path; | ||
std::string openssl_cmd = std::string(openssl_executable_path) + " ec -in " + pem_key_path + " -outform DER -out " + out_path_openssl; | ||
|
||
ASSERT_EQ(system(tool_cmd.c_str()), 0); | ||
ASSERT_EQ(system(openssl_cmd.c_str()), 0); | ||
|
||
bssl::UniquePtr<BIO> tool_bio(BIO_new_file(out_path, "rb")); | ||
bssl::UniquePtr<BIO> openssl_bio(BIO_new_file(out_path_openssl, "rb")); | ||
ASSERT_TRUE(tool_bio); | ||
ASSERT_TRUE(openssl_bio); | ||
|
||
bssl::UniquePtr<EC_KEY> tool_key(d2i_ECPrivateKey_bio(tool_bio.get(), nullptr)); | ||
bssl::UniquePtr<EC_KEY> openssl_key(d2i_ECPrivateKey_bio(openssl_bio.get(), nullptr)); | ||
ASSERT_TRUE(tool_key); | ||
ASSERT_TRUE(openssl_key); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.