diff --git a/crypto/dsa/internal.h b/crypto/dsa/internal.h index 174cc07d82..fb9b287cc0 100644 --- a/crypto/dsa/internal.h +++ b/crypto/dsa/internal.h @@ -40,8 +40,6 @@ struct dsa_st { CRYPTO_EX_DATA ex_data; }; -#define OPENSSL_DSA_MAX_MODULUS_BITS 10000 - // dsa_check_key performs cheap self-checks on |dsa|, and ensures it is within // DoS bounds. It returns one on success and zero on error. int dsa_check_key(const DSA *dsa); diff --git a/crypto/test/test_util.cc b/crypto/test/test_util.cc index e367736484..6e67d3782d 100644 --- a/crypto/test/test_util.cc +++ b/crypto/test/test_util.cc @@ -15,11 +15,13 @@ #include "test_util.h" #include +#include #include #include "../internal.h" #include "openssl/pem.h" +#include "openssl/rand.h" void hexdump(FILE *fp, const char *msg, const void *in, size_t len) { @@ -166,27 +168,33 @@ size_t createTempFILEpath(char buffer[PATH_MAX]) { } size_t createTempDirPath(char buffer[PATH_MAX]) { - char pathname[PATH_MAX]; - char tempdir[PATH_MAX]; - - if (0 == GetTempPathA(PATH_MAX, pathname)) { + char temp_path[PATH_MAX]; + union { + uint8_t bytes[8]; + uint64_t value; + } random_bytes; + + // Get the temporary path + if (0 == GetTempPathA(PATH_MAX, temp_path)) { return 0; } - // Generate a unique name using Windows API - if (0 == GetTempFileNameA(pathname, "awslctestdir", 0, tempdir)) { + if (!RAND_bytes(random_bytes.bytes, sizeof(random_bytes.bytes))) { return 0; } - // Delete the file that GetTempFileNameA created - DeleteFileA(tempdir); + int written = snprintf(buffer, PATH_MAX, "%s\\awslctest_%" PRIX64, temp_path, random_bytes.value); - if (!CreateDirectoryA(tempdir, NULL)) { + // Check for truncation of dirname + if (written < 0 || written >= PATH_MAX) { return 0; } - strncpy(buffer, tempdir, PATH_MAX); - return strnlen(buffer, PATH_MAX); + if (!CreateDirectoryA(buffer, NULL)) { + return 0; + } + + return (size_t)written; } FILE* createRawTempFILE() { @@ -196,6 +204,7 @@ FILE* createRawTempFILE() { } return fopen(filename, "w+b"); } + #else #include #include diff --git a/include/openssl/dsa.h b/include/openssl/dsa.h index 5733e06132..af470216f2 100644 --- a/include/openssl/dsa.h +++ b/include/openssl/dsa.h @@ -70,6 +70,8 @@ extern "C" { #endif +#define OPENSSL_DSA_MAX_MODULUS_BITS 10000 + // DSA contains functions for signing and verifying with the Digital Signature // Algorithm. @@ -187,8 +189,8 @@ OPENSSL_EXPORT DSA *DSAparams_dup(const DSA *dsa); // Key generation. // DSA_generate_key generates a public/private key pair in |dsa|, which must -// already have parameters setup. It returns one on success and zero on -// error. +// already have parameters setup. Only supports generating up to |OPENSSL_DSA_MAX_MODULUS_BITS| +// bit keys. It returns one on success and zero on error. OPENSSL_EXPORT int DSA_generate_key(DSA *dsa); diff --git a/tool-openssl/CMakeLists.txt b/tool-openssl/CMakeLists.txt index a4942bdee0..aa9ba24425 100644 --- a/tool-openssl/CMakeLists.txt +++ b/tool-openssl/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable( crl.cc dgst.cc rehash.cc + req.cc rsa.cc s_client.cc tool.cc @@ -83,6 +84,8 @@ if(BUILD_TESTING) dgst_test.cc rehash.cc rehash_test.cc + req.cc + req_test.cc rsa.cc rsa_test.cc s_client.cc diff --git a/tool-openssl/internal.h b/tool-openssl/internal.h index af36506c23..bd2dfe64e7 100644 --- a/tool-openssl/internal.h +++ b/tool-openssl/internal.h @@ -34,12 +34,16 @@ bool CRLTool(const args_list_t &args); bool dgstTool(const args_list_t &args); bool md5Tool(const args_list_t &args); bool RehashTool(const args_list_t &args); +bool reqTool(const args_list_t &args); bool rsaTool(const args_list_t &args); bool SClientTool(const args_list_t &args); bool VerifyTool(const args_list_t &args); bool VersionTool(const args_list_t &args); bool X509Tool(const args_list_t &args); +// Req Tool Utilities +bssl::UniquePtr parse_subject_name(std::string &subject_string); + // Rehash tool Utils typedef struct hash_entry_st { // Represents a single certificate/CRL file diff --git a/tool-openssl/req.cc b/tool-openssl/req.cc new file mode 100644 index 0000000000..d9fa514002 --- /dev/null +++ b/tool-openssl/req.cc @@ -0,0 +1,672 @@ +// 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 +#include +#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 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 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 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 prompt_for_subject( + X509_REQ *req, bool isCSR, unsigned long chtype = MBSTRING_ASC) { + + // Get the subject name from the request + bssl::UniquePtr 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(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_attr(X509_ATTRIBUTE_create_by_NID( + nullptr, attr.nid, MBSTRING_ASC, + reinterpret_cast(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 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_new_mem_buf(config, -1)); + if (!bio) { + fprintf(stderr, "Failed to create memory BIO\n"); + return false; + } + + bssl::UniquePtr 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 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 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 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 req(X509_REQ_new()); + bssl::UniquePtr 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; +} diff --git a/tool-openssl/req_test.cc b/tool-openssl/req_test.cc new file mode 100644 index 0000000000..560523c6d4 --- /dev/null +++ b/tool-openssl/req_test.cc @@ -0,0 +1,467 @@ +// 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 "../tool/internal.h" + +#include +#include +#include "../crypto/test/test_util.h" +#include "internal.h" +#include "test_util.h" + +static void setup_config(char *openssl_config_path) { + FILE *config_file = fopen(openssl_config_path, "w"); + if (config_file == NULL) { + fprintf(stderr, "Error opening config file for writing\n"); + return; + } + + // Write the OpenSSL configuration content + fprintf(config_file, + "[ req ]\n" + "default_bits = 2048\n" + "default_keyfile = keyfile.pem\n" + "distinguished_name = req_distinguished_name\n" + "attributes = req_attributes\n" + "prompt = no\n" + "output_password = mypass\n" + "x509_extensions = v3_ca\n" + "\n" + "[ req_distinguished_name ]\n" + "C = GB\n" + "ST = Test State or Province\n" + "L = Test Locality\n" + "O = Organization Name\n" + "OU = Organizational Unit Name\n" + "CN = Common Name\n" + "emailAddress = test@email.address\n" + "\n" + "[ req_attributes ]\n" + "challengePassword = A challenge password\n" + "\n" + "[ v3_ca ]\n" + "subjectKeyIdentifier = hash\n" + "authorityKeyIdentifier = keyid:always,issuer:always\n" + "basicConstraints = critical, CA:true\n"); + + fclose(config_file); +} + +class ReqComparisonTest : public ::testing::Test { + protected: + void SetUp() override { + // Skip gtests if env variables not set + tool_executable_path = getenv("AWSLC_TOOL_PATH"); + openssl_executable_path = getenv("OPENSSL_TOOL_PATH"); + 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"; + } + + ASSERT_GT(createTempFILEpath(cert_path_openssl), 0u); + ASSERT_GT(createTempFILEpath(cert_path_awslc), 0u); + ASSERT_GT(createTempFILEpath(csr_path_openssl), 0u); + ASSERT_GT(createTempFILEpath(csr_path_awslc), 0u); + ASSERT_GT(createTempFILEpath(key_path_openssl), 0u); + ASSERT_GT(createTempFILEpath(key_path_awslc), 0u); + ASSERT_GT(createTempFILEpath(openssl_config_path), 0u); + + setup_config(openssl_config_path); + } + + void TearDown() override { + if (tool_executable_path != nullptr && openssl_executable_path != nullptr) { + RemoveFile(cert_path_openssl); + RemoveFile(cert_path_awslc); + RemoveFile(csr_path_openssl); + RemoveFile(csr_path_awslc); + RemoveFile(key_path_openssl); + RemoveFile(key_path_awslc); + RemoveFile(openssl_config_path); + } + } + + public: + int ExecuteCommand(const std::string &command) { + return system(command.c_str()); + } + + // Load a CSR from a PEM file + bssl::UniquePtr LoadCSR(const char *path) { + bssl::UniquePtr bio(BIO_new_file(path, "r")); + if (!bio) { + return NULL; + } + + bssl::UniquePtr csr( + PEM_read_bio_X509_REQ(bio.get(), nullptr, nullptr, nullptr)); + return csr; + } + + // Load an X509 certificate from a PEM file + bssl::UniquePtr LoadCertificate(const char *path) { + bssl::UniquePtr bio(BIO_new_file(path, "r")); + if (!bio) { + return NULL; + } + + bssl::UniquePtr cert( + PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)); + return cert; + } + + bool CompareCSRs(X509_REQ *csr1, X509_REQ *csr2) { + if (!csr1 || !csr2) + return false; + + // 1. Compare subjects + X509_NAME *name1 = X509_REQ_get_subject_name(csr1); + X509_NAME *name2 = X509_REQ_get_subject_name(csr2); + if (X509_NAME_cmp(name1, name2) != 0) + return false; + + // 2. Compare signature algorithms + int sig_nid1 = X509_REQ_get_signature_nid(csr1); + int sig_nid2 = X509_REQ_get_signature_nid(csr2); + if (sig_nid1 != sig_nid2) + return false; + + // 3. Compare public key type and parameters + EVP_PKEY *pkey1 = X509_REQ_get0_pubkey(csr1); + EVP_PKEY *pkey2 = X509_REQ_get0_pubkey(csr2); + if (!pkey1 || !pkey2) { + return false; + } + if (EVP_PKEY_id(pkey1) != EVP_PKEY_id(pkey2)) { + return false; + } + + // For RSA keys, check key size + if (EVP_PKEY_id(pkey1) == EVP_PKEY_RSA) { + RSA *rsa1 = EVP_PKEY_get0_RSA(pkey1); + RSA *rsa2 = EVP_PKEY_get0_RSA(pkey2); + if (!rsa1 || !rsa2) { + return false; + } + if (RSA_size(rsa1) != RSA_size(rsa2)) { + return false; + } + } + + // 4. Verify that both CSRs have valid signatures + if (X509_REQ_verify(csr1, pkey1) != 1) { + return false; + } + if (X509_REQ_verify(csr2, pkey2) != 1) { + return false; + } + + return true; + } + + bool CheckCertificateValidityPeriod(X509 *cert, int expected_days) { + if (!cert) + return false; + + const ASN1_TIME *not_before = X509_get0_notBefore(cert); + const ASN1_TIME *not_after = X509_get0_notAfter(cert); + if (!not_before || !not_after) + return false; + + // Get the difference in days between not_before and not_after + int days, seconds; + if (!ASN1_TIME_diff(&days, &seconds, not_before, not_after)) { + return false; + } + + return (days == expected_days); + } + + // Improved certificate comparison function + bool CompareCertificates(X509 *cert1, X509 *cert2, int expected_days) { + if (!cert1 || !cert2) { + return false; + } + + // 1. Compare subjects + X509_NAME *subj1 = X509_get_subject_name(cert1); + X509_NAME *subj2 = X509_get_subject_name(cert2); + if (X509_NAME_cmp(subj1, subj2) != 0) { + return false; + } + + // 2. Compare issuers + X509_NAME *issuer1 = X509_get_issuer_name(cert1); + X509_NAME *issuer2 = X509_get_issuer_name(cert2); + if (X509_NAME_cmp(issuer1, issuer2) != 0) { + return false; + } + + // 3. Both certificates should be self-signed + if (X509_NAME_cmp(subj1, issuer1) != 0) { + return false; + } + if (X509_NAME_cmp(subj2, issuer2) != 0) { + return false; + } + + // 4. Compare signature algorithms + int sig_nid1 = X509_get_signature_nid(cert1); + int sig_nid2 = X509_get_signature_nid(cert2); + if (sig_nid1 != sig_nid2) { + return false; + } + + // 5. Check validity periods + if (!CheckCertificateValidityPeriod(cert1, expected_days)) { + return false; + } + if (!CheckCertificateValidityPeriod(cert2, expected_days)) { + return false; + } + + // 6. Check public key type and parameters + EVP_PKEY *pkey1 = X509_get0_pubkey(cert1); + EVP_PKEY *pkey2 = X509_get0_pubkey(cert2); + if (!pkey1 || !pkey2) { + return false; + } + + if (EVP_PKEY_id(pkey1) != EVP_PKEY_id(pkey2)) { + return false; + } + + // For RSA keys, check key size + if (EVP_PKEY_id(pkey1) == EVP_PKEY_RSA) { + RSA *rsa1 = EVP_PKEY_get0_RSA(pkey1); + RSA *rsa2 = EVP_PKEY_get0_RSA(pkey2); + if (!rsa1 || !rsa2) { + return false; + } + + if (RSA_size(rsa1) != RSA_size(rsa2)) { + return false; + } + } + + // 7. Verify signatures (self-signed) + if (X509_verify(cert1, pkey1) != 1) { + return false; + } + if (X509_verify(cert2, pkey2) != 1) { + return false; + } + + // 8. Compare extensions - simplified approach + int ext_count1 = X509_get_ext_count(cert1); + int ext_count2 = X509_get_ext_count(cert2); + if (ext_count1 != ext_count2) { + return false; + } + + // Compare each extension by index (assuming same order) + for (int i = 0; i < ext_count1; i++) { + X509_EXTENSION *ext1 = X509_get_ext(cert1, i); + X509_EXTENSION *ext2 = X509_get_ext(cert2, i); + if (!ext1 || !ext2) { + return false; + } + + // Compare extension OIDs + ASN1_OBJECT *obj1 = X509_EXTENSION_get_object(ext1); + ASN1_OBJECT *obj2 = X509_EXTENSION_get_object(ext2); + if (!obj1 || !obj2) { + return false; + } + + if (OBJ_cmp(obj1, obj2) != 0) { + return false; + } + + // Compare critical flags + if (X509_EXTENSION_get_critical(ext1) != + X509_EXTENSION_get_critical(ext2)) { + return false; + } + } + + return true; + } + + char cert_path_openssl[PATH_MAX]; + char cert_path_awslc[PATH_MAX]; + char csr_path_openssl[PATH_MAX]; + char csr_path_awslc[PATH_MAX]; + char key_path_openssl[PATH_MAX]; + char key_path_awslc[PATH_MAX]; + char openssl_config_path[PATH_MAX]; + const char *tool_executable_path; + const char *openssl_executable_path; +}; + +TEST_F(ReqComparisonTest, GenerateBasicCSR) { + // Subject for certificate + std::string subject = + "/C=US/ST=Washington/L=Seattle/O=Example Inc/CN=example.com"; + + std::string awslc_command = std::string(tool_executable_path) + " req -new " + + "-newkey rsa:2048 -nodes -keyout " + + key_path_awslc + " -out " + csr_path_awslc + + " -subj \"" + subject + "\""; + + std::string openssl_command = + std::string(openssl_executable_path) + " req -new " + + "-newkey rsa:2048 -nodes -config " + openssl_config_path + " -keyout " + + key_path_openssl + " -out " + csr_path_openssl + " -subj \"" + subject + + "\""; + + // Execute both commands, return values may not match due to missing conf + // support + ExecuteCommand(awslc_command); + ExecuteCommand(openssl_command); + + // Cross-check CSR attributes + auto csr_tool = LoadCSR(csr_path_awslc); + auto csr_openssl = LoadCSR(csr_path_openssl); + + ASSERT_TRUE(csr_tool != nullptr) << "Failed to load CSR generated by tool"; + ASSERT_TRUE(csr_openssl != nullptr) + << "Failed to load CSR generated by OpenSSL"; + + // Compare CSR attributes + ASSERT_TRUE(CompareCSRs(csr_tool.get(), csr_openssl.get())) + << "CSR attributes don't match"; +} + +// Test for generating a self-signed certificate +TEST_F(ReqComparisonTest, GenerateSelfSignedCertificate) { + std::string subject = + "/C=US/ST=Washington/L=Seattle/O=Example Inc/CN=example.com"; + + std::string tool_command = + std::string(tool_executable_path) + " req -x509 -new " + + "-newkey rsa:2048 -nodes -days 365 -keyout " + key_path_awslc + " -out " + + cert_path_awslc + " -subj \"" + subject + "\""; + + std::string openssl_command = + std::string(openssl_executable_path) + " req -x509 -new " + + "-newkey rsa:2048 -nodes -config " + openssl_config_path + + " -days 365 -keyout " + key_path_openssl + " -out " + cert_path_openssl + + " -subj \"" + subject + "\""; + + // Execute both commands, return values may not match due to missing conf + // support + ExecuteCommand(tool_command); + ExecuteCommand(openssl_command); + + // Load certificates + auto cert_tool = LoadCertificate(cert_path_awslc); + auto cert_openssl = LoadCertificate(cert_path_openssl); + + ASSERT_TRUE(cert_tool != nullptr) + << "Failed to load certificate generated by tool"; + ASSERT_TRUE(cert_openssl != nullptr) + << "Failed to load certificate generated by OpenSSL"; + + // Compare certificates in detail with 365 days validity period + ASSERT_TRUE(CompareCertificates(cert_tool.get(), cert_openssl.get(), 365)) + << "Certificates generated by tool and OpenSSL have different attributes"; +} + +struct SubjectNameTestCase { + std::string input; + bool expect_success; + int expected_entry_count; + std::vector expected_values; + + SubjectNameTestCase(const std::string &input_, bool expect_success_, + int expected_entry_count_, + const std::vector &expected_values_) + : input(input_), + expect_success(expect_success_), + expected_entry_count(expected_entry_count_), + expected_values(expected_values_) {} +}; + +void PrintTo(const SubjectNameTestCase &test_case, std::ostream *os); + +class SubjectNameTest : public testing::TestWithParam { + protected: + static std::string GetEntryValue(X509_NAME *name, int index) { + unsigned char *tmp; + X509_NAME_ENTRY *entry = X509_NAME_get_entry(name, index); + int len = ASN1_STRING_to_UTF8(&tmp, X509_NAME_ENTRY_get_data(entry)); + std::string result = ""; + if (len > 0) { + result.assign(reinterpret_cast(tmp), len); + } + OPENSSL_free(tmp); + return result; + } +}; + +void PrintTo(const SubjectNameTestCase &test_case, std::ostream *os) { + *os << "SubjectNameTestCase{" + << "input: \"" << test_case.input << "\", " + << "expect_success: " << (test_case.expect_success ? "true" : "false") + << ", " + << "expected_entry_count: " << test_case.expected_entry_count << ", " + << "expected_values: ["; + + for (size_t i = 0; i < test_case.expected_values.size(); ++i) { + if (i > 0) + *os << ", "; + *os << "\"" << test_case.expected_values[i] << "\""; + } + + *os << "]}"; +} + +static const SubjectNameTestCase kSubjectNameTestCases[] = { + // Valid subject with multiple fields + SubjectNameTestCase("/C=US/ST=California/O=Example/CN=test.com", true, 4, + {"US", "California", "Example", "test.com"}), + // Escaped characters + SubjectNameTestCase("/CN=test\\/example\\.com", true, 1, + {"test/example.com"}), + // Missing leading slash + SubjectNameTestCase("CN=test.com", false, 0, {""}), + // Missing equals sign + SubjectNameTestCase("/CNtest.com", false, 0, {""}), + // Empty value + SubjectNameTestCase("/CN=/O=test", true, 1, {"test"}), + // Unknown attribute + SubjectNameTestCase("/UNKNOWN=test/CN=example.com", true, 1, + {"example.com"}), + // Empty subject + SubjectNameTestCase("/", true, 0, {""})}; + +INSTANTIATE_TEST_SUITE_P(SubjectNameTests, SubjectNameTest, + testing::ValuesIn(kSubjectNameTestCases)); + +TEST_P(SubjectNameTest, ParseSubjectName) { + const SubjectNameTestCase &test_case = GetParam(); + std::string mutable_input = test_case.input; // Create mutable copy + + bssl::UniquePtr name = parse_subject_name(mutable_input); + + if (!test_case.expect_success) { + EXPECT_EQ(name, nullptr); + return; + } + + ASSERT_NE(name, nullptr); + EXPECT_EQ(X509_NAME_entry_count(name.get()), test_case.expected_entry_count); + + for (size_t i = 0; i < test_case.expected_values.size(); ++i) { + if (!test_case.expected_values[i].empty()) { + EXPECT_EQ(GetEntryValue(name.get(), i), test_case.expected_values[i]); + } + } +} diff --git a/tool-openssl/tool.cc b/tool-openssl/tool.cc index 857d5d368b..dad21d7457 100644 --- a/tool-openssl/tool.cc +++ b/tool-openssl/tool.cc @@ -15,11 +15,12 @@ #include "./internal.h" -static const std::array kTools = {{ +static const std::array kTools = {{ {"crl", CRLTool}, {"dgst", dgstTool}, {"md5", md5Tool}, {"rehash", RehashTool}, + {"req", reqTool}, {"rsa", rsaTool}, {"s_client", SClientTool}, {"verify", VerifyTool},