| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <inttypes.h> |
| |
| #include <iostream> |
| #include <map> |
| #include <set> |
| #include <string> |
| |
| #include "base/at_exit.h" |
| #include "base/base_paths.h" |
| #include "base/command_line.h" |
| #include "base/containers/span.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "crypto/openssl_util.h" |
| #include "crypto/sha2.h" |
| #include "net/cert/root_store_proto_full/root_store.pb.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/boringssl/src/include/openssl/bio.h" |
| #include "third_party/boringssl/src/include/openssl/err.h" |
| #include "third_party/boringssl/src/include/openssl/pem.h" |
| #include "third_party/protobuf/src/google/protobuf/text_format.h" |
| |
| using chrome_root_store::RootStore; |
| |
| namespace { |
| |
| // Returns a map from hex-encoded SHA-256 hash to DER certificate, or |
| // `absl::nullopt` if not found. |
| absl::optional<std::map<std::string, std::string>> DecodeCerts( |
| base::StringPiece in) { |
| // TODO(https://crbug.com/1216547): net/cert/pem.h has a much nicer API, but |
| // it would require some build refactoring to avoid a circular dependency. |
| // This is assuming that the chrome trust store code goes in |
| // net/cert/internal, which it may not. |
| bssl::UniquePtr<BIO> bio(BIO_new_mem_buf(in.data(), in.size())); |
| if (!bio) { |
| return absl::nullopt; |
| } |
| std::map<std::string, std::string> certs; |
| for (;;) { |
| char* name; |
| char* header; |
| unsigned char* data; |
| long len; |
| if (!PEM_read_bio(bio.get(), &name, &header, &data, &len)) { |
| uint32_t err = ERR_get_error(); |
| if (ERR_GET_LIB(err) == ERR_LIB_PEM && |
| ERR_GET_REASON(err) == PEM_R_NO_START_LINE) { |
| // Found the last PEM block. |
| break; |
| } |
| LOG(ERROR) << "Error reading PEM."; |
| return absl::nullopt; |
| } |
| bssl::UniquePtr<char> scoped_name(name); |
| bssl::UniquePtr<char> scoped_header(header); |
| bssl::UniquePtr<unsigned char> scoped_data(data); |
| if (strcmp(name, "CERTIFICATE") != 0) { |
| LOG(ERROR) << "Found PEM block of type " << name |
| << " instead of CERTIFICATE"; |
| return absl::nullopt; |
| } |
| std::string sha256_hex = |
| base::ToLowerASCII(base::HexEncode(crypto::SHA256Hash( |
| base::make_span(data, base::checked_cast<size_t>(len))))); |
| certs[sha256_hex] = std::string(data, data + len); |
| } |
| return std::move(certs); |
| } |
| |
| absl::optional<RootStore> ReadTextRootStore( |
| const base::FilePath& root_store_path, |
| const base::FilePath& certs_path) { |
| std::string root_store_text; |
| if (!base::ReadFileToString(base::MakeAbsoluteFilePath(root_store_path), |
| &root_store_text)) { |
| LOG(ERROR) << "Could not read " << root_store_path; |
| return absl::nullopt; |
| } |
| |
| RootStore root_store; |
| if (!google::protobuf::TextFormat::ParseFromString(root_store_text, |
| &root_store)) { |
| LOG(ERROR) << "Could not parse " << root_store_path; |
| return absl::nullopt; |
| } |
| |
| std::map<std::string, std::string> certs; |
| if (!certs_path.empty()) { |
| std::string certs_data; |
| if (!base::ReadFileToString(base::MakeAbsoluteFilePath(certs_path), |
| &certs_data)) { |
| LOG(ERROR) << "Could not read " << certs_path; |
| return absl::nullopt; |
| } |
| auto certs_opt = DecodeCerts(certs_data); |
| if (!certs_opt) { |
| LOG(ERROR) << "Could not decode " << certs_path; |
| return absl::nullopt; |
| } |
| certs = std::move(*certs_opt); |
| } |
| |
| // Replace the filenames with the actual certificate contents. |
| for (auto& anchor : *root_store.mutable_trust_anchors()) { |
| if (anchor.certificate_case() != |
| chrome_root_store::TrustAnchor::kSha256Hex) { |
| continue; |
| } |
| |
| auto iter = certs.find(anchor.sha256_hex()); |
| if (iter == certs.end()) { |
| LOG(ERROR) << "Could not find certificate " << anchor.sha256_hex(); |
| return absl::nullopt; |
| } |
| |
| // Remove the certificate from `certs`. This both checks for duplicate |
| // certificates and allows us to check for unused certificates later. |
| anchor.set_der(std::move(iter->second)); |
| certs.erase(iter); |
| } |
| |
| if (!certs.empty()) { |
| LOG(ERROR) << "Unused certificate (SHA-256 hash " << certs.begin()->first |
| << ") in " << certs_path; |
| return absl::nullopt; |
| } |
| |
| return std::move(root_store); |
| } |
| |
| // Returns true if file was correctly written, false otherwise. |
| bool WriteRootCppFile(const RootStore& root_store, |
| const base::FilePath cpp_path) { |
| // Root store should have at least one trust anchors. |
| CHECK_GT(root_store.trust_anchors_size(), 0); |
| |
| std::string string_to_write = |
| "// This file is auto-generated, DO NOT EDIT.\n\n"; |
| |
| for (int i = 0; i < root_store.trust_anchors_size(); i++) { |
| const auto& anchor = root_store.trust_anchors(i); |
| // Every trust anchor at this point should have a DER. |
| CHECK(!anchor.der().empty()); |
| std::string der = anchor.der(); |
| |
| base::StringAppendF(&string_to_write, |
| "constexpr uint8_t kChromeRootCert%d[] = {", i); |
| |
| // Convert each character to hex representation, escaped. |
| for (auto c : der) { |
| base::StringAppendF(&string_to_write, "0x%02xu,", |
| static_cast<uint8_t>(c)); |
| } |
| |
| // End struct |
| string_to_write += "};\n"; |
| } |
| |
| string_to_write += "constexpr ChromeRootCertInfo kChromeRootCertList[] = {\n"; |
| |
| for (int i = 0; i < root_store.trust_anchors_size(); i++) { |
| base::StringAppendF(&string_to_write, " {kChromeRootCert%d},\n", i); |
| } |
| string_to_write += "};"; |
| |
| base::StringAppendF(&string_to_write, |
| "\n\n\nstatic const int64_t kRootStoreVersion = %" PRId64 |
| ";\n", |
| root_store.version_major()); |
| if (!base::WriteFile(cpp_path, string_to_write)) { |
| return false; |
| } |
| return true; |
| } |
| |
| // Returns true if file was correctly written, false otherwise. |
| bool WriteEvCppFile(const RootStore& root_store, |
| const base::FilePath cpp_path) { |
| // There should be at least one EV root. |
| CHECK_GT(root_store.trust_anchors_size(), 0); |
| |
| std::string string_to_write = |
| "// This file is auto-generated, DO NOT EDIT.\n\n" |
| "static const EVMetadata kEvRootCaMetadata[] = {\n"; |
| |
| for (auto& anchor : root_store.trust_anchors()) { |
| // Every trust anchor at this point should have a DER. |
| CHECK(!anchor.der().empty()); |
| if (anchor.ev_policy_oids_size() == 0) { |
| // The same input file is used for the Chrome Root Store and EV enabled |
| // certificates. Skip anchors that have no EV policy OIDs when generating |
| // the EV include file. |
| continue; |
| } |
| |
| std::string sha256_hash = crypto::SHA256HashString(anchor.der()); |
| |
| // Begin struct. Assumed type of EVMetadata: |
| // |
| // struct EVMetadata { |
| // static const size_t kMaxOIDsPerCA = 2; |
| // SHA256HashValue fingerprint; |
| // const base::StringPiece policy_oids[kMaxOIDsPerCA]; |
| // }; |
| string_to_write += " {\n"; |
| string_to_write += " {{"; |
| |
| int wrap_count = 0; |
| for (auto c : sha256_hash) { |
| if (wrap_count != 0) { |
| if (wrap_count % 11 == 0) { |
| string_to_write += ",\n "; |
| } else { |
| string_to_write += ", "; |
| } |
| } |
| base::StringAppendF(&string_to_write, "0x%02x", static_cast<uint8_t>(c)); |
| wrap_count++; |
| } |
| |
| string_to_write += "}},\n"; |
| string_to_write += " {\n"; |
| |
| // struct expects exactly two policy oids, and we can only support 1 or 2 |
| // policy OIDs. These checks will need to change if we ever merge the EV and |
| // Chrome Root Store textprotos. |
| const int kMaxPolicyOids = 2; |
| int oids_size = anchor.ev_policy_oids_size(); |
| std::string hexencode_hash = |
| base::HexEncode(sha256_hash.data(), sha256_hash.size()); |
| if (oids_size > kMaxPolicyOids) { |
| PLOG(ERROR) << hexencode_hash << " has too many OIDs!"; |
| return false; |
| } |
| for (int i = 0; i < kMaxPolicyOids; i++) { |
| std::string oid; |
| if (i < oids_size) { |
| oid = anchor.ev_policy_oids(i); |
| } |
| string_to_write += " \"" + oid + "\",\n"; |
| } |
| |
| // End struct |
| string_to_write += " },\n"; |
| string_to_write += " },\n"; |
| } |
| string_to_write += "};\n"; |
| if (!base::WriteFile(cpp_path, string_to_write)) { |
| PLOG(ERROR) << "Error writing cpp include file"; |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| int main(int argc, char** argv) { |
| base::AtExitManager at_exit_manager; |
| base::CommandLine::Init(argc, argv); |
| |
| logging::LoggingSettings settings; |
| settings.logging_dest = |
| logging::LOG_TO_SYSTEM_DEBUG_LOG | logging::LOG_TO_STDERR; |
| logging::InitLogging(settings); |
| |
| crypto::EnsureOpenSSLInit(); |
| |
| base::CommandLine& command_line = *base::CommandLine::ForCurrentProcess(); |
| base::FilePath proto_path = command_line.GetSwitchValuePath("write-proto"); |
| base::FilePath root_store_cpp_path = |
| command_line.GetSwitchValuePath("write-cpp-root-store"); |
| base::FilePath ev_roots_cpp_path = |
| command_line.GetSwitchValuePath("write-cpp-ev-roots"); |
| base::FilePath root_store_path = |
| command_line.GetSwitchValuePath("root-store"); |
| base::FilePath certs_path = command_line.GetSwitchValuePath("certs"); |
| |
| if ((proto_path.empty() && root_store_cpp_path.empty() && |
| ev_roots_cpp_path.empty()) || |
| root_store_path.empty() || command_line.HasSwitch("help")) { |
| std::cerr << "Usage: root_store_tool " |
| << "--root-store=TEXTPROTO_FILE " |
| << "[--certs=CERTS_FILE] " |
| << "[--write-proto=PROTO_FILE] " |
| << "[--write-cpp-root-store=CPP_FILE] " |
| << "[--write-cpp-ev-roots=CPP_FILE] " << std::endl; |
| return 1; |
| } |
| |
| absl::optional<RootStore> root_store = |
| ReadTextRootStore(root_store_path, certs_path); |
| if (!root_store) { |
| return 1; |
| } |
| |
| // TODO(https://crbug.com/1216547): Figure out how to use the serialized |
| // proto to support component update. |
| // components/resources/ssl/ssl_error_assistant/push_proto.py |
| // does it through a GCS bucket (I think) so that might be an option. |
| if (!proto_path.empty()) { |
| std::string serialized; |
| if (!root_store->SerializeToString(&serialized)) { |
| LOG(ERROR) << "Error serializing root store proto" |
| << root_store->DebugString(); |
| return 1; |
| } |
| if (!base::WriteFile(proto_path, serialized)) { |
| PLOG(ERROR) << "Error writing serialized proto root store"; |
| return 1; |
| } |
| } |
| |
| if (!root_store_cpp_path.empty() && |
| !WriteRootCppFile(*root_store, root_store_cpp_path)) { |
| PLOG(ERROR) << "Error writing root store C++ include file"; |
| return 1; |
| } |
| if (!ev_roots_cpp_path.empty() && |
| !WriteEvCppFile(*root_store, ev_roots_cpp_path)) { |
| PLOG(ERROR) << "Error writing EV roots C++ include file"; |
| return 1; |
| } |
| |
| return 0; |
| } |