diff --git a/tool-openssl/CMakeLists.txt b/tool-openssl/CMakeLists.txt index 815489213d..79d48029be 100644 --- a/tool-openssl/CMakeLists.txt +++ b/tool-openssl/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable( tool.cc x509.cc version.cc + verify.cc ) target_include_directories(openssl PUBLIC ${PROJECT_SOURCE_DIR}/include) @@ -59,6 +60,8 @@ if(BUILD_TESTING) rsa_test.cc x509.cc x509_test.cc + verify.cc + verify_test.cc ) target_link_libraries(tool_openssl_test boringssl_gtest_main ssl crypto) diff --git a/tool-openssl/internal.h b/tool-openssl/internal.h index 5453a5fdae..5382b6f469 100644 --- a/tool-openssl/internal.h +++ b/tool-openssl/internal.h @@ -33,5 +33,6 @@ bool md5Tool(const args_list_t &args); bool rsaTool(const args_list_t &args); bool X509Tool(const args_list_t &args); bool VersionTool(const args_list_t &args); +bool VerifyTool(const args_list_t &args); #endif //INTERNAL_H diff --git a/tool-openssl/tool.cc b/tool-openssl/tool.cc index c4d2f24412..55db5e798a 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 = {{ {"dgst", dgstTool}, {"md5", md5Tool}, {"rsa", rsaTool}, {"x509", X509Tool}, + {"verify", VerifyTool}, {"version", VersionTool} }}; diff --git a/tool-openssl/verify.cc b/tool-openssl/verify.cc new file mode 100644 index 0000000000..6dead029a2 --- /dev/null +++ b/tool-openssl/verify.cc @@ -0,0 +1,176 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include "internal.h" + +static const argument_t kArguments[] = { + { "-help", kBooleanArgument, "Display option summary" }, + { "-CAfile", kOptionalArgument, "A file of trusted certificates. The " + "file should contain one or more certificates in PEM format." }, + { "", kOptionalArgument, "" } +}; + +// setup_verification_store sets up an X509 certificate store for verification. +// It configures the store with file and directory lookups. It loads the +// specified CA file if provided and otherwise uses default locations. +static X509_STORE *setup_verification_store(std::string CAfile) { + bssl::UniquePtr store(X509_STORE_new()); + X509_LOOKUP *lookup; + + if (!store) { + return nullptr; + } + + if (!CAfile.empty()) { + lookup = X509_STORE_add_lookup(store.get(), X509_LOOKUP_file()); + if (!lookup || !X509_LOOKUP_load_file(lookup, CAfile.c_str(), X509_FILETYPE_PEM)) { + fprintf(stderr, "Error loading file %s\n", CAfile.c_str()); + return nullptr; + } + } + + // Add default dir path + lookup = X509_STORE_add_lookup(store.get(), X509_LOOKUP_hash_dir()); + if (!lookup || !X509_LOOKUP_add_dir(lookup, NULL, X509_FILETYPE_DEFAULT)) { + return nullptr; + } + + return store.release(); +} + +static int cb(int ok, X509_STORE_CTX *ctx) { + if (!ok) { + int cert_error = X509_STORE_CTX_get_error(ctx); + X509 *current_cert = X509_STORE_CTX_get_current_cert(ctx); + + if (current_cert != NULL) { + X509_NAME_print_ex_fp(stderr, + X509_get_subject_name(current_cert), + 0, XN_FLAG_ONELINE); + fprintf(stderr, "\n"); + } + fprintf(stderr, "%serror %d at %d depth lookup: %s\n", + X509_STORE_CTX_get0_parent_ctx(ctx) ? "[CRL path] " : "", + cert_error, + X509_STORE_CTX_get_error_depth(ctx), + X509_verify_cert_error_string(cert_error)); + + /* + * Pretend that some errors are ok, so they don't stop further + * processing of the certificate chain. Setting ok = 1 does this. + * After X509_verify_cert() is done, we verify that there were + * no actual errors, even if the returned value was positive. + */ + switch (cert_error) { + case X509_V_ERR_NO_EXPLICIT_POLICY: + /* fall thru */ + case X509_V_ERR_CERT_HAS_EXPIRED: + /* Continue even if the leaf is a self-signed cert */ + case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: + /* Continue after extension errors too */ + case X509_V_ERR_INVALID_CA: + case X509_V_ERR_INVALID_NON_CA: + case X509_V_ERR_PATH_LENGTH_EXCEEDED: + case X509_V_ERR_CRL_HAS_EXPIRED: + case X509_V_ERR_CRL_NOT_YET_VALID: + case X509_V_ERR_UNHANDLED_CRITICAL_EXTENSION: + /* errors due to strict conformance checking (-x509_strict) */ + case X509_V_ERR_INVALID_PURPOSE: + ok = 1; + } + } + return ok; +} + +static int check(X509_STORE *ctx, const char *file) { + bssl::UniquePtr cert; + int i = 0, ret = 0; + + if (file) { + ScopedFILE cert_file(fopen(file, "rb")); + if (!cert_file) { + fprintf(stderr, "error %s: reading certificate failed\n", file); + return 0; + } + cert.reset(PEM_read_X509(cert_file.get(), nullptr, nullptr, nullptr)); + + } else { + bssl::UniquePtr input(BIO_new_fp(stdin, BIO_CLOSE)); + cert.reset(PEM_read_bio_X509(input.get(), nullptr, nullptr, nullptr)); + } + + if (cert.get() == nullptr) { + return 0; + } + + bssl::UniquePtr store_ctx(X509_STORE_CTX_new()); + if (store_ctx == nullptr || store_ctx.get() == nullptr) { + fprintf(stderr, "error %s: X.509 store context allocation failed\n", + (file == nullptr) ? "stdin" : file); + return 0; + } + + if (!X509_STORE_CTX_init(store_ctx.get(), ctx, cert.get(), nullptr)) { + fprintf(stderr, + "error %s: X.509 store context initialization failed\n", + (file == nullptr) ? "stdin" : file); + return 0; + } + + i = X509_verify_cert(store_ctx.get()); + if (i > 0 && X509_STORE_CTX_get_error(store_ctx.get()) == X509_V_OK) { + fprintf(stdout, "%s: OK\n", (file == nullptr) ? "stdin" : file); + ret = 1; + } else { + fprintf(stderr, + "error %s: verification failed\n", + (file == nullptr) ? "stdin" : file); + } + + return ret; +} + +bool VerifyTool(const args_list_t &args) { + std::string cafile; + size_t i = 0; + + if (args.size() == 1 && args[0] == "-help") { + fprintf(stderr, + "Usage: verify [options] [cert.pem...]\n" + "Certificates must be in PEM format. They can be specified in one or more files.\n" + "If no files are specified, the tool will read from stdin.\n\n" + "Valid options are:\n"); + PrintUsage(kArguments); + return false; + } else if (args.size() > 1 && args[0] == "-CAfile") { + cafile = args[1]; + i += 2; + } + + bssl::UniquePtr store(setup_verification_store(cafile)); + // Initialize certificate verification store + if (!store.get()) { + fprintf(stderr, "Error: Unable to setup certificate verification store."); + return false; + } + X509_STORE_set_verify_cb(store.get(), cb); + + ERR_clear_error(); + + int ret = 1; + + // No additional file or certs provided, read from stdin + if (args.size() == i) { + ret &= check(store.get(), NULL); + } else { + // Certs provided as files + for (; i < args.size(); i++) { + ret &= check(store.get(), args[i].c_str()); + } + } + + return ret == 1; +} diff --git a/tool-openssl/verify_test.cc b/tool-openssl/verify_test.cc new file mode 100644 index 0000000000..9e8c3ce7c9 --- /dev/null +++ b/tool-openssl/verify_test.cc @@ -0,0 +1,139 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include "openssl/x509.h" +#include +#include +#include "internal.h" +#include "test_util.h" +#include "../crypto/test/test_util.h" + + +class VerifyTest : public ::testing::Test { +protected: + void SetUp() override { + ASSERT_GT(createTempFILEpath(ca_path), 0u); + ASSERT_GT(createTempFILEpath(in_path), 0u); + + bssl::UniquePtr x509(CreateAndSignX509Certificate()); + ASSERT_TRUE(x509); + + ScopedFILE in_file(fopen(in_path, "wb")); + ASSERT_TRUE(in_file); + ASSERT_TRUE(PEM_write_X509(in_file.get(), x509.get())); + + ScopedFILE ca_file(fopen(ca_path, "wb")); + ASSERT_TRUE(ca_file); + ASSERT_TRUE(PEM_write_X509(ca_file.get(), x509.get())); + } + void TearDown() override { + RemoveFile(ca_path); + RemoveFile(in_path); + } + char ca_path[PATH_MAX]; + char in_path[PATH_MAX]; +}; + + +// ----------------------------- Verify Option Tests ----------------------------- + +// Test -CAfile with self-signed certificate +TEST_F(VerifyTest, VerifyTestSelfSignedCertWithCAfileTest) { + args_list_t args = {"-CAfile", ca_path, in_path}; + bool result = VerifyTool(args); + ASSERT_TRUE(result); +} + +// Test self-signed certificate without -CAfile +TEST_F(VerifyTest, VerifyTestSelfSignedCertWithoutCAfile) { + args_list_t args = {in_path}; + bool result = VerifyTool(args); + ASSERT_FALSE(result); +} + + +// -------------------- Verify OpenSSL Comparison Tests -------------------------- + +// Comparison tests cannot run without set up of environment variables: +// AWSLC_TOOL_PATH and OPENSSL_TOOL_PATH. + +class VerifyComparisonTest : 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(in_path), 0u); + ASSERT_GT(createTempFILEpath(ca_path), 0u); + ASSERT_GT(createTempFILEpath(out_path_tool), 0u); + ASSERT_GT(createTempFILEpath(out_path_openssl), 0u); + + x509.reset(CreateAndSignX509Certificate()); + ASSERT_TRUE(x509); + + ScopedFILE in_file(fopen(in_path, "wb")); + ASSERT_TRUE(in_file); + ASSERT_TRUE(PEM_write_X509(in_file.get(), x509.get())); + + ScopedFILE ca_file(fopen(ca_path, "wb")); + ASSERT_TRUE(ca_file); + ASSERT_TRUE(PEM_write_X509(ca_file.get(), x509.get())); + } + + void TearDown() override { + if (tool_executable_path != nullptr && openssl_executable_path != nullptr) { + RemoveFile(in_path); + RemoveFile(out_path_tool); + RemoveFile(out_path_openssl); + RemoveFile(ca_path); + } + } + + char in_path[PATH_MAX]; + char ca_path[PATH_MAX]; + char out_path_tool[PATH_MAX]; + char out_path_openssl[PATH_MAX]; + bssl::UniquePtr x509; + const char* tool_executable_path; + const char* openssl_executable_path; + std::string tool_output_str; + std::string openssl_output_str; +}; + +// Test against OpenSSL with -CAfile & self-signed cert fed in as a file +// "openssl verify -CAfile cert.pem cert.pem" +TEST_F(VerifyComparisonTest, VerifyToolOpenSSLCAFileSelfSignedComparison) { + std::string tool_command = std::string(tool_executable_path) + " verify -CAfile " + ca_path + " " + in_path + " &> " + out_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " verify -CAfile " + ca_path + " " + in_path + " &> " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} + +// Test against OpenSSL with -CAfile & 2 self-signed cert fed in as files +// "openssl verify -CAfile cert.pem cert.pem cert.pem" +TEST_F(VerifyComparisonTest, VerifyToolOpenSSLCAFileMultipleFilesComparison) { + std::string tool_command = std::string(tool_executable_path) + " verify -CAfile " + ca_path + " " + in_path + " " + in_path + " &> " + out_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " verify -CAfile " + ca_path + " " + in_path + " " + in_path + " &> " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} + +// Test against OpenSSL with -CAfile & self-signed cert fed through stdin +// "cat cert.pem | openssl verify -CAfile cert.pem" +TEST_F(VerifyComparisonTest, VerifyToolOpenSSLCAFileSelfSignedStdinComparison) { + std::string tool_command = "cat " + std::string(ca_path) + " | " + std::string(tool_executable_path) + " verify -CAfile " + ca_path + " &> " + out_path_tool; + std::string openssl_command = "cat " + std::string(ca_path) + " | " + std::string(openssl_executable_path) + " verify -CAfile " + ca_path + " &> " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} diff --git a/tool-openssl/x509.cc b/tool-openssl/x509.cc index deb909c271..b12fc66ede 100644 --- a/tool-openssl/x509.cc +++ b/tool-openssl/x509.cc @@ -16,9 +16,19 @@ static const argument_t kArguments[] = { { "-noout", kBooleanArgument, "Prevents output of the encoded version of the certificate" }, { "-dates", kBooleanArgument, "Print the start and expiry dates of a certificate" }, { "-modulus", kBooleanArgument, "Prints out value of the modulus of the public key contained in the certificate" }, + { "-subject", kBooleanArgument, "Prints the subject name"}, + { "-subject_hash", kBooleanArgument, "Prints subject hash value"}, + { "-subject_hash_old", kBooleanArgument, "Prints old OpenSSL style (MD5) subject hash value"}, + { "-fingerprint", kBooleanArgument, "Prints the certificate fingerprint"}, { "-checkend", kOptionalArgument, "Check whether cert expires in the next arg seconds" }, { "-days", kOptionalArgument, "Number of days until newly generated certificate expires - default 30" }, { "-text", kBooleanArgument, "Pretty print the contents of the certificate"}, + { "-inform", kOptionalArgument, "This specifies the input format normally the command will expect an X509 " + "certificate but this can change if other options such as -req are present. " + "The DER format is the DER encoding of the certificate and PEM is the base64 " + "encoding of the DER encoding with header and footer lines added. The default " + "format is PEM."}, + { "-enddate", kBooleanArgument, "Prints out the expiry date of the certificate, that is the notAfter date."}, { "", kOptionalArgument, "" } }; @@ -64,8 +74,10 @@ bool X509Tool(const args_list_t &args) { return false; } - std::string in_path, out_path, signkey_path, checkend_str, days_str; - bool noout = false, modulus = false, dates = false, req = false, help = false, text = false; + std::string in_path, out_path, signkey_path, checkend_str, days_str, inform; + bool noout = false, modulus = false, dates = false, req = false, help = false, + text = false, subject = false, fingerprint = false, enddate = false, + subject_hash = false, subject_hash_old = false; std::unique_ptr checkend, days; GetBoolArgument(&help, "-help", parsed_args); @@ -76,7 +88,13 @@ bool X509Tool(const args_list_t &args) { GetBoolArgument(&noout, "-noout", parsed_args); GetBoolArgument(&dates, "-dates", parsed_args); GetBoolArgument(&modulus, "-modulus", parsed_args); + GetBoolArgument(&subject, "-subject", parsed_args); + GetBoolArgument(&subject_hash, "-subject_hash", parsed_args); + GetBoolArgument(&subject_hash_old, "-subject_hash_old", parsed_args); + GetBoolArgument(&fingerprint, "-fingerprint", parsed_args); GetBoolArgument(&text, "-text", parsed_args); + GetString(&inform, "-inform", "", parsed_args); + GetBoolArgument(&enddate, "-enddate", parsed_args); // Display x509 tool option summary if (help) { @@ -91,11 +109,7 @@ bool X509Tool(const args_list_t &args) { BIO_write_filename(output_bio.get(), out_path.c_str()); } - // Check for required option -in, and -req must include -signkey - if (in_path.empty()) { - fprintf(stderr, "Error: missing required argument '-in'\n"); - return false; - } + // -req must include -signkey if (req && signkey_path.empty()) { fprintf(stderr, "Error: '-req' option must be used with '-signkey' option\n"); return false; @@ -135,14 +149,34 @@ bool X509Tool(const args_list_t &args) { days.reset(new unsigned(std::stoul(days_str))); } - ScopedFILE in_file(fopen(in_path.c_str(), "rb")); - if (!in_file) { - fprintf(stderr, "Error: unable to load certificate from '%s'\n", in_path.c_str()); - return false; + // Check -inform has a valid value + if(!inform.empty()) { + if (inform != "DER" && inform != "PEM") { + fprintf(stderr, "Error: '-inform' option must specify a valid encoding DER|PEM\n"); + return false; + } + } + + // Read from stdin if no -in path provided + ScopedFILE in_file; + if (in_path.empty()) { + in_file.reset(stdin); + } else { + in_file.reset(fopen(in_path.c_str(), "rb")); + if (!in_file) { + fprintf(stderr, "Error: unable to load certificate from '%s'\n", in_path.c_str()); + return false; + } } if (req) { - bssl::UniquePtr csr(PEM_read_X509_REQ(in_file.get(), nullptr, nullptr, nullptr)); + bssl::UniquePtr csr; + if (!inform.empty() && inform == "DER") { + csr.reset(d2i_X509_REQ_fp(in_file.get(), nullptr)); + } else { + csr.reset(PEM_read_X509_REQ(in_file.get(), nullptr, nullptr, nullptr)); + } + if (!csr) { fprintf(stderr, "Error: error parsing CSR from '%s'\n", in_path.c_str()); ERR_print_errors_fp(stderr); @@ -194,26 +228,19 @@ bool X509Tool(const args_list_t &args) { return false; } } else { - // Parse x509 certificate from input PEM file - bssl::UniquePtr x509(PEM_read_X509(in_file.get(), nullptr, nullptr, nullptr)); + // Parse x509 certificate from input file + bssl::UniquePtr x509; + if (!inform.empty() && inform == "DER") { + x509.reset(d2i_X509_fp(in_file.get(), nullptr)); + } else { + x509.reset(PEM_read_X509(in_file.get(), nullptr, nullptr, nullptr)); + } + if (!x509) { fprintf(stderr, "Error: error parsing certificate from '%s'\n", in_path.c_str()); ERR_print_errors_fp(stderr); return false; } - if(text) { - X509_print(output_bio.get(), x509.get()); - } - - if (dates) { - BIO_printf(output_bio.get(), "notBefore="); - ASN1_TIME_print(output_bio.get(), X509_get_notBefore(x509.get())); - BIO_printf(output_bio.get(), "\n"); - - BIO_printf(output_bio.get(), "notAfter="); - ASN1_TIME_print(output_bio.get(), X509_get_notAfter(x509.get())); - BIO_printf(output_bio.get(), "\n"); - } if (modulus) { bssl::UniquePtr pkey(X509_get_pubkey(x509.get())); @@ -250,6 +277,63 @@ bool X509Tool(const args_list_t &args) { } } + if(text) { + X509_print(output_bio.get(), x509.get()); + } + + if (subject) { + X509_NAME *subject_name = X509_get_subject_name(x509.get()); + if (!subject_name) { + fprintf(stderr, "Error: unable to obtain subject from certificate\n"); + return false; + } + + BIO_printf(output_bio.get(), "subject="); + X509_NAME_print_ex(output_bio.get(), subject_name, 0, XN_FLAG_ONELINE); + BIO_printf(output_bio.get(), "\n"); + } + + if (fingerprint) { + unsigned int out_len; + unsigned char md[EVP_MAX_MD_SIZE]; + const EVP_MD *digest = EVP_sha1(); + + if (!X509_digest(x509.get(), digest, md, &out_len)) { + fprintf(stderr, "Error: unable to obtain digest\n"); + return false; + } + BIO_printf(output_bio.get(), "%s Fingerprint=", + OBJ_nid2sn(EVP_MD_type(digest))); + for (int j = 0; j < (int)out_len; j++) { + BIO_printf(output_bio.get(), "%02X%c", md[j], (j + 1 == (int)out_len) + ? '\n' : ':'); + } + } + + if (subject_hash) { + BIO_printf(output_bio.get(), "%08x\n", X509_subject_name_hash(x509.get())); + } + + if(subject_hash_old) { + BIO_printf(output_bio.get(), "%08x\n", X509_subject_name_hash_old(x509.get())); + } + + if (dates) { + BIO_printf(output_bio.get(), "notBefore="); + ASN1_TIME_print(output_bio.get(), X509_get_notBefore(x509.get())); + BIO_printf(output_bio.get(), "\n"); + + BIO_printf(output_bio.get(), "notAfter="); + ASN1_TIME_print(output_bio.get(), X509_get_notAfter(x509.get())); + BIO_printf(output_bio.get(), "\n"); + } + + if (!dates && enddate) { + BIO_printf(output_bio.get(), "notAfter="); + ASN1_TIME_print(output_bio.get(), X509_get_notAfter(x509.get())); + BIO_printf(output_bio.get(), "\n"); + } + if (checkend) { bssl::UniquePtr current_time(ASN1_TIME_set(nullptr, std::time(nullptr))); ASN1_TIME *end_time = X509_getm_notAfter(x509.get()); diff --git a/tool-openssl/x509_test.cc b/tool-openssl/x509_test.cc index 1292638708..5e5d48c963 100644 --- a/tool-openssl/x509_test.cc +++ b/tool-openssl/x509_test.cc @@ -9,37 +9,71 @@ #include "../crypto/test/test_util.h" #include -X509* CreateAndSignX509Certificate() { - bssl::UniquePtr x509(X509_new()); - if (!x509) return nullptr; - - // Set validity period for 30 days - if (!X509_gmtime_adj(X509_getm_notBefore(x509.get()), 0) || - !X509_gmtime_adj(X509_getm_notAfter(x509.get()), 60 * 60 * 24 * 30L)) { - return nullptr; - } + X509* CreateAndSignX509Certificate() { + bssl::UniquePtr x509(X509_new()); + if (!x509) return nullptr; - bssl::UniquePtr pkey(EVP_PKEY_new()); - if (!pkey) { - return nullptr; - } - bssl::UniquePtr rsa(RSA_new()); - bssl::UniquePtr bn(BN_new()); - if (!bn || !BN_set_word(bn.get(), RSA_F4) || - !RSA_generate_key_ex(rsa.get(), 2048, bn.get(), nullptr) || - !EVP_PKEY_assign_RSA(pkey.get(), rsa.release())) { - return nullptr; - } - if (!X509_set_pubkey(x509.get(), pkey.get())) { - return nullptr; - } + // Set version to X509v3 + X509_set_version(x509.get(), X509_VERSION_3); - if (X509_sign(x509.get(), pkey.get(), EVP_sha256()) <= 0) { - return nullptr; - } + // Set validity period for 30 days + if (!X509_gmtime_adj(X509_getm_notBefore(x509.get()), 0) || + !X509_gmtime_adj(X509_getm_notAfter(x509.get()), 60 * 60 * 24 * 30L)) { + return nullptr; + } - return x509.release(); -} + bssl::UniquePtr pkey(EVP_PKEY_new()); + if (!pkey) { + return nullptr; + } + bssl::UniquePtr rsa(RSA_new()); + bssl::UniquePtr bn(BN_new()); + if (!bn || !BN_set_word(bn.get(), RSA_F4) || + !RSA_generate_key_ex(rsa.get(), 2048, bn.get(), nullptr) || + !EVP_PKEY_assign_RSA(pkey.get(), rsa.release())) { + return nullptr; + } + if (!X509_set_pubkey(x509.get(), pkey.get())) { + return nullptr; + } + + X509_NAME *subject_name = X509_NAME_new(); + if (!X509_NAME_add_entry_by_NID( + subject_name, NID_organizationName, MBSTRING_UTF8, + reinterpret_cast("Org"), /*len=*/-1, /*loc=*/-1, + /*set=*/0) || + !X509_NAME_add_entry_by_NID( + subject_name, NID_commonName, MBSTRING_UTF8, + reinterpret_cast("Name"), /*len=*/-1, /*loc=*/-1, + /*set=*/0)) { + return nullptr; + } + + // self-signed + if (!X509_set_subject_name(x509.get(), subject_name) || + !X509_set_issuer_name(x509.get(), subject_name)) { + return nullptr; + }; + X509_NAME_free(subject_name); + + // Add X509v3 extensions + X509V3_CTX ctx; + X509V3_set_ctx_nodb(&ctx); + X509V3_set_ctx(&ctx, x509.get(), x509.get(), nullptr, nullptr, 0); + + X509_EXTENSION *ext; + if (!(ext = X509V3_EXT_conf_nid(nullptr, &ctx, NID_basic_constraints, const_cast("critical,CA:TRUE"))) || + !X509_add_ext(x509.get(), ext, -1)) { + return nullptr; + } + X509_EXTENSION_free(ext); + + if (X509_sign(x509.get(), pkey.get(), EVP_sha256()) <= 0) { + return nullptr; + } + + return x509.release(); + } class X509Test : public ::testing::Test { protected: @@ -48,6 +82,7 @@ class X509Test : public ::testing::Test { ASSERT_GT(createTempFILEpath(csr_path), 0u); ASSERT_GT(createTempFILEpath(out_path), 0u); ASSERT_GT(createTempFILEpath(signkey_path), 0u); + ASSERT_GT(createTempFILEpath(der_cert_path), 0u); bssl::UniquePtr pkey(EVP_PKEY_new()); ASSERT_TRUE(pkey); @@ -68,6 +103,11 @@ class X509Test : public ::testing::Test { ASSERT_TRUE(in_file); ASSERT_TRUE(PEM_write_X509(in_file.get(), x509.get())); + // Write DER certificate + ScopedFILE der_file(fopen(der_cert_path, "wb")); + ASSERT_TRUE(der_file); + ASSERT_TRUE(i2d_X509_fp(der_file.get(), x509.get())); + bssl::UniquePtr csr(X509_REQ_new()); ASSERT_TRUE(csr); X509_REQ_set_pubkey(csr.get(), pkey.get()); @@ -83,11 +123,13 @@ class X509Test : public ::testing::Test { RemoveFile(csr_path); RemoveFile(out_path); RemoveFile(signkey_path); + RemoveFile(der_cert_path); } char in_path[PATH_MAX]; char csr_path[PATH_MAX]; char out_path[PATH_MAX]; char signkey_path[PATH_MAX]; + char der_cert_path[PATH_MAX]; }; @@ -113,6 +155,31 @@ TEST_F(X509Test, X509ToolModulusTest) { ASSERT_TRUE(result); } +// Test -subject +TEST_F(X509Test, X509ToolSubjectTest) { + args_list_t args = {"-in", in_path, "-subject"}; + bool result = X509Tool(args); + ASSERT_TRUE(result); +} + +// Test -subject_hash and -subject_hash_old +TEST_F(X509Test, X509ToolSubjectHashTest) { + args_list_t args = {"-in", in_path, "-subject_hash"}; + bool result = X509Tool(args); + ASSERT_TRUE(result); + + args = {"-in", in_path, "-subject_hash_old"}; + result = X509Tool(args); + ASSERT_TRUE(result); +} + +// Test -fingerprint +TEST_F(X509Test, X509ToolFingerprintTest) { + args_list_t args = {"-in", in_path, "-fingerprint"}; + bool result = X509Tool(args); + ASSERT_TRUE(result); +} + // Test signkey TEST_F(X509Test, X509ToolSignkeyTest) { args_list_t args = {"-in", in_path, "-signkey", signkey_path}; @@ -134,6 +201,24 @@ TEST_F(X509Test, X509ToolDatesTest) { ASSERT_TRUE(result); } +// Test -enddate +TEST_F(X509Test, X509ToolEnddateTest) { + args_list_t args = {"-in", in_path, "-enddate"}; + bool result = X509Tool(args); + ASSERT_TRUE(result); +} + +// Test -inform +TEST_F(X509Test, X509ToolInformTest) { + args_list_t args = {"-in", der_cert_path, "-inform", "DER"}; + bool result = X509Tool(args); + ASSERT_TRUE(result); + + args = {"-in", in_path, "-inform", "PEM"}; + result = X509Tool(args); + ASSERT_TRUE(result); +} + // Test -checkend TEST_F(X509Test, X509ToolCheckendTest) { args_list_t args = {"-in", in_path, "-checkend", "3600"}; @@ -177,10 +262,9 @@ TEST_F(X509OptionUsageErrorsTest, MutuallyExclusiveOptionsTests) { } } -// Test missing -in required option and test -req without -signkey +// Test -req without -signkey TEST_F(X509OptionUsageErrorsTest, RequiredOptionTests) { std::vector> testparams = { - {"-out", "output.pem"}, {"-in", in_path, "-req"}, }; for (const auto& args : testparams) { @@ -188,14 +272,15 @@ TEST_F(X509OptionUsageErrorsTest, RequiredOptionTests) { } } -// Test argument errors for -days: !<0 || non-integer, -checkend: !<=0 || non-integer +// Test argument errors for -days: !<0 || non-integer, -checkend: !<=0 || non-integer, -inform != {DER, PEM} TEST_F(X509OptionUsageErrorsTest, DaysAndCheckendArgTests) { std::vector> testparams = { {"-in", in_path, "-checkend", "abc"}, {"-in", in_path, "-checkend", "-1"}, {"-in", in_path, "-signkey", signkey_path, "-days", "abc"}, {"-in", in_path, "-signkey", signkey_path, "-days", "0"}, - {"-in", in_path, "-signkey", signkey_path, "-days", "-1.7"} + {"-in", in_path, "-signkey", signkey_path, "-days", "-1.7"}, + {"-in", in_path, "-inform", "RANDOM"} }; for (const auto& args : testparams) { TestOptionUsageErrors(args); @@ -224,6 +309,7 @@ class X509ComparisonTest : public ::testing::Test { ASSERT_GT(createTempFILEpath(out_path_tool), 0u); ASSERT_GT(createTempFILEpath(out_path_openssl), 0u); ASSERT_GT(createTempFILEpath(signkey_path), 0u); + ASSERT_GT(createTempFILEpath(der_cert_path), 0u); x509.reset(CreateAndSignX509Certificate()); ASSERT_TRUE(x509); @@ -232,6 +318,10 @@ class X509ComparisonTest : public ::testing::Test { ASSERT_TRUE(in_file); ASSERT_TRUE(PEM_write_X509(in_file.get(), x509.get())); + ScopedFILE der_file(fopen(der_cert_path, "wb")); + ASSERT_TRUE(der_file); + ASSERT_TRUE(i2d_X509_fp(der_file.get(), x509.get())); + bssl::UniquePtr pkey(EVP_PKEY_new()); ASSERT_TRUE(pkey); bssl::UniquePtr rsa(RSA_new()); @@ -260,6 +350,7 @@ class X509ComparisonTest : public ::testing::Test { RemoveFile(out_path_tool); RemoveFile(out_path_openssl); RemoveFile(signkey_path); + RemoveFile(der_cert_path); } } @@ -268,6 +359,7 @@ class X509ComparisonTest : public ::testing::Test { char out_path_tool[PATH_MAX]; char out_path_openssl[PATH_MAX]; char signkey_path[PATH_MAX]; + char der_cert_path[PATH_MAX]; bssl::UniquePtr x509; bssl::UniquePtr csr; const char* tool_executable_path; @@ -276,6 +368,21 @@ class X509ComparisonTest : public ::testing::Test { std::string openssl_output_str; }; +// normalize_subject extracts the subject line from |input|. It removes all +// whitespaces from the subject line and replaces it in |input|. +static std::string normalize_subject(std::string input) { + size_t subject_start = input.find("subject="); + if (subject_start != std::string::npos) { + size_t line_end = input.find('\n', subject_start); + if (line_end != std::string::npos) { + std::string subject_line = input.substr(subject_start, line_end - subject_start); + subject_line.erase(remove(subject_line.begin(), subject_line.end(), ' '), subject_line.end()); + input.replace(subject_start, line_end - subject_start, subject_line); + } + } + return input; +} + // Certificate boundaries const std::string CERT_BEGIN = "-----BEGIN CERTIFICATE-----"; const std::string CERT_END = "-----END CERTIFICATE-----"; @@ -325,6 +432,73 @@ TEST_F(X509ComparisonTest, X509ToolCompareModulusOpenSSL) { ASSERT_EQ(tool_output_str, openssl_output_str); } +// Test against OpenSSL output "openssl x509 -in file -subject" +TEST_F(X509ComparisonTest, X509ToolCompareSubjectOpenSSL) { + std::string tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -subject > " + out_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -subject > " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + // OpenSSL master and versions <= 3.2 have differences in spacing for the subject field + tool_output_str = normalize_subject(tool_output_str); + openssl_output_str = normalize_subject(openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); + + tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -subject -out " + out_path_tool; + openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -subject -out " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + // OpenSSL master and versions <= 3.2 have differences in spacing for the subject field + tool_output_str = normalize_subject(tool_output_str); + openssl_output_str = normalize_subject(openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} + +// Test against OpenSSL output "openssl x509 -in file -fingerprint -subject_hash -subject_hash_old" +TEST_F(X509ComparisonTest, X509ToolCompareFingerprintOpenSSL) { + std::string tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -fingerprint -subject_hash -subject_hash_old > " + out_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -fingerprint -subject_hash -subject_hash_old > " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); + + tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -fingerprint -subject_hash -subject_hash_old -out " + out_path_tool; + openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -fingerprint -subject_hash -subject_hash_old -out " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} + +// Test against OpenSSL output "openssl x509 -in file -noout -subject -fingerprint" +TEST_F(X509ComparisonTest, X509ToolCompareSubjectFingerprintOpenSSL) { + std::string tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -noout -subject -fingerprint > " + out_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -noout -subject -fingerprint > " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + // OpenSSL master and versions <= 3.2 have differences in spacing for the subject field + tool_output_str = normalize_subject(tool_output_str); + openssl_output_str = normalize_subject(openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); + + tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -noout -subject -fingerprint -out " + out_path_tool; + openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -noout -subject -fingerprint -out " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + // OpenSSL master and versions <= 3.2 have differences in spacing for the subject field + tool_output_str = normalize_subject(tool_output_str); + openssl_output_str = normalize_subject(openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} + // Test against OpenSSL output "openssl x509 -in in_file -checkend 0" TEST_F(X509ComparisonTest, X509ToolCompareCheckendOpenSSL) { std::string tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -checkend 0 > " + out_path_tool; @@ -367,3 +541,71 @@ TEST_F(X509ComparisonTest, X509ToolCompareDatesNooutOpenSSL) { ASSERT_EQ(tool_output_str, openssl_output_str); } + +// Test against OpenSSL output "openssl x509 -in file -dates -enddate", notAfter date should only be printed out once +TEST_F(X509ComparisonTest, X509ToolCompareDatesEnddateOpenSSL) { + std::string tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -dates -enddate > " + out_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -dates -enddate > " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); + + tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -dates -enddate -out " + out_path_tool; + openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -dates -enddate -out " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} + +// Test against OpenSSL output "openssl x509 -in file -inform DER -enddate" +TEST_F(X509ComparisonTest, X509ToolCompareInformDEREnddateOpenSSL) { + std::string tool_command = std::string(tool_executable_path) + " x509 -in " + der_cert_path + " -inform DER -enddate > " + out_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " x509 -in " + der_cert_path + " -inform DER -enddate > " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); + + tool_command = std::string(tool_executable_path) + " x509 -in " + der_cert_path + " -inform DER -enddate -out " + out_path_tool; + openssl_command = std::string(openssl_executable_path) + " x509 -in " + der_cert_path + " -inform DER -enddate -out " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} + +// Test against OpenSSL output "openssl x509 -in file -inform DER -enddate" +TEST_F(X509ComparisonTest, X509ToolCompareInformPEMEnddateOpenSSL) { + std::string tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -inform PEM -enddate > " + out_path_tool; + std::string openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -inform PEM -enddate > " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); + + tool_command = std::string(tool_executable_path) + " x509 -in " + in_path + " -inform PEM -enddate -out " + out_path_tool; + openssl_command = std::string(openssl_executable_path) + " x509 -in " + in_path + " -inform PEM -enddate -out " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +} + +// Test against OpenSSL output reading from stdin "openssl x509 -fingerprint -dates" +TEST_F(X509ComparisonTest, X509ToolCompareStdinFingerprintDatesOpenSSL) { + std::string tool_command = "cat " + std::string(in_path) + " | " + std::string(tool_executable_path) + " x509 -fingerprint -dates > " + out_path_tool; + std::string openssl_command = "cat " + std::string(in_path) + " | " + std::string(openssl_executable_path) + " x509 -fingerprint -dates > " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); + + tool_command = "cat " + std::string(in_path) + " | " + std::string(tool_executable_path) + " x509 -fingerprint -dates -out " + out_path_tool; + openssl_command = "cat " + std::string(in_path) + " | " + std::string(openssl_executable_path) + " x509 -fingerprint -dates -out " + out_path_openssl; + + RunCommandsAndCompareOutput(tool_command, openssl_command, out_path_tool, out_path_openssl, tool_output_str, openssl_output_str); + + ASSERT_EQ(tool_output_str, openssl_output_str); +}