Skip to content

Commit 0cb5d65

Browse files
authored
Add more options to genrsa (#2770)
### Issues: `CryptoAlg-3387` ### Description of changes: Added options: - passout - des3 - aes 128, aes 192, aes 256 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.
1 parent 6c1c11b commit 0cb5d65

File tree

2 files changed

+156
-13
lines changed

2 files changed

+156
-13
lines changed

tool-openssl/genrsa.cc

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ static const char kKeyArgName[] = "key_size";
1818
static const argument_t kArguments[] = {
1919
{"-help", kBooleanArgument, "Display this summary"},
2020
{"-out", kOptionalArgument, "Output file to write the key to"},
21+
{"-aes128", kBooleanArgument, "Encrypt the private key with AES-128-CBC"},
22+
{"-aes192", kBooleanArgument, "Encrypt the private key with AES-192-CBC"},
23+
{"-aes256", kBooleanArgument, "Encrypt the private key with AES-256-CBC"},
24+
{"-des3", kBooleanArgument, "Encrypt the private key with DES3"},
25+
{"-passout", kOptionalArgument, "Output file pass phrase source"},
2126
{"", kOptionalArgument, ""}};
2227

2328
static void DisplayHelp(BIO *bio) {
@@ -105,10 +110,15 @@ bool genrsaTool(const args_list_t &args) {
105110
ordered_args::ordered_args_map_t parsed_args;
106111
args_list_t extra_args{};
107112
std::string out_path;
108-
bool help = false;
113+
bool help = false, aes128 = false, aes192 = false, aes256 = false, des3 = false;
114+
bssl::UniquePtr<std::string> passout_arg(new std::string());
109115
bssl::UniquePtr<BIO> bio;
110116
bssl::UniquePtr<EVP_PKEY> pkey;
111117
unsigned KeySizeBits = 0;
118+
const EVP_CIPHER *cipher = NULL;
119+
const char *password = NULL;
120+
int password_len = 0;
121+
int cipher_count = 0;
112122

113123
// Parse command line arguments
114124
if (!ordered_args::ParseOrderedKeyValueArguments(parsed_args, extra_args,
@@ -122,6 +132,11 @@ bool genrsaTool(const args_list_t &args) {
122132

123133
ordered_args::GetBoolArgument(&help, "-help", parsed_args);
124134
ordered_args::GetString(&out_path, "-out", "", parsed_args);
135+
ordered_args::GetBoolArgument(&aes128, "-aes128", parsed_args);
136+
ordered_args::GetBoolArgument(&aes192, "-aes192", parsed_args);
137+
ordered_args::GetBoolArgument(&aes256, "-aes256", parsed_args);
138+
ordered_args::GetBoolArgument(&des3, "-des3", parsed_args);
139+
ordered_args::GetString(passout_arg.get(), "-passout", "", parsed_args);
125140

126141
// Parse and validate key size first (catches multiple key sizes)
127142
if (!ParseKeySize(extra_args, KeySizeBits)) {
@@ -146,6 +161,13 @@ bool genrsaTool(const args_list_t &args) {
146161
return true; // Help display is a successful exit
147162
}
148163

164+
if (!passout_arg->empty()) {
165+
if (!pass_util::ExtractPassword(passout_arg)) {
166+
fprintf(stderr, "Error: Failed to extract password\n");
167+
goto err;
168+
}
169+
}
170+
149171
// Set up output BIO
150172
bio = CreateOutputBIO(out_path);
151173
if (!bio) {
@@ -159,9 +181,32 @@ bool genrsaTool(const args_list_t &args) {
159181
goto err;
160182
}
161183

162-
// Write the key
163-
if (!PEM_write_bio_PrivateKey(bio.get(), pkey.get(), NULL, NULL, 0, NULL,
164-
NULL)) {
184+
// Cipher selection and mutual exclusion validation
185+
cipher_count = aes128 + aes192 + aes256 + des3;
186+
if (cipher_count > 1) {
187+
fprintf(stderr, "Error: Only one encryption cipher may be specified\n");
188+
goto err;
189+
}
190+
191+
if (aes128) {
192+
cipher = EVP_get_cipherbyname("aes-128-cbc");
193+
} else if (aes192) {
194+
cipher = EVP_get_cipherbyname("aes-192-cbc");
195+
} else if (aes256) {
196+
cipher = EVP_get_cipherbyname("aes-256-cbc");
197+
} else if (des3) {
198+
cipher = EVP_get_cipherbyname("des-ede3-cbc");
199+
} else {
200+
cipher = NULL;
201+
}
202+
203+
// Write the key with optional encryption
204+
password = (!passout_arg->empty()) ? passout_arg->c_str() : NULL;
205+
password_len = (!passout_arg->empty()) ? static_cast<int>(passout_arg->length()) : 0;
206+
207+
if (!PEM_write_bio_PrivateKey(bio.get(), pkey.get(), cipher,
208+
(unsigned char*)password, password_len,
209+
NULL, NULL)) {
165210
goto err;
166211
}
167212

tool-openssl/genrsa_test.cc

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@
1717

1818
const std::vector<unsigned> kStandardKeySizes = {1024, 2048, 3072, 4096};
1919

20+
struct CipherTestCase {
21+
const char* cipher_flag; // "-aes128", "-des3", etc.
22+
const char* cipher_name; // "AES-128-CBC", "DES3-CBC", etc.
23+
};
24+
25+
static const CipherTestCase kCipherTestCases[] = {
26+
{"-des3", "DES3-CBC"},
27+
{"-aes128", "AES-128-CBC"},
28+
{"-aes192", "AES-192-CBC"},
29+
{"-aes256", "AES-256-CBC"},
30+
};
31+
2032

2133
class GenRSATestBase : public ::testing::Test {
2234
protected:
@@ -63,7 +75,49 @@ class GenRSATestBase : public ::testing::Test {
6375
unsigned actual_bits = RSA_bits(rsa.get());
6476
if (actual_bits != expected_bits) {
6577
ADD_FAILURE() << "Key size mismatch. Expected: " << expected_bits
66-
<< " bits, Got: " << actual_bits << " bits";
78+
<< " bits, Got: " << actual_bits << " bits";
79+
return false;
80+
}
81+
}
82+
83+
return true;
84+
}
85+
86+
bool ValidateEncryptedKeyFile(const char *path, const char *password, unsigned expected_bits = 0) {
87+
if (!path) {
88+
ADD_FAILURE() << "Path parameter is null";
89+
return false;
90+
}
91+
92+
if (!password) {
93+
ADD_FAILURE() << "Password parameter is null";
94+
return false;
95+
}
96+
97+
bssl::UniquePtr<BIO> bio(BIO_new_file(path, "rb"));
98+
if (!bio) {
99+
ADD_FAILURE() << "Failed to open encrypted key file: " << path;
100+
return false;
101+
}
102+
103+
bssl::UniquePtr<RSA> rsa(
104+
PEM_read_bio_RSAPrivateKey(bio.get(), nullptr, nullptr,
105+
const_cast<char*>(password)));
106+
if (!rsa) {
107+
ADD_FAILURE() << "Failed to parse encrypted RSA key from PEM file";
108+
return false;
109+
}
110+
111+
if (RSA_check_key(rsa.get()) != 1) {
112+
ADD_FAILURE() << "Encrypted RSA key failed consistency check";
113+
return false;
114+
}
115+
116+
if (expected_bits > 0) {
117+
unsigned actual_bits = RSA_bits(rsa.get());
118+
if (actual_bits != expected_bits) {
119+
ADD_FAILURE() << "Key size mismatch. Expected: " << expected_bits
120+
<< " bits, Got: " << actual_bits << " bits";
67121
return false;
68122
}
69123
}
@@ -100,6 +154,10 @@ class GenRSAParamTest : public GenRSATestBase,
100154
public ::testing::WithParamInterface<unsigned> {};
101155

102156

157+
class GenRSACipherParamTest : public GenRSATestBase,
158+
public ::testing::WithParamInterface<CipherTestCase> {};
159+
160+
103161
TEST_P(GenRSAParamTest, GeneratesKeyFile) {
104162
unsigned key_size = GetParam();
105163

@@ -151,6 +209,36 @@ TEST_P(GenRSAParamTest, OpenSSLCompatibility) {
151209
INSTANTIATE_TEST_SUITE_P(StandardKeySizes, GenRSAParamTest,
152210
::testing::ValuesIn(kStandardKeySizes));
153211

212+
TEST_P(GenRSACipherParamTest, EncryptedKeyGeneration) {
213+
const CipherTestCase& cipher_test = GetParam();
214+
215+
args_list_t args{cipher_test.cipher_flag, "-passout", "pass:testpassword", "-out", out_path_tool, "2048"};
216+
EXPECT_TRUE(genrsaTool(args)) << cipher_test.cipher_name << " encrypted key generation should work";
217+
EXPECT_TRUE(ValidateEncryptedKeyFile(out_path_tool, "testpassword"))
218+
<< cipher_test.cipher_name << " encrypted key should be valid";
219+
}
220+
221+
TEST_P(GenRSACipherParamTest, OpenSSLCompatibility) {
222+
const CipherTestCase& cipher_test = GetParam();
223+
224+
if (!HasCrossCompatibilityTools()) {
225+
GTEST_SKIP() << "Skipping test: AWSLC_TOOL_PATH and/or OPENSSL_TOOL_PATH "
226+
"environment variables are not set";
227+
return;
228+
}
229+
230+
args_list_t args{cipher_test.cipher_flag, "-passout", "pass:testpassword", "-out", out_path_tool, "2048"};
231+
EXPECT_TRUE(genrsaTool(args)) << "AWS-LC " << cipher_test.cipher_name << " key generation failed";
232+
233+
std::string verify_cmd = std::string(openssl_executable_path) +
234+
" rsa -in " + out_path_tool +
235+
" -passin pass:testpassword -check -noout";
236+
EXPECT_EQ(system(verify_cmd.c_str()), 0) << "OpenSSL verification of AWS-LC " << cipher_test.cipher_name << " key failed";
237+
}
238+
239+
INSTANTIATE_TEST_SUITE_P(AllCiphers, GenRSACipherParamTest,
240+
::testing::ValuesIn(kCipherTestCases));
241+
154242
TEST_F(GenRSATest, DefaultKeyGeneration) {
155243
args_list_t args{"-out", out_path_tool};
156244
EXPECT_TRUE(genrsaTool(args)) << "Default key generation failed";
@@ -210,17 +298,27 @@ TEST_F(GenRSATest, FileIOErrors) {
210298
}
211299

212300
TEST_F(GenRSATest, ArgumentValidation) {
213-
// Test missing key size (should use default)
214-
{
215-
args_list_t args{"-out", out_path_tool};
216-
EXPECT_TRUE(genrsaTool(args)) << "Default key size should work";
217-
EXPECT_TRUE(ValidateKeyFile(out_path_tool))
218-
<< "Default key should be valid";
219-
}
220-
221301
// Test help takes precedence
222302
{
223303
args_list_t args{"-help", "-out", out_path_tool, "2048"};
224304
EXPECT_TRUE(genrsaTool(args)) << "Help should work even with other args";
225305
}
226306
}
307+
308+
TEST_F(GenRSATest, CipherMutualExclusionValidation) {
309+
// Test that multiple cipher options are rejected
310+
{
311+
args_list_t args{"-aes128", "-aes256", "-passout", "pass:testpassword", "-out", out_path_tool, "2048"};
312+
EXPECT_FALSE(genrsaTool(args)) << "Command should fail with multiple cipher options";
313+
}
314+
315+
{
316+
args_list_t args{"-aes128", "-des3", "-passout", "pass:testpassword", "-out", out_path_tool, "2048"};
317+
EXPECT_FALSE(genrsaTool(args)) << "Command should fail with multiple cipher options";
318+
}
319+
320+
{
321+
args_list_t args{"-aes192", "-des3", "-passout", "pass:testpassword", "-out", out_path_tool, "2048"};
322+
EXPECT_FALSE(genrsaTool(args)) << "Command should fail with multiple cipher options";
323+
}
324+
}

0 commit comments

Comments
 (0)