tool-openssl/req.cc (503 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR ISC
#include <openssl/base.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <stdio.h>
#include <string.h>
#include "../tool/internal.h"
#include "internal.h"
#define DEFAULT_KEY_LENGTH 2048
#define MIN_KEY_LENGTH 512
#define MAX_KEY_LENGTH 16384
#define BUF_SIZE 1024
#define DEFAULT_CHAR_TYPE MBSTRING_ASC
// Notes: -x509 option assumes -new when -in is not passed in with OpenSSL. We
// do not support -in as of now, so -new is implied with -x509.
//
// In general, OpenSSL supports a default config file which it defaults to when
// user input is not provided. We don't support this default config file
// interface. For fields that are not overriden by user input, we hardcode
// default values (e.g. X509 extensions, -keyout defaults to privkey.pem, etc.)
static const argument_t kArguments[] = {
{"-help", kBooleanArgument, "Display option summary"},
{"-new", kBooleanArgument,
"Generates a new certificate request."
"It will prompt the user for the relevant field values."
"If the -newkey option is not given it will generate a new "
"private key with 2048 bits length"},
{"-newkey", kOptionalArgument,
"This option is used to generate a new private "
"key. This option supports RSA keys in the format [rsa:]nbits. "
"If nbits is not given, i.e. -newkey rsa is specified, 2048 "
"bits length is used. This implies -new unless used with -x509."},
{"-days", kOptionalArgument,
"When -x509 is in use this specifies the number of "
"days from today to certify the certificate for, otherwise it "
"is ignored. n should be a positive integer. The default is"
"30 days."},
{"-nodes", kBooleanArgument,
"If this option is specified then if a private "
"key is created it will not be encrypted."},
{"-x509", kBooleanArgument,
"This option outputs a certificate instead of"
"a certificate request. If the -newkey option is not given it "
"will generate a new private key with 2048 bits length"},
{"-subj", kOptionalArgument,
"Sets subject name for new request. The arg must "
"be formatted as /type0=value0/type1=value1/type2=.... "
"Keyword characters may be escaped by \\ (backslash), and "
"whitespace is retained."},
{"-keyout", kOptionalArgument,
"This specifies the output filename for the "
" private key or writes to a file called privkey.pem in the current "
"directory."},
{"-out", kOptionalArgument,
"This specifies the output filename to write to or "
"standard output by default."},
{"", kOptionalArgument, ""}};
// Parse key specification string and generate key. Valid strings are in the
// format rsa:nbits. RSA key with 2048 bit length is used by default is
// |keyspec| is not valid.
static EVP_PKEY *generate_key(const char *keyspec) {
EVP_PKEY *pkey = NULL;
long keylen = DEFAULT_KEY_LENGTH;
int pkey_type = EVP_PKEY_RSA;
// Parse keyspec
if (OPENSSL_strncasecmp(keyspec, "rsa:", 4) == 0) {
char *endptr = NULL;
long value = strtol(keyspec + 4, &endptr, 10);
if (endptr != keyspec + 4 && *endptr == '\0' && errno != ERANGE) {
keylen = value;
} else {
fprintf(stderr, "Invalid RSA key length: %s, using default length\n",
keyspec + 4);
}
} else if (OPENSSL_strcasecmp(keyspec, "rsa") == 0) {
keylen = DEFAULT_KEY_LENGTH;
} else {
fprintf(
stderr,
"Unknown key specification: %s, using RSA key with 2048 bit length\n",
keyspec);
}
// Validate key length
if (keylen < MIN_KEY_LENGTH) {
fprintf(stderr, "Key length too short (minimum %d bits)\n", MIN_KEY_LENGTH);
return NULL;
}
if (keylen > MAX_KEY_LENGTH) {
fprintf(stderr, "Key length too large (maximum %d bits)\n", MAX_KEY_LENGTH);
return NULL;
}
// Create key generation context
bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new_id(pkey_type, NULL));
if (ctx == NULL) {
return NULL;
}
if (EVP_PKEY_keygen_init(ctx.get()) <= 0 ||
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), keylen) <= 0 ||
EVP_PKEY_keygen(ctx.get(), &pkey) <= 0) {
return NULL;
}
return pkey;
}
// Parse the subject string provided by a user with the -subj option.
bssl::UniquePtr<X509_NAME> parse_subject_name(std::string &subject_string) {
const char *subject_name_ptr = subject_string.c_str();
if (*subject_name_ptr++ != '/') {
fprintf(stderr,
"name is expected to be in the format "
"/type0=value0/type1=value1/type2=... where characters may "
"be escaped by \\. This name is not in that format: '%s'\n",
--subject_name_ptr);
return nullptr;
}
// Create new X509_NAME
bssl::UniquePtr<X509_NAME> name(X509_NAME_new());
if (!name) {
return nullptr;
}
std::string type;
std::string value;
while (*subject_name_ptr) {
// Reset strings for new iteration
type.clear();
value.clear();
// Parse type
while (*subject_name_ptr && *subject_name_ptr != '=') {
type += *subject_name_ptr++;
}
if (!*subject_name_ptr) {
fprintf(stderr, "Hit end of string before finding the equals.\n");
return nullptr;
}
// Skip '='
subject_name_ptr++;
// Parse value
while (*subject_name_ptr && *subject_name_ptr != '/') {
if (*subject_name_ptr == '\\' && *(subject_name_ptr + 1)) {
// Handle escaped character
subject_name_ptr++;
value += *subject_name_ptr++;
} else {
value += *subject_name_ptr++;
}
}
// Skip trailing '/' if present
if (*subject_name_ptr == '/') {
subject_name_ptr++;
}
// Convert type to NID, skip unknown attributes
int nid = OBJ_txt2nid(type.c_str());
if (nid == NID_undef) {
fprintf(stderr, "Warning: Skipping unknown attribute \"%s\"\n",
type.c_str());
// Skip unknown attributes
continue;
}
// Skip empty values
if (value.empty()) {
fprintf(stderr,
"Warning: No value specified for attribute \"%s\", Skipped\n",
type.c_str());
continue;
}
// Add entry to the name
if (!X509_NAME_add_entry_by_NID(name.get(), nid, DEFAULT_CHAR_TYPE,
(unsigned char *)value.c_str(), -1, -1,
0)) {
OPENSSL_PUT_ERROR(X509, ERR_R_X509_LIB);
return nullptr;
}
}
return name;
}
typedef struct {
const char *field_name;
const char *short_desc;
const char *default_value;
int nid;
} ReqField;
static const char *prompt_field(const ReqField &field, char *buffer,
size_t buffer_size) {
// Prompt with default value if available
if (field.default_value && field.default_value[0]) {
fprintf(stdout, "%s [%s]: ", field.short_desc, field.default_value);
} else {
fprintf(stdout, "%s: ", field.short_desc);
}
fflush(stdout);
// Get input with fgets
if (fgets(buffer, buffer_size, stdin) == NULL) {
fprintf(stderr, "Error reading input\n");
return NULL;
}
// Remove newline character if present
size_t len = OPENSSL_strnlen(buffer, buffer_size);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
len--;
}
if (strcmp(buffer, ".") == 0) {
// Empty entry requested
return "";
}
if (len == 0 && field.default_value) {
return field.default_value;
}
if (len > 0) {
// Use provided input
return buffer;
}
// Empty input and no default - use empty string
return "";
}
// Default values for subject fields
const ReqField subject_fields[] = {
{"countryName", "Country Name (2 letter code)", "AU", NID_countryName},
{"stateOrProvinceName", "State or Province Name (full name)",
"Some-State", NID_stateOrProvinceName},
{"localityName", "Locality Name (eg, city)", "", NID_localityName},
{"organizationName", "Organization Name (eg, company)",
"Internet Widgits Pty Ltd", NID_organizationName},
{"organizationalUnitName", "Organizational Unit Name (eg, section)", "",
NID_organizationalUnitName},
{"commonName", "Common Name (e.g. server FQDN or YOUR name)", "",
NID_commonName},
{"emailAddress", "Email Address", "", NID_pkcs9_emailAddress}};
// Extra attributes for CSR
const ReqField extra_attributes[] = {
{"challengePassword", "A challenge password", "",
NID_pkcs9_challengePassword},
{"unstructuredName", "An optional company name", "",
NID_pkcs9_unstructuredName}};
static bssl::UniquePtr<X509_NAME> prompt_for_subject(
X509_REQ *req, bool isCSR, unsigned long chtype = MBSTRING_ASC) {
// Get the subject name from the request
bssl::UniquePtr<X509_NAME> subj(X509_NAME_new());
if (!subj) {
fprintf(stderr, "Error getting subject name from request\n");
return NULL;
}
char buffer[BUF_SIZE];
// Process each subject field
for (const auto &field : subject_fields) {
const char *value = prompt_field(field, buffer, sizeof(buffer));
if (value == NULL) {
return NULL;
}
// Only add non-empty values
if (OPENSSL_strnlen(value, BUF_SIZE) > 0) {
if (!X509_NAME_add_entry_by_NID(
subj.get(), field.nid, chtype,
reinterpret_cast<const unsigned char *>(value), -1, -1, 0)) {
fprintf(stderr, "Error adding %s to subject\n", field.field_name);
return NULL;
}
}
}
if (X509_NAME_entry_count(subj.get()) == 0) {
fprintf(stderr, "Error: At least one subject field must be provided.\n");
return NULL;
}
// If this is a CSR, handle extra attributes
if (isCSR) {
fprintf(stdout, "\nPlease enter the following 'extra' attributes\n");
fprintf(stdout, "to be sent with your certificate request\n");
// Process each extra attribute
for (const auto &attr : extra_attributes) {
const char *value = prompt_field(attr, buffer, sizeof(buffer));
if (value == NULL) {
return NULL;
}
// Only add non-empty attributes
if (OPENSSL_strnlen(value, BUF_SIZE) > 0) {
bssl::UniquePtr<X509_ATTRIBUTE> x509_attr(X509_ATTRIBUTE_create_by_NID(
nullptr, attr.nid, MBSTRING_ASC,
reinterpret_cast<const unsigned char *>(value), -1));
if (!x509_attr) {
fprintf(stderr, "Error creating attribute %s\n", attr.field_name);
return NULL;
}
if (!X509_REQ_add1_attr(req, x509_attr.get())) {
fprintf(stderr, "Error adding attribute %s to request\n",
attr.field_name);
return NULL;
}
}
}
}
return subj;
}
static int make_certificate_request(X509_REQ *req, EVP_PKEY *pkey,
std::string &subject_name, bool isCSR) {
bssl::UniquePtr<X509_NAME> name;
// version 1
if (!X509_REQ_set_version(req, 0L)) {
return 0;
}
if (subject_name.empty()) { // Prompt the user
fprintf(stdout,
"You are about to be asked to enter information that will be "
"incorporated\n");
fprintf(stdout, "into your certificate request.\n");
fprintf(stdout,
"What you are about to enter is what is called a Distinguished Name "
"or a DN.\n");
fprintf(stdout,
"There are quite a few fields but you can leave some blank\n");
fprintf(stdout, "For some fields there will be a default value,\n");
fprintf(stdout, "If you enter '.', the field will be left blank.\n");
fprintf(stdout, "\n");
name = prompt_for_subject(req, isCSR);
} else { // Parse user provided string
name = parse_subject_name(subject_name);
if (!name) {
return 0;
}
}
if (!X509_REQ_set_subject_name(req, name.get())) {
return 0;
}
if (!X509_REQ_set_pubkey(req, pkey)) {
return 0;
}
return 1;
}
static int req_password_callback(char *buf, int size, int rwflag,
void *userdata) {
const char *prompt = "Enter PEM pass phrase:";
char verify_buf[BUF_SIZE];
int len;
// Display prompt
fprintf(stderr, "%s", prompt);
fflush(stderr);
// Get password
if (fgets(buf, size, stdin) == NULL) {
fprintf(stderr, "Error reading password\n");
return 0;
}
// Remove trailing newline
len = OPENSSL_strnlen(buf, sizeof(buf));
if (len > 0 && buf[len - 1] == '\n') {
buf[--len] = '\0';
}
// For encryption only (which is the case for req tool)
if (rwflag) {
// Verify password
fprintf(stderr, "Verifying - %s", prompt);
fflush(stderr);
if (fgets(verify_buf, sizeof(verify_buf), stdin) == NULL) {
fprintf(stderr, "Error reading verification password\n");
return 0;
}
// Remove trailing newline
int verify_len = OPENSSL_strnlen(verify_buf, sizeof(verify_buf));
if (verify_len > 0 && verify_buf[verify_len - 1] == '\n')
verify_buf[--verify_len] = '\0';
// Check if passwords match
if (strncmp(buf, verify_buf, BUF_SIZE) != 0) {
fprintf(stderr, "Passwords don't match\n");
return 0;
}
// Enforce minimum length
if (len < 4) {
fprintf(stderr, "Password too short (minimum 4 characters)\n");
return 0;
}
}
return len;
}
// Function to add extensions to a certificate
static bool add_cert_extensions(X509 *cert) {
const char *config =
"[v3_ca]\n"
"subjectKeyIdentifier=hash\n"
"authorityKeyIdentifier=keyid:always,issuer:always\n"
"basicConstraints=critical,CA:true\n";
// Create a BIO for the config
bssl::UniquePtr<BIO> bio(BIO_new_mem_buf(config, -1));
if (!bio) {
fprintf(stderr, "Failed to create memory BIO\n");
return false;
}
bssl::UniquePtr<CONF> conf(NCONF_new(NULL));
if (!conf) {
fprintf(stderr, "Failed to create CONF structure\n");
return false;
}
if (NCONF_load_bio(conf.get(), bio.get(), NULL) <= 0) {
fprintf(stderr, "Failed to load config from BIO\n");
return false;
}
// Set up X509V3 context for certificate
X509V3_CTX ctx;
X509V3_set_ctx_nodb(&ctx);
X509V3_set_ctx(&ctx, cert, cert, NULL, NULL, 0); // Self-signed
X509V3_set_nconf(&ctx, conf.get());
// Add extensions from config to the certificate
bool result = X509V3_EXT_add_nconf(conf.get(), &ctx, "v3_ca", cert) != 0;
return result;
}
// Generate a random serial number for a certificate
static bool generate_serial(X509 *cert) {
bssl::UniquePtr<BIGNUM> bn(BN_new());
if (!bn) {
fprintf(stderr, "Failed to create BIGNUM for serial\n");
return false;
}
constexpr int SERIAL_RAND_BITS = 159;
if (!BN_rand(bn.get(), SERIAL_RAND_BITS, BN_RAND_TOP_ANY,
BN_RAND_BOTTOM_ANY)) {
fprintf(stderr, "Failed to generate random serial number\n");
return false;
}
ASN1_INTEGER *serial = X509_get_serialNumber(cert);
if (!serial) {
fprintf(stderr, "Failed to get certificate serial number field\n");
return false;
}
if (!BN_to_ASN1_INTEGER(bn.get(), serial)) {
fprintf(stderr, "Failed to convert BIGNUM to ASN1_INTEGER\n");
return false;
}
return true;
}
bool reqTool(const args_list_t &args) {
args_map_t parsed_args;
args_list_t extra_args;
if (!ParseKeyValueArguments(parsed_args, extra_args, args, kArguments) ||
extra_args.size() > 0) {
PrintUsage(kArguments);
return false;
}
std::string newkey, subj, keyout, out;
unsigned int days;
bool help = false, new_flag = false, x509_flag = false, nodes = false;
GetBoolArgument(&help, "-help", parsed_args);
GetBoolArgument(&new_flag, "-new", parsed_args);
GetBoolArgument(&x509_flag, "-x509", parsed_args);
GetBoolArgument(&nodes, "-nodes", parsed_args);
GetString(&newkey, "-newkey", "", parsed_args);
GetUnsigned(&days, "-days", 30u, parsed_args);
GetString(&subj, "-subj", "", parsed_args);
GetString(&keyout, "-keyout", "", parsed_args);
GetString(&out, "-out", "", parsed_args);
if (help) {
PrintUsage(kArguments);
return false;
}
if (!new_flag && !x509_flag && newkey.empty()) {
fprintf(stderr,
"Error: Missing required options, -x509, -new, or -newkey must be "
"specified. \n");
return false;
}
std::string keyspec = "rsa:2048";
if (!newkey.empty()) {
keyspec = newkey;
}
bssl::UniquePtr<EVP_PKEY> pkey(generate_key(keyspec.c_str()));
if (!pkey) {
fprintf(stderr, "Error: Failed to generate private key.\n");
return false;
}
// Generate and write private key
const EVP_CIPHER *cipher = NULL;
if (!nodes) {
cipher = EVP_des_ede3_cbc();
}
bssl::UniquePtr<BIO> out_bio;
if (!keyout.empty()) {
fprintf(stderr, "Writing private key to %s\n", keyout.c_str());
out_bio.reset(BIO_new_file(keyout.c_str(), "w"));
} else {
// Default to privkey.pem in the current directory
const char *default_keyfile = "privkey.pem";
fprintf(stderr, "Writing private key to %s (default)\n", default_keyfile);
out_bio.reset(BIO_new_file(default_keyfile, "w"));
}
// If encryption disabled, don't use password prompting callback
if (!out_bio ||
!PEM_write_bio_PrivateKey(out_bio.get(), pkey.get(), cipher, NULL, 0,
cipher ? req_password_callback : NULL, NULL)) {
fprintf(stderr, "Failed to write private key.\n");
return false;
}
// At this point, one of -new -newkey or -x509 must be defined
// Like OpenSSL, generate CSR first - then convert to cert if needed
bssl::UniquePtr<X509_REQ> req(X509_REQ_new());
bssl::UniquePtr<X509> cert(X509_new());
// Always create a CSR first
if (req == NULL ||
!make_certificate_request(req.get(), pkey.get(), subj, !x509_flag)) {
fprintf(stderr, "Failed to create certificate request\n");
return false;
}
// Convert CSR to certificate
if (x509_flag) {
if (cert == NULL) {
fprintf(stderr, "Failed to create X509 structure\n");
return false;
}
// Set version
if (!X509_set_version(cert.get(), 2)) {
fprintf(stderr, "Failed to set certificate version\n");
return false;
}
// Generate random serial number
if (!generate_serial(cert.get())) {
fprintf(stderr, "Failed to generate serial number\n");
return false;
}
// Set subject and issuer from CSR
if (!X509_set_subject_name(cert.get(),
X509_REQ_get_subject_name(req.get())) ||
!X509_set_issuer_name(cert.get(),
X509_REQ_get_subject_name(req.get()))) {
fprintf(stderr, "Failed to set subject/issuer\n");
return false;
}
// Set expiration to be 'days' days from now
if (!X509_gmtime_adj(X509_getm_notBefore(cert.get()), 0)) {
fprintf(stderr, "Failed to set notBefore field\n");
return false;
}
if (!X509_time_adj_ex(X509_getm_notAfter(cert.get()), days, 0, NULL)) {
fprintf(stderr, "Failed to set notAfter field\n");
return false;
}
// Copy public key from CSR
EVP_PKEY *tmppkey = X509_REQ_get0_pubkey(req.get());
if (!tmppkey || !X509_set_pubkey(cert.get(), tmppkey)) {
fprintf(stderr, "Failed to set public key\n");
return false;
}
// Add extensions to certificate
if (!add_cert_extensions(cert.get())) {
fprintf(stderr, "Failed to add extensions to certificate\n");
return false;
}
// Sign the certificate
if (!X509_sign(cert.get(), pkey.get(), EVP_sha256())) {
fprintf(stderr, "Failed to sign certificate\n");
return false;
}
} else {
// Sign the request
if (!X509_REQ_sign(req.get(), pkey.get(), EVP_sha256())) {
return false;
}
}
if (!out.empty()) {
out_bio.reset(BIO_new_file(out.c_str(), "w"));
} else {
// Default to stdout
out_bio.reset(BIO_new_fp(stdout, BIO_CLOSE));
}
// Handle writing out.
if (x509_flag) {
if (!PEM_write_bio_X509(out_bio.get(), cert.get())) {
fprintf(stderr, "Failed to write certificate\n");
return false;
}
} else {
if (!PEM_write_bio_X509_REQ(out_bio.get(), req.get())) {
fprintf(stderr, "Failed to write certificate request\n");
return false;
}
}
return true;
}