Skip to content

Commit ffe498f

Browse files
authored
Add minimal EC CLI tool implementation (#2640)
### Issues: Addresses #CryptoAlg-3382 ### Description of changes: This PR implements a minimal EC CLI tool for AWS-LC to provide EC key processing capabilities similar to OpenSSL's `openssl ec` command. The tool supports essential EC key operations including format conversion between PEM/DER for both private and public keys, using AWS-LC's native EC APIs for proper key handling. ### Testing: - **Unit tests:** 13 comprehensive test cases in `ec_test.cc` covering: - Format conversion (PEM ↔ DER) for both private and public keys - Round-trip validation ensuring data integrity - Error handling for invalid inputs and file operations - Cross-compatibility with OpenSSL when environment variables are set - **Manual verification:** Tested format conversion and OpenSSL interoperability during development By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license. --------- Co-authored-by: kingstjo <[email protected]>
1 parent 0f99f4e commit ffe498f

File tree

5 files changed

+370
-1
lines changed

5 files changed

+370
-1
lines changed

tool-openssl/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ add_executable(
99

1010
crl.cc
1111
dgst.cc
12+
ec.cc
1213
genrsa.cc
1314
pass_util.cc
1415
pkcs8.cc
@@ -89,6 +90,8 @@ if(BUILD_TESTING)
8990
crl_test.cc
9091
dgst.cc
9192
dgst_test.cc
93+
ec.cc
94+
ec_test.cc
9295
genrsa.cc
9396
genrsa_test.cc
9497
pass_util.cc

tool-openssl/ec.cc

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0 OR ISC
3+
4+
#include <openssl/bio.h>
5+
#include <openssl/ec.h>
6+
#include <openssl/err.h>
7+
#include <openssl/pem.h>
8+
#include <string>
9+
#include "internal.h"
10+
11+
enum Format {
12+
FORMAT_PEM = 1,
13+
FORMAT_DER = 2
14+
};
15+
16+
static const argument_t kArguments[] = {
17+
{"-help", kBooleanArgument, "Display this summary"},
18+
{"-inform", kOptionalArgument, "Input format (PEM or DER), default PEM"},
19+
{"-in", kOptionalArgument, "Input file, default stdin"},
20+
{"-pubout", kBooleanArgument, "Output public key, not private"},
21+
{"-out", kOptionalArgument, "Output file, default stdout"},
22+
{"-outform", kOptionalArgument, "Output format (PEM or DER), default PEM"},
23+
{"", kOptionalArgument, ""}};
24+
25+
bool ecTool(const args_list_t &args) {
26+
ordered_args::ordered_args_map_t parsed_args;
27+
args_list_t extra_args;
28+
std::string in_path, out_path, inform_str, outform_str;
29+
bool help = false, pubout = false;
30+
int input_format = FORMAT_PEM, output_format = FORMAT_PEM;
31+
bssl::UniquePtr<BIO> input_bio, output_bio;
32+
bssl::UniquePtr<EC_KEY> ec_key;
33+
34+
if (!ordered_args::ParseOrderedKeyValueArguments(parsed_args, extra_args,
35+
args, kArguments)) {
36+
PrintUsage(kArguments);
37+
goto err;
38+
}
39+
40+
ordered_args::GetBoolArgument(&help, "-help", parsed_args);
41+
ordered_args::GetString(&in_path, "-in", "", parsed_args);
42+
ordered_args::GetString(&out_path, "-out", "", parsed_args);
43+
ordered_args::GetString(&inform_str, "-inform", "PEM", parsed_args);
44+
ordered_args::GetString(&outform_str, "-outform", "PEM", parsed_args);
45+
ordered_args::GetBoolArgument(&pubout, "-pubout", parsed_args);
46+
47+
if (help) {
48+
PrintUsage(kArguments);
49+
return true;
50+
}
51+
52+
if (isStringUpperCaseEqual(inform_str, "DER")) {
53+
input_format = FORMAT_DER;
54+
} else if (isStringUpperCaseEqual(inform_str, "PEM")) {
55+
input_format = FORMAT_PEM;
56+
} else {
57+
fprintf(stderr, "Error: Invalid input format '%s'. Must be PEM or DER\n", inform_str.c_str());
58+
goto err;
59+
}
60+
61+
if (isStringUpperCaseEqual(outform_str, "DER")) {
62+
output_format = FORMAT_DER;
63+
} else if (isStringUpperCaseEqual(outform_str, "PEM")) {
64+
output_format = FORMAT_PEM;
65+
} else {
66+
fprintf(stderr, "Error: Invalid output format '%s'. Must be PEM or DER\n", outform_str.c_str());
67+
goto err;
68+
}
69+
70+
input_bio.reset(in_path.empty() ? BIO_new_fp(stdin, BIO_NOCLOSE)
71+
: BIO_new_file(in_path.c_str(), "rb"));
72+
if (!input_bio) {
73+
fprintf(stderr, "Error: Could not open input\n");
74+
goto err;
75+
}
76+
77+
ec_key.reset(input_format == FORMAT_DER
78+
? d2i_ECPrivateKey_bio(input_bio.get(), nullptr)
79+
: PEM_read_bio_ECPrivateKey(input_bio.get(), nullptr,
80+
nullptr, nullptr));
81+
if (!ec_key) {
82+
fprintf(stderr, "Error: Could not read EC key in %s format\n",
83+
input_format == FORMAT_DER ? "DER" : "PEM");
84+
goto err;
85+
}
86+
87+
output_bio.reset(out_path.empty() ? BIO_new_fp(stdout, BIO_NOCLOSE)
88+
: BIO_new_file(out_path.c_str(), "wb"));
89+
if (!output_bio) {
90+
fprintf(stderr, "Error: Could not open output\n");
91+
goto err;
92+
}
93+
94+
if (pubout) {
95+
if (!(output_format == FORMAT_DER
96+
? i2d_EC_PUBKEY_bio(output_bio.get(), ec_key.get())
97+
: PEM_write_bio_EC_PUBKEY(output_bio.get(), ec_key.get()))) {
98+
goto err;
99+
}
100+
} else {
101+
if (!(output_format == FORMAT_DER
102+
? i2d_ECPrivateKey_bio(output_bio.get(), ec_key.get())
103+
: PEM_write_bio_ECPrivateKey(output_bio.get(), ec_key.get(),
104+
nullptr, nullptr, 0, nullptr,
105+
nullptr))) {
106+
goto err;
107+
}
108+
}
109+
110+
return true;
111+
112+
err:
113+
ERR_print_errors_fp(stderr);
114+
return false;
115+
}

tool-openssl/ec_test.cc

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0 OR ISC
3+
4+
#include <gtest/gtest.h>
5+
#include <openssl/pem.h>
6+
#include <openssl/ec.h>
7+
#include "internal.h"
8+
#include "test_util.h"
9+
#include "../crypto/test/test_util.h"
10+
11+
static EC_KEY* CreateTestECKey() {
12+
bssl::UniquePtr<EC_KEY> ec_key(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
13+
if (!ec_key || !EC_KEY_generate_key(ec_key.get())) {
14+
return nullptr;
15+
}
16+
return ec_key.release();
17+
}
18+
19+
class ECTest : public ::testing::Test {
20+
protected:
21+
void SetUp() override {
22+
ASSERT_GT(createTempFILEpath(pem_key_path), 0u);
23+
ASSERT_GT(createTempFILEpath(der_key_path), 0u);
24+
ASSERT_GT(createTempFILEpath(out_path), 0u);
25+
26+
tool_executable_path = getenv("AWSLC_TOOL_PATH");
27+
openssl_executable_path = getenv("OPENSSL_TOOL_PATH");
28+
29+
if (tool_executable_path != nullptr && openssl_executable_path != nullptr) {
30+
ASSERT_GT(createTempFILEpath(out_path_openssl), 0u);
31+
32+
// Use OpenSSL to generate test keys for better cross-compatibility
33+
std::string pem_cmd = std::string(openssl_executable_path) + " ecparam -genkey -name prime256v1 -out " + pem_key_path;
34+
std::string der_cmd = std::string(openssl_executable_path) + " ecparam -genkey -name prime256v1 | " +
35+
std::string(openssl_executable_path) + " ec -outform DER -out " + der_key_path;
36+
37+
ASSERT_EQ(system(pem_cmd.c_str()), 0) << "Failed to generate PEM key with OpenSSL";
38+
ASSERT_EQ(system(der_cmd.c_str()), 0) << "Failed to generate DER key with OpenSSL";
39+
} else {
40+
// Fallback to AWS-LC key generation
41+
ec_key.reset(CreateTestECKey());
42+
ASSERT_TRUE(ec_key);
43+
44+
bssl::UniquePtr<BIO> pem_bio(BIO_new_file(pem_key_path, "wb"));
45+
ASSERT_TRUE(pem_bio);
46+
ASSERT_TRUE(PEM_write_bio_ECPrivateKey(pem_bio.get(), ec_key.get(), nullptr, nullptr, 0, nullptr, nullptr));
47+
BIO_flush(pem_bio.get());
48+
49+
bssl::UniquePtr<BIO> der_bio(BIO_new_file(der_key_path, "wb"));
50+
ASSERT_TRUE(der_bio);
51+
ASSERT_TRUE(i2d_ECPrivateKey_bio(der_bio.get(), ec_key.get()));
52+
BIO_flush(der_bio.get());
53+
}
54+
}
55+
56+
void TearDown() override {
57+
RemoveFile(pem_key_path);
58+
RemoveFile(der_key_path);
59+
RemoveFile(out_path);
60+
if (tool_executable_path != nullptr && openssl_executable_path != nullptr) {
61+
RemoveFile(out_path_openssl);
62+
}
63+
}
64+
65+
char pem_key_path[PATH_MAX];
66+
char der_key_path[PATH_MAX];
67+
char out_path[PATH_MAX];
68+
char out_path_openssl[PATH_MAX];
69+
const char* tool_executable_path;
70+
const char* openssl_executable_path;
71+
bssl::UniquePtr<EC_KEY> ec_key;
72+
};
73+
74+
TEST_F(ECTest, ReadPEMOutputPEM) {
75+
args_list_t args = {"-in", pem_key_path, "-out", out_path};
76+
ASSERT_TRUE(ecTool(args));
77+
78+
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb"));
79+
ASSERT_TRUE(out_bio);
80+
bssl::UniquePtr<EC_KEY> parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr));
81+
ASSERT_TRUE(parsed_key);
82+
}
83+
84+
TEST_F(ECTest, ReadPEMOutputDER) {
85+
args_list_t args = {"-in", pem_key_path, "-outform", "DER", "-out", out_path};
86+
ASSERT_TRUE(ecTool(args));
87+
88+
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb"));
89+
ASSERT_TRUE(out_bio);
90+
bssl::UniquePtr<EC_KEY> parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr));
91+
ASSERT_TRUE(parsed_key);
92+
}
93+
94+
TEST_F(ECTest, ReadDEROutputPEM) {
95+
args_list_t args = {"-in", der_key_path, "-inform", "DER", "-out", out_path};
96+
ASSERT_TRUE(ecTool(args));
97+
98+
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb"));
99+
ASSERT_TRUE(out_bio);
100+
bssl::UniquePtr<EC_KEY> parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr));
101+
ASSERT_TRUE(parsed_key);
102+
}
103+
104+
TEST_F(ECTest, ReadDEROutputDER) {
105+
args_list_t args = {"-in", der_key_path, "-inform", "DER", "-outform", "DER", "-out", out_path};
106+
ASSERT_TRUE(ecTool(args));
107+
108+
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb"));
109+
ASSERT_TRUE(out_bio);
110+
bssl::UniquePtr<EC_KEY> parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr));
111+
ASSERT_TRUE(parsed_key);
112+
}
113+
114+
TEST_F(ECTest, PublicKeyExtractionPEM) {
115+
args_list_t args = {"-in", pem_key_path, "-pubout", "-out", out_path};
116+
ASSERT_TRUE(ecTool(args));
117+
118+
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb"));
119+
ASSERT_TRUE(out_bio);
120+
bssl::UniquePtr<EC_KEY> parsed_key(PEM_read_bio_EC_PUBKEY(out_bio.get(), nullptr, nullptr, nullptr));
121+
ASSERT_TRUE(parsed_key);
122+
}
123+
124+
TEST_F(ECTest, PublicKeyExtractionDER) {
125+
args_list_t args = {"-in", der_key_path, "-inform", "DER", "-pubout", "-outform", "DER", "-out", out_path};
126+
ASSERT_TRUE(ecTool(args));
127+
128+
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb"));
129+
ASSERT_TRUE(out_bio);
130+
bssl::UniquePtr<EC_KEY> parsed_key(d2i_EC_PUBKEY_bio(out_bio.get(), nullptr));
131+
ASSERT_TRUE(parsed_key);
132+
}
133+
134+
TEST_F(ECTest, RoundTripPEMtoDERtoPEM) {
135+
char temp_der[PATH_MAX];
136+
ASSERT_GT(createTempFILEpath(temp_der), 0u);
137+
138+
// Load original key for comparison
139+
bssl::UniquePtr<BIO> orig_bio(BIO_new_file(pem_key_path, "rb"));
140+
ASSERT_TRUE(orig_bio);
141+
bssl::UniquePtr<EC_KEY> orig_key(PEM_read_bio_ECPrivateKey(orig_bio.get(), nullptr, nullptr, nullptr));
142+
ASSERT_TRUE(orig_key);
143+
144+
args_list_t args1 = {"-in", pem_key_path, "-outform", "DER", "-out", temp_der};
145+
ASSERT_TRUE(ecTool(args1));
146+
147+
args_list_t args2 = {"-in", temp_der, "-inform", "DER", "-out", out_path};
148+
ASSERT_TRUE(ecTool(args2));
149+
150+
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb"));
151+
ASSERT_TRUE(out_bio);
152+
bssl::UniquePtr<EC_KEY> parsed_key(PEM_read_bio_ECPrivateKey(out_bio.get(), nullptr, nullptr, nullptr));
153+
ASSERT_TRUE(parsed_key);
154+
155+
// Validate key content matches
156+
const BIGNUM *orig_priv = EC_KEY_get0_private_key(orig_key.get());
157+
const BIGNUM *parsed_priv = EC_KEY_get0_private_key(parsed_key.get());
158+
ASSERT_EQ(BN_cmp(orig_priv, parsed_priv), 0);
159+
160+
RemoveFile(temp_der);
161+
}
162+
163+
TEST_F(ECTest, RoundTripDERtoPEMtoDER) {
164+
char temp_pem[PATH_MAX];
165+
ASSERT_GT(createTempFILEpath(temp_pem), 0u);
166+
167+
// Load original key for comparison
168+
bssl::UniquePtr<BIO> orig_bio(BIO_new_file(der_key_path, "rb"));
169+
ASSERT_TRUE(orig_bio);
170+
bssl::UniquePtr<EC_KEY> orig_key(d2i_ECPrivateKey_bio(orig_bio.get(), nullptr));
171+
ASSERT_TRUE(orig_key);
172+
173+
args_list_t args1 = {"-in", der_key_path, "-inform", "DER", "-out", temp_pem};
174+
ASSERT_TRUE(ecTool(args1));
175+
176+
args_list_t args2 = {"-in", temp_pem, "-outform", "DER", "-out", out_path};
177+
ASSERT_TRUE(ecTool(args2));
178+
179+
bssl::UniquePtr<BIO> out_bio(BIO_new_file(out_path, "rb"));
180+
ASSERT_TRUE(out_bio);
181+
bssl::UniquePtr<EC_KEY> parsed_key(d2i_ECPrivateKey_bio(out_bio.get(), nullptr));
182+
ASSERT_TRUE(parsed_key);
183+
184+
// Validate key content matches
185+
const BIGNUM *orig_priv = EC_KEY_get0_private_key(orig_key.get());
186+
const BIGNUM *parsed_priv = EC_KEY_get0_private_key(parsed_key.get());
187+
ASSERT_EQ(BN_cmp(orig_priv, parsed_priv), 0);
188+
189+
RemoveFile(temp_pem);
190+
}
191+
192+
TEST_F(ECTest, HelpOption) {
193+
args_list_t args = {"-help"};
194+
ASSERT_TRUE(ecTool(args));
195+
}
196+
197+
TEST_F(ECTest, InvalidInputFile) {
198+
args_list_t args = {"-in", "/nonexistent/file.pem", "-out", out_path};
199+
ASSERT_FALSE(ecTool(args));
200+
}
201+
202+
TEST_F(ECTest, InvalidOutputPath) {
203+
args_list_t args = {"-in", pem_key_path, "-out", "/nonexistent/dir/output.pem"};
204+
ASSERT_FALSE(ecTool(args));
205+
}
206+
207+
TEST_F(ECTest, CompareWithOpenSSLPEMOutput) {
208+
if (tool_executable_path == nullptr || openssl_executable_path == nullptr) {
209+
GTEST_SKIP() << "Skipping test: AWSLC_TOOL_PATH and/or OPENSSL_TOOL_PATH environment variables are not set";
210+
}
211+
212+
std::string tool_cmd = std::string(tool_executable_path) + " ec -in " + pem_key_path + " -out " + out_path;
213+
std::string openssl_cmd = std::string(openssl_executable_path) + " ec -in " + pem_key_path + " -out " + out_path_openssl;
214+
215+
ASSERT_EQ(system(tool_cmd.c_str()), 0);
216+
ASSERT_EQ(system(openssl_cmd.c_str()), 0);
217+
218+
bssl::UniquePtr<BIO> tool_bio(BIO_new_file(out_path, "rb"));
219+
bssl::UniquePtr<BIO> openssl_bio(BIO_new_file(out_path_openssl, "rb"));
220+
ASSERT_TRUE(tool_bio);
221+
ASSERT_TRUE(openssl_bio);
222+
223+
bssl::UniquePtr<EC_KEY> tool_key(PEM_read_bio_ECPrivateKey(tool_bio.get(), nullptr, nullptr, nullptr));
224+
bssl::UniquePtr<EC_KEY> openssl_key(PEM_read_bio_ECPrivateKey(openssl_bio.get(), nullptr, nullptr, nullptr));
225+
ASSERT_TRUE(tool_key);
226+
ASSERT_TRUE(openssl_key);
227+
}
228+
229+
TEST_F(ECTest, CompareWithOpenSSLDEROutput) {
230+
if (tool_executable_path == nullptr || openssl_executable_path == nullptr) {
231+
GTEST_SKIP() << "Skipping test: AWSLC_TOOL_PATH and/or OPENSSL_TOOL_PATH environment variables are not set";
232+
}
233+
234+
std::string tool_cmd = std::string(tool_executable_path) + " ec -in " + pem_key_path + " -outform DER -out " + out_path;
235+
std::string openssl_cmd = std::string(openssl_executable_path) + " ec -in " + pem_key_path + " -outform DER -out " + out_path_openssl;
236+
237+
ASSERT_EQ(system(tool_cmd.c_str()), 0);
238+
ASSERT_EQ(system(openssl_cmd.c_str()), 0);
239+
240+
bssl::UniquePtr<BIO> tool_bio(BIO_new_file(out_path, "rb"));
241+
bssl::UniquePtr<BIO> openssl_bio(BIO_new_file(out_path_openssl, "rb"));
242+
ASSERT_TRUE(tool_bio);
243+
ASSERT_TRUE(openssl_bio);
244+
245+
bssl::UniquePtr<EC_KEY> tool_key(d2i_ECPrivateKey_bio(tool_bio.get(), nullptr));
246+
bssl::UniquePtr<EC_KEY> openssl_key(d2i_ECPrivateKey_bio(openssl_bio.get(), nullptr));
247+
ASSERT_TRUE(tool_key);
248+
ASSERT_TRUE(openssl_key);
249+
}

tool-openssl/internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ tool_func_t FindTool(int argc, char **argv, int &starting_arg);
9292
bool CRLTool(const args_list_t &args);
9393
bool dgstTool(const args_list_t &args);
9494
bool genrsaTool(const args_list_t &args);
95+
bool ecTool(const args_list_t &args);
9596
bool md5Tool(const args_list_t &args);
9697
bool pkcs8Tool(const args_list_t &args);
9798
bool pkeyTool(const args_list_t &args);

0 commit comments

Comments
 (0)